いつもはLaravel(PHP)かASP.NET MVC(C#)を使っていますが、たまにDjangoを使う際の思い出しメモ。
確認したいこと
私の知りたいことは、
- Djangoでの基本お作法がどうなっているのか?(CoCとかへの対応レベル)
 - いわゆるMVC (MTV)はどこに、どのように記述するのか?
 - モデルはどうやって記述する?
 - View(Controller的なもの)はどうやって記述する?
 - Template(View的なもの)はどうやって記述する?共通部分と個別部分は?
 - ルーティングはどうやって設定する?
 - WebAPIは必須なのでjsonどうやって返すの?
 
と言ったあたりです。
そういう意味では、ほぼ、こちらの記事に網羅していただいているのですが(ありがたいことです)、私のニーズとは少々違うところもあるので、自分なりにまとめてみます。
他のフレームワーク利用者がとまどうこと
他のフレームワークを利用しているがゆえに???と思うことが多いので、既に他のフレームワークを利用している人はあらかじめ違いを理解しておくと何かと楽です。
MVCではなくMTVモデル
DjangoはMVCならぬ、MTVモデルというのを採用しています。
ただ、モデルが違うというよりVC部の呼び名が違うだけというイメージなので特に新しい概念は必要無い感じです。
MVCとMTVの対応は下記のような感じでしょうか。
- M ⇒ M(Model)
 - V ⇒ T(Template)
 - C ⇒ View
 
Viwe(views.py)をいじる行為はMVCにおけるConrollerをいじっている雰囲気で、TemplateはMVCのView(HTMLの記述)をいじる感じです。
プロジェクト、アプリケーションという概念がある
プロジェクトという大きな概念に、アプリケーションを含むという概念のようです。
全体に共通する設定はプロジェクトというスコープで設定し、各アプリケーションの設定や記述はアプリケーションで行うイメージです。
個人的にはVisualStudioのソリューションとプロジェクトの関係のような印象を受けました。
ディレクトリ構造が(説明上)少々分かりにくい
これは慣れの問題だと思いますが、例えば、django_testという名前のプロジェクトを作成すると、下記のようなファイル群が生成されます。
django_test/
    manage.py
    django_test/
        __init__.py
        settings.py
        urls.py
        wsgi.py
プロジェクト全体を保持するディレクトリ(最上位のdjango_test)と、プロジェクトの関連ファイルを保持する同名のサブディレクトリが生成されるため、単にdjango_testディレクトリと言った際、どちらのdjango_testディレクトリかわかりにくいという傾向があります。
なお、本記事における操作は最上位のdjango_testをカレンドディレクトリとしての操作となります。
form(の自動生成)がちょっと特殊
この記事では取り扱いませんが、Djangoでは「formを自動生成する」という文化があるため、それを行うためのFormクラスやModelFormクラスというものが存在します。「formを自動生成する」という前知識がないと、???となってしまいます。
開発環境
すでにPython環境(3.x)があるものとして先に進みます。
私はMacでvenvで仮想環境を作って利用しています。環境セットアップはこちらをどうぞ。
pythonのバージョンは3.6.1で、pip freezeの結果は下記のような感じです。
pip freeze
Django==1.11
django-bootstrap-form==3.2.1
PyMySQL==0.7.11
pytz==2017.2
準備と確認
具体的なコーディングに入る前に、プロジェクトの作成や設定を行います。
プロジェクトの作成
まずはDjangoのプロジェクトを作成します。下記コマンドを実行すると、django_testというディレクトリが作成され、必要なファイルが生成されます。
django-admin.py startproject django_test
とりあえず動くか確認
この時点でとりあえず動くか確認してみましょう。開発用サーバを起動します。
python manage.py runserver
Djangoではmanage.pyを各種制御コマンドとして利用します。Laravelのartisanみたいな感じ。
起動したら下記URLにアクセスしてみます。
そっけない画面に[It worked!]って出ていればOKです。
migrateが出来てないぞ!!!的な警告が表示されますが、後で対応するので問題ありません。気になる人は先に、下記のmigrateを実行してみて下さい。
確認が完了したら Ctr + C にて、開発サーバを終了させておきます。
データベースの設定
では、データベースを利用するための設定をします。
Djangoでは標準でSQLiteを利用するようになっていますが、MySQLを利用したいと思います。
必要なパッケージのインストール
続いてPythonからMySQLを利用するために必要なパッケージをインストールします。
いろいろ種類があるようですが、PyMySQLを利用します。
pip install PyMySQL
settings.pyの編集
パッケージがうまく入ったら、データベースのアクセス情報を設定します。Djangoではプロジェクトディレクトリ以下(ここではdjango_test)のsettings.pyにてデータベースの接続情報を設定します。
DATABASESを下記のような記述にします。データベース名、アクセス情報は適宜変更してください。
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'djangodb',
        'USER': 'root',
        'PASSWORD': 'root',
        'HOST': '',
        'PORT': '',
    }
}
当然ですが、MySQLが稼働しており、djangodbというデータベースが作成されていることが前提となっています。
また、プロジェクトがPyMySQLパッケージを読み込むように、settings.pyの上部に下記の記述を追加します。
import pymysql
pymysql.install_as_MySQLdb()
本当は何処に書くのがいいのか。誰か教えてください。
マイグレーションの実行
データベースの接続情報を設定したのでマイグレーションを実行してみます。
python manage.py migrate
データベースの設定がうまくいっていれば正しく実行されるはずです。エラーだと各種設定を確認してください。
正しく実行されると、下記のテーブルが作成されたようです。結構たくさんありませんね。
+----------------------------+
| Tables_in_djangodb         |
+----------------------------+
| auth_group                 |
| auth_group_permissions     |
| auth_permission            |
| auth_user                  |
| auth_user_groups           |
| auth_user_user_permissions |
| django_admin_log           |
| django_content_type        |
| django_migrations          |
| django_session             |
+----------------------------+
ついでにLANGUAGE_CODEとTIME_ZONEもいじっておく
settings.pyをいじったついでに他の設定もいじっておきます。
LANGUAGE_CODE = 'ja'
TIME_ZONE = 'Asia/Tokyo'
この時点でrunserverを実行し、再度 http://localhost:8000 を確認するとメッセージが日本語化されているはずです。
アプリケーションを作成する
プロジェクト全体にかかわる設定が終わったのでアプリケーションを作成します。
ここではsampleという名のアプリケーションを作成します。
アプリケーションの作成
下記コマンドを実行しアプリケーションを作成します。
python manage.py startapp sample
実行するとsampleというディレクトリが追加され、それ以下にアプリケーション関連ファイルが生成されます。
階層構造は下記のようになっているはずです。
django_test/
    sample/
        __init__.py
        admin.py
        apps.py
        migrations/
            __init__.py
        models.py
        tests.py
        views.py
    manage.py
    django_test/
