sgykfjsm.github.com

DjangoをTango with Djangoで学ぶ - 3 -

DjangoをTango with Djangoで学ぶ - 2 -の続き。今回はモデルの基礎の話。

Webアプリケーションでデータを保存・参照・更新・削除という、いわゆるCRUD操作を行う場合はデータベースを利用するのが一般的で、特にリレーショナル・データベース(RDB)が利用される。RDBに対する操作はSQLを用いられる。これはまぁ一般常識。フルスタックWebアプリケーションの場合、SQLで操作するデータをModelとして定義し、それをORM(Object Relational Mapper)を使って、Modelとデータ(データベースの中にあるテーブル定義)と対応させる。Djangoでもこういった機能を提供しており、今回はこれを学習する。

さて、Tango with Djangoというテキストで開発するアプリケーションはRangoであるが、ここで改めてRangoの要件を簡単に説明とすると、以下の様になる。

  • RangoはWebサイトのURLリンク集である。
  • WebサイトのURLはCategoryエンティティが持つ分類に関する情報と紐づく
  • WebサイトのURLで参照されるページの情報(タイトル、URLそして閲覧回数)を持つ。これをPageエンティティと呼ぶ。
  • PageエンティティはCateforyエンティティを参照する。RDB的に言うと、Categoryエンティティに外部キーを持つ。
  • CategoryエンティティとPageエンティティはhousesオブジェクトを介して1 to manyの関係を持つ。下図参照。
1
[Category]-|---------<houses>-----|-<[Page]

さて、早速Modelに関するコーディングを始めていきたいところだが、その前にDjangoアプリケーションにおけるデータベースの設定を確認する。

通常、Djangoアプリケーションをセットアップした時点で以下のような設定がsettings.pyに含まれているはず。

settings.py

1
2
3
4
5
6
7
8
9
# Database
# https://docs.djangoproject.com/en/1.11/ref/settings/#databases

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
    }
}

デフォルト設定だとSQlite3を利用することになっているが、もちろんMySQLなど他のデータベースエンジンを利用することができる。その場合の設定の詳細は公式ドキュメントの”Databases”を参照すること。

では、これからモデルの定義を行なう。これまでと同様にアプリケーション固有の設定は各アプリケーションディレクトリの下で行う。今回の場合だとrango/model.pyが対象だ。

rango/model.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from django.db import models


class Category(models.Model):
    name = models.CharField(max_length=128, unique=True)

    def __str__(self):
        return self.name


class Page(models.Model):
    category = models.ForeignKey(Category)
    title = models.CharField(max_length=128)
    url = models.URLField()
    views = models.IntegerField(default=0)

    def __str__(self):
        return self.title

上のコードを見れば何となくそれぞれの意味がわかると思う。class CategoryはCategoryテーブルを表現している。もう1つのclass PageはPageテーブルを表現している。

ここで最も気をつけなければならないことは、各クラスともにmodels.Modelを継承していることだ。これは忘れてはならない。

class Categoryには1つのフィールドが定義されており、それは見ての通りChar型で最大データ長は128バイトでユニーク制約を持つ。これらはメソッド名や引数名を見たら分かると思う。class Pageについても同様だ。ここでは利用されていないが、デフォルトをdefault=で指定することもできるし、カラムがnullableかどうかも指定できる。

テーブル間の関係はone-to-one, one-to-manyそしてmany-to-manyの3つに大別できるが、models.ForeignKeyによって、one-to-manyを表現している。この場合、PageテーブルがCategoryテーブルを参照しているので、PageテーブルがManyで、CategoryテーブルがOneとなる。なお、one-to-oneOneToOneField, many-to-manyManyToManyFieldという見たままのネーミングとなっている。

それぞれのクラスには__str__が定義されている。このメソッドは各クラスのオブジェクトを文字列として扱うときに利用されるメソッドだ。これを定義しないと継承元の__str__が呼び出されて、<Category:Category object>というよく分からない文字列が出力されるので、モデルを定義する際には__str__も定義することがベストプラクティスの1つとなる。

最後に注意する点として、各クラスには暗黙的にidカラムが設定され、そのカラムはinteger NOT NULL PRIMARY KEY AUTOINCREMENTという設定がされていることだ。

とりあえずはテーブルの定義が完了したので、ここでDjangoのマイグレーションツールを試してみる。

まずはデフォルトで設定されているテーブル類のマイグレーションを行なう。「デフォルトで設定されているテーブル」とはsettings.pyの中にあるINSTALLED_APPSだ。

なお、久しぶりのDjangoコマンドの実行なので、virtualenvのactivateを忘れないようにしよう。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ python manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying sessions.0001_initial... OK

これでDjangoにとっての基本的なテーブルが出来た。次にテーブルを操作するためのスーパーユーザー(要はrootユーザーみたいなもの)を用意する。