アプリケーションの追加
アプリケーションを作成したら、それをプロジェクト?が認識できるようにsettings.pyのINSTALLED_APPSに追加します。
この辺は「おまじない」系だと思うので、文句を言わずに進めます。
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
+   'sample',
]
モデルの定義
やっとコーディングの開始です。何から書いてもいいのですが、まずはモデルから手を付けます。
モデルを記述する
sample以下に生成されたmodels.pyにモデルを記述します。
ここでは会員情報の格納をイメージしたMemberモデルを定義してみます。なお、この記述はマイグレーションファイル生成時のテーブル生成情報として利用されされます。
既存テーブルを利用する方法もあるようです。
class Member(models.Model):
	name = models.CharField('氏名', max_length=255)
	email = models.CharField('E-Mail', max_length=255)
	age = models.IntegerField('年齢', blank=True, default=0)
	def __str__(self):
		return self.name
マイグレーションファイルの作成と実行
では、マイグレーションファイルを生成し、マイグレートを実行します。
モデルを生成したアプリケーション名を指定して、マイグレーションファイルを生成します。
python manage.py makemigrations sample
python manage.py migrate
モデルに変更を加えるたび、この作業を行います。
マイグレートが成功したようなので、生成されたテーブルを見ています。
どうやら、テーブル名は(複数形とかじゃなく)applicationname_tablenameという命名になるようですね。
+----------------------------+
| Tables_in_djangodb         |
+----------------------------+
| auth_group                 |
| auth_group_permissions     |
| auth_permission            |
| auth_user                  |
| auth_user_groups           |
| auth_user_user_permissions |
| django_admin_log           |
| django_content_type        |
| django_migrations          |
| django_session             |
| sample_member              |
+----------------------------+
中身を見てみます。
default値が0になってないようですが、細かなことは気にせず、先に進みます。
+-------+--------------+------+-----+---------+----------------+
| Field | Type         | Null | Key | Default | Extra          |
+-------+--------------+------+-----+---------+----------------+
| id    | int(11)      | NO   | PRI | NULL    | auto_increment |
| name  | varchar(255) | NO   |     | NULL    |                |
| email | varchar(255) | NO   |     | NULL    |                |
| age   | int(11)      | NO   |     | NULL    |                |
+-------+--------------+------+-----+---------+----------------+
管理サイトを利用する
Djangoにはスキャフォールドはありませんが、モデルを管理する管理サイトが最初から定義されています。
DBに直接データをINSERTしてもいいのですが、せっかくなので管理サイトを有効にし、利用してみます。
データを投入するFixtureという仕組みもあります。
ログインユーザーの作成
何もしなくても http://localhost:8000/admin にアクセスすると、管理サイト自体にはアクセスできますが、ログインするユーザーとパスワードが定義されていないため利用できません。なので、それを生成します。
開発用サーバが起動していない場合はrunserverで起動させて下さい。
python manage.py createsuperuser
必要に応じてユーザー名、メールアドレス、パスワードを設定して下さい。
完了したら生成されたユーザーとパスワードでログインしてみます。
管理対象テーブルを追加する
管理サイトにログインしてみるとグループとユーザーをいう項目はありますが、Memberは見当たりません。
管理対象とするには、管理対象として登録する必要があるようです。
sample以下のadmin.pyを下記のようにします。
from django.contrib import admin
from sample.models import Member
# Register your models here.
admin.site.register(Member)
再度管理画面にログイン(更新)すると、membersという記述が追加されています。
では、後で表示に使いたいのでデータを追加しておきましょう。
表示させてみる(準備編)
登録したデータを表示させる前に、どのような仕組みで表示が行われるのかを調べてみます。
表示のためには、
- テンプレート(HTML)の定義
 - Viewの定義(MVCでいうコントローラー)
 - ルーティングの定義
 
という流れになることが一般的なようですが、まずはテンプレートを利用せず、Viewとルーティング定義のみを利用してみます。
表示の仕組み1(とりあえず何かを表示してみる)
views.pyをいじる
ではViewを定義します。具体的にはsample以下のviews.pyに記述を追加します。
とりあえずindexというメソッドに"Indexです!"と直接返す記述をしました。
from django.shortcuts import render
from django.http import HttpResponse
# Create your views here.
def index(request):
	#とりあえず文字列を返す。
	return HttpResponse("Indexです!")
ルーティングを定義
続いてURLとメソッドをマップします。
なお、Djangoでのルーティングの定義は、1つのファイルに全てを記述するのではなく、
- 各アプリケーションフォルダにurls.pyにを生成し、アプリケーション単位でのルーティングを記述する
 - ソリューション全体のurls.pyにて各アプリケーションのurls.pyファイルをincludeする
 
というのがお作法のようなので、それに従います。
では、sample以下にurls.pyを生成し、下記のように記述します。
from django.conf.urls import url
from sample import views
urlpatterns = [
	url(r'^members/$', views.index, name='index'),
]
ルート(マッピングするURL)は、正規表現で記述するようです。めんどいといえばめんどいですが、確実といえば確実ですね。
なお、最初のr''は、''内では\等の特殊文字も普通の文字として扱いますという意味のようです。
アプリケーション内の記述は終わったので、ソリューション(django_test)以下のurls.pyに以下の記述を行います。
from django.conf.urls import url, include
from django.contrib import admin
urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^sample/', include('sample.urls', namespace='sample'))
]
定義が終わったので、runserverし、下記のURLにアクセスしてみます。
Indexです!と表示されていればOKです。
表示の仕組み2(テンプレートを利用してみる)
では、views.pyとurls.pyの関係がわかったので、テンプレート(HTML)を定義して利用してみます。
共通ファイルを作成する
Djangoにおいては、共通となるテンプレートは、
project_name/application_name/templates/base.html
というルールで配置するようです。ここでは、
django_test/sample/templates/base.html
というファイルを生成・記述します。なお、templatesディレクトリは存在しないので作成します。
私はBootstrapをよく利用するので、Bootstrapのサンプルをちょっと変更(CDNを利用するように変更)し、共通テンプレートとしました。
{% load staticfiles %}
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>{% block title %}My books{% endblock %}</title>
    <!-- Bootstrap -->
    <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
  </head>
  <body>
    <div class="container">
      {% block content %}
        {{ content }}
      {% endblock %}
    </div>
    <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
    <!-- Include all compiled plugins (below), or include individual files as needed -->
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
  </body>
</html>
ご察しの通り、上記記述の、
<title>{% block title %}My books{% endblock %}</title>
<div class="container">
      {% block content %}
        {{ content }}
      {% endblock %}