1
2
3
4
5
6
$ python manage.py createsuperuser
Username (leave blank to use 'you'):
Email address:
Password:
Password (again):
Superuser created successfully.

つぎに、アプリケーション固有のテーブルのマイグレーション、つまり先ほど定義したrango/models.pyを元にマイグレーションを行なう。

1
2
3
4
5
$ python manage.py makemigrations rango
Migrations for 'rango':
  rango/migrations/0001_initial.py
    - Create model Category
    - Create model Page

生成されたファイルの中身を見ても良いが、より確実な確認として、生成したファイルから更に生成されるSQLを確認してみよう

1
2
3
4
5
6
7
8
9
10
11
12
$ python manage.py sqlmigrate rango 0001
BEGIN;
--
-- Create model Category
--
CREATE TABLE "rango_category" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "name" varchar(128) NOT NULL UNIQUE);
--
-- Create model Page
--
CREATE TABLE "rango_page" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "title" varchar(128) NOT NULL, "url" varchar(200) NOT NULL, "views" integer NOT NULL, "category_id" integer NOT NULL REFERENCES "rango_category" ("id"));
CREATE INDEX "rango_page_category_id_0872388a" ON "rango_page" ("category_id");
COMMIT;

改行されていないので少し見にくいが、期待通りのSQLが生成されていることがわかると思う。

makemigrationsで生成されたのはmigrateで利用されるパーツのようなもの。このパーツをDjangoアプリケーションとして組み込むためにはもう1度migrateを実行すれば良い。

1
2
3
4
5
$ python manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, rango, sessions
Running migrations:
  Applying rango.0001_initial... OK

これでrango固有のテーブルがDjangoアプリケーションに取り込まれた。今回はSQLiteなのでブラウザとして SQLite Database Browser を使うと、取り込まれている様子がわかると思う。

DjangoはShell機能を提供している。といっても実際はただのPythonのREPLではあるけど。起動方法はいつものようにmanage.pyを使う。以下の通り。

1
2
3
4
5
6
$ python manage.py shell
Python 3.5.3 (default, Sep  4 2017, 22:33:15)
[GCC 4.2.1 Compatible Apple LLVM 8.1.0 (clang-802.0.42)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>>

この起動によってプロジェクトの諸々の設定も取り込まれる。

1
2
3
4
5
6
7
8
9
10
11
12
13
$ python manage.py shell
Python 3.5.3 (default, Sep  4 2017, 22:33:15)
[GCC 4.2.1 Compatible Apple LLVM 8.1.0 (clang-802.0.42)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from rango.models import Category
>>> print(Category.objects.all())
<QuerySet []>
>>> c = Category(name="Test")
>>> c.save()
>>> print(Category.objects.all())
<QuerySet [<Category: Test>]>
>>> quit()

上で紹介したDB Browserを使うと、ちゃんとDBに”Test”が保存されていることを確認することができる。

Djangoの目を引く特徴の1つとしてWebの管理画面をデフォルトで提供していることが挙げられる。これを使って、DBの中のデータを見たり編集することができる。ここで改めてプロジェクトディレクトリにあるsettings.pyurls.pyを見ると、以下があることに気づくだろう。

settings.py

1
2
3
4
5
6
7
...
INSTALLED_APPS = [
    'django.contrib.admin',   # <- これ
...
    'rango',
]
...

urls.py

1
2
3
4
5
6
...
urlpatterns = [
...
    url(r'^admin/', admin.site.urls),  # <- これ
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
...

上記2つの設定があることを確認したらpython manage.py runserverでアプリケーションを起動してみよう。

1
$ python manage.py runserver

早速ブラウザでhttp://localhost:8000/admin/ にアクセスするとログイン画面が出てくるので、python manage.py createsuperuserで作成したユーザでログインすれば良い。もしどんなユーザを作成したか忘れてしまったのならば、改めて再作成すれば良い。

ログイン後、表示された画面にはAUTHENTICATION AND AUTHORIZATIONというタイトルの項目があるが、肝心のrangoアプリケーションのテーブルが存在しないことに気づくだろう。これはDjangoのAdmin機能がrangoアプリケーションのmodelsの存在を知らないからだ。なので、教えてあげる必要がある。rango/admin.pyに以下のように編集する。

rango/admin.py

1
2
3
4
5
6
7
8
9
--- a/rango/admin.py
+++ b/rango/admin.py
@@ -1,3 +1,5 @@
 from django.contrib import admin
+from rango.models import Category, Page

-# Register your models here.
+admin.site.register(Category)
+admin.site.register(Page)

上記のとおりに編集してWebの画面をリロードすると、RANGOという項目が追加されるはず。”Categorys”をクリックすると、先ほどShell機能を使って追加した”Test”が存在することがわかる。このような方法でもDBを確認することができるし、おそらくDjangoの開発中はこちらを利用することが多いだろう。

ところで、Webの管理画面では”Category”が”Categorys”となっていることに気づいただろうか?これは別にDjangoがtypoしているわけではなくて、モデルのメタデータのverbose_name_pluralのデフォルト設定によるもの。気になるようであれば、自分で修正することができる。以下の様に。

rango/models.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ git diff | cat -
diff --git a/rango/models.py b/rango/models.py
index c65a919..b9b694d 100644
--- a/rango/models.py
+++ b/rango/models.py
@@ -4,6 +4,9 @@ from django.db import models
 class Category(models.Model):
     name = models.CharField(max_length=128, unique=True)

+    class Meta:
+        verbose_name_plural = 'Categories'
+
     def __str__(self):
         return self.name

編集後は再度画面をリロードして確認しておくこと。

ここまでの作業でモデルが用意できた。実際の開発では、このモデルにデータを入れて動作確認やテストをしたりするわけだけど、そのためのテストデータを毎回用意するのは正直ダルい。ので、一般的にはpopulation scriptと呼ばれるダミーデータ生成のためのスクリプトを用意する。ダミーデータとは言え、ランダムな文字列を適当に入れても、あまり役に立たない。ここでは、それっぽいデータを用意してDBに格納するスクリプトを書く。populate_rango.pyという名前でプロジェクトディレクトリの直下にファイルを用意しよう。こんな感じ。

populate_rango.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
import os
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tango_with_django_project.settings')

import django
django.setup()

from rango.models import Category, Page


def populate():
    python_pages = [
        {"title": "Official Python Tutorial",  "url": "http://docs.python.org/2/tutorial"},
        {"title": "How to Think like a Computer Scientis",  "url": "http://www.greenteapress.com/thinkpython"},
        {"title": "Learn Python in 10 Minutes",  "url": "http://www.korokithakis.net/tutorials/python"},
    ]

    django_pages = [
        {"title": "Official Django Tutorial", "url": "https://docs.djangoproject.com/en/1.9/intro/tutorial01/"},
        {"title": "Django Rocks", "url": "http://djangorocks.com"},
        {"title": "How to Tango with Django", "url": "http://www.tangowithdjango.com/"},
    ]

    other_pages = [
        {"title": "Bottle", "url": "https://bottlepy.org/docs/dev/"},
        {"title": "Flask", "url": "http://flask.pocoo.org/"},
    ]

    cats = {
        "Python": {"pages": python_pages},
        "Django": {"pages": django_pages},
        "Other Frameworks": {"pages": other_pages},
    }

    for cat, cat_data in cats.items():
        c = add_cat(cat)
        for p in cat_data["pages"]:
            add_page(c, p["title"], p["url"])

    for c in Category.objects.all():
        for p in Page.objects.filter(category=c):
            print("- {0} - {1}".format(str(c), str(p)))


def add_page(cat, title, url, views=0):
    p = Page.objects.get_or_create(category=cat, title=title)[0]
    p.url = url
    p.views = views
    p.save()

    return p


def add_cat(name):
    c = Category.objects.get_or_create(name=name)[0]
    c.save()

    return c


if __name__ == '__main__':
    print("Starting Rango population script...")
    populate()

このスクリプトを簡単に解説する。まず、一番下のif __name__ == '__main__':は「ファイルを直接実行したかどうか」を判定していて、直接実行した場合には"Starting Rango population script..."をターミナルに表示して、populate()という関数を実行する。populate()のほとんどはテストデータの定義だけど、後半の数行はfooループを実行している。最初のループでは、Categoryテーブルにデータを入れて、それからPageテーブルにデータを追加している。次のループはちゃんとデータが格納されているかどうかを確認するために、テーブルからデータを取り出してターミナルに表示している。それだけ。

実際にpopulate scriptを実行してからpython manage.py runserverを実行、ブラウザでhttp://127.0.0.1:8000/admin/ にアクセスして各テーブルのページを開くと、追加したデータを確認できる。

ここまでのおさらい

DjangoにおけるModelsとはデータモデル、より直接的に言うと、データの定義や操作のこととなる。Djangoでは以下のような作業順序で行なうことが一般的だ。

  1. 自分のDjangoアプリケーションの中にmodels.pyを用意して、その中にテーブル定義をモデルクラスとして記述する。
  2. admin.pyの中に定義したモデルクラスを登録する。
  3. python manage.py makemigrations ${app_name}を実行してマイグレーションの準備を行なう。
  4. python manage.py migrateを実行してDBに対するマイグレーションを行なう。
  5. モデルの動作確認やテストのためにpopulation script(ダミーデータの生成スクリプト)を用意する。

色々な理由でDBを削除して作り直すことが発生すると思うけど、その場合はpython manage.py migrateを実行すればDBが新たに作成される。その際には、ちゃんとマイグレーションのスクリプト(アプリケーションのディレクトリにあるmigrationsディレクトリの中のスクリプト)がちゃんと存在していることを確認しておくこと。また、管理者用アカウントは`python manage.py createsuperuserを実行して作成する。