</div>
部分が必要に応じて動的に置き換わります。
staticコンテンツを置くディレクトリの定義などもできますが、ここでは割愛します。そういう意味では、{% load staticfiles %}という記述も今回の範囲では必要ないと思います。
各ページ(index.html)を記述する
各ページを記述します。Djangoでは、他のMVCフレームワークの用にView(コントローラー)名とフォルダ名との厳密な関連付けが無いようですが、ここではtemplates以下にmembersというディレクトリを作成し、その下にindex.htmlを作成・記述します。
base.htmlの読み込みと、ひとまずbase.htmlで定義したblockにマッピングするコンテンツのみ記述してみます。
{% extends "base.html" %}
{% block title %}
一覧を表示する予定
{% endblock title %}
{% block content %}
<h1>とりあえずのコンテンツ</h1>
{% endblock content %}
Djangoのテンプレートシステムでは、
{% block block_name %}{% endblock block_name %}
というルールでブロックを定義し、block_nameに対応する変数を準備することで動的な表示を実現するようです。
views.pyを個別ベースを見に行くように記述を変更する
では、先程"Indexです!"とダイレクトにレスポンスしていた記述を、作成したindex.htmlを見に行くようにviews.pyの記述を変更します。
def index(request):
	# return HttpResponse("Indexです!")
	return render(request, 'members/index.html')
編集が完了したらrunserverし、
にアクセスして正しく表示されるか確認してください。
タイトルも正しく差し替わっているでしょうか?
データを表示してみる
では、いよいよデータベースに格納したデータを表示してみます。
views.pyを再度編集
views.pyを編集し、データベースから値を取得し、テンプレートへ返すよう記述を変更します。
from django.shortcuts import render
from django.http import HttpResponse
from sample.models import Member
# Create your views here.
def index(request):
	# return HttpResponse("Indexです!")
	members = Member.objects.all().order_by('id') #値を取得
	return render(request, 'members/index.html', {'members':members}) #Templateに値を渡す
index.htmlの編集
表示する側も編集します。受け取ったデータをテーブル形式で表示してみます。
{% extends "base.html" %}
{% block title %}
一覧を表示する予定
{% endblock title %}
{% block content %}
<h3>一覧表示</h3>
<table class="table table-striped table-bordered">
	<thead>
		<tr>
			<th>ID</th>
			<th>氏名</th>
			<th>E-Mail</th>
			<th>年齢</th>
		</tr>
	</thead>
	<tbody>
		{% for member in members %}
		<tr>
			<td>{{ member.id }}</td>
			<td>{{ member.name }}</td>
			<td>{{ member.email }}</td>
			<td>{{ member.age }}</td>
		</tr>
		{% endfor %}
	</tbody>
</table>
{% endblock content %}
特に難しいはありませんが、ループ部分を、
{% for member in members %}
<tr><td>ループする内容</td></tr>
{% endfor %}
のように記述しています。
完了したら、確認して見てください。
JSONを返してみる
では最後にWebAPIを作成してみます。要はjsonでのレスポンスです。
Djangoでは残念ながら?一手間必要なようです。
なお、本格的な?APIのためにはDjango Rest Frameworkというのを使うこともできるようです。
views.pyへの記述
sample/views.pyに下記のメソッドを追加します。ここではapiというメソッドにしました(命名としては良くないです)。
取得したデータをOrderedDictに格納した上で、レスポンスするといいようです。
OrderedDictはその名のとおり、整列化されたディクショナリー型で、通常のディクショナリー型では、データの順序がバラバラになってしまうため、一度、OrderedDict型に格納するようです。
def api(request):
	members = []
	for member in Member.objects.all().order_by('id'):
		member_dict = OrderedDict([
				('id',member.id),
				('name',member.name),
				('email',member.email),
				('age',member.age),
			])
		members.append(member_dict)
	data = OrderedDict([
			('status','ok'),
			('members',members),
		])
	
	json_str = json.dumps(data, ensure_ascii=False, indent=2)
	return HttpResponse(json_str, content_type='application/json; charset=utf-8')
テンプレートは利用せず、views.pyで直接レスポンスします。最上位階層にstatusといったものも追加してみました(個人的によく利用するので)。
ルートの追加
追加したapiメソッドに対応するURLをマップします。とりあえず以下のようにしました。
from django.conf.urls import url
from sample import views
urlpatterns = [
	url(r'^members/$', views.index, name='Index'),
	url(r'^members/api/$', views.api, name='Api'),
]
それでは、runserverし、
にアクセスして、期待するjsonが帰ってくるか確認してください。
以下のようなjsonが帰ってきました。
{
  "status": "ok",
  "members": [
    {
      "id": 1,
      "name": "hoge",
      "email": "hoge@hoge.com",
      "age": 20
    },
    {
      "id": 2,
      "name": "foo",
      "email": "foo@foo.com",
      "age": 40
    }
  ]
}
今後
とりあえず基本的な動作は理解できたので、次のステップとしてはCRUDをやってみたいと思います。