もた日記

くだらないことを真面目にやる

Djangoメモ(4) : 掲示板アプリのモデルを作成

Python 3.6.4 Django 2.0.2

A Complete Beginner's Guide to Djangoのチュートリアルを参考に掲示板アプリのモデルを作成する。


掲示板アプリのモデル

チュートリアルでは下の図のようなモデルを作成する。

f:id:wonder-wall:20180305203751p:plain

それぞれのモデルの説明をまとめると以下の通り。

Board

  • Boardはカテゴリのようなもの(例:Pythonに関するTopicを投稿するBoardなど)
  • 複数のBoardがある
  • 管理者のみがBoardを作成できる

Topic

  • TopicはBoard内のスレッドようなもの
  • TopicはBoardに属する
  • Topicは一般ユーザが作成できる

Post

  • PostはTopicに対する返信
  • PostはTopicに属する
  • PostはTopicを作成したユーザ以外でも返信できる

User

  • Django built-inのユーザモデル
  • Topic, Postがどのユーザによるものか対応付ける


モデルの作成

モデルを作成するためboards/models.pyをチュートリアル通りに編集する。

from django.db import models
from django.contrib.auth.models import User


class Board(models.Model):
    name = models.CharField(max_length=30, unique=True)
    description = models.CharField(max_length=100)


class Topic(models.Model):
    subject = models.CharField(max_length=255)
    last_updated = models.DateTimeField(auto_now_add=True)
    board = models.ForeignKey(Board, related_name='topics')
    starter = models.ForeignKey(User, related_name='topics')


class Post(models.Model):
    message = models.TextField(max_length=4000)
    topic = models.ForeignKey(Topic, related_name='posts')
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(null=True)
    created_by = models.ForeignKey(User, related_name='posts')
    updated_by = models.ForeignKey(User, null=True, related_name='+')

コードについてのおおまかな説明は以下の通り。

  • 各モデルは一つのクラスで表現される。
  • 各フィールドは Field クラスのインスタンスとして表現される。
  • CharField は文字のフィールド、 DateTimeField は日時フィー ルド(フィールド一覧)。
  • インスタンス名はデータベースの列の名前として使用する。
  • auto_now_add=Trueはインスタンス作成時に現在時刻を登録する。
  • related_nameはbackwards relationに名前をつける(詳細はこのブログを参照)。
  • related_name='+'はbackwards relationを作成しない。
  • Django built-inのユーザモデルを使用するためUserは記述しない。


migrateでモデルを有効にする

まずはモデルに変更があったことを伝えるためにmakemigrationsを実行したがアプリケーションが見つからないというメッセージが表示されてしまった。

$ python manage.py makemigrations boards
App 'boards' could not be found. Is it in INSTALLED_APPS?

アプリケーションをプロジェクトに含めるには構成クラスへの参照を設定に追加する必要があるためmyproject/settings.pyINSTALLED_APPSboards.apps.BoardsConfigを追加する(BoardsConfigクラスはboards/apps.pyにある)。

# Application definition

INSTALLED_APPS = [
    'boards.apps.BoardsConfig', # 追加分
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

編集後、再実行したら下記エラーが表示された。

$ python manage.py makemigrations boards
Traceback (most recent call last):
  File "manage.py", line 15, in <module>
    execute_from_command_line(sys.argv)
  File "/home/vagrant/.pyenv/versions/venv/lib/python3.6/site-packages/django/core/management/__init__.py", line 371, in execute_from_command_line
    utility.execute()
  File "/home/vagrant/.pyenv/versions/venv/lib/python3.6/site-packages/django/core/management/__init__.py", line 347, in execute
    django.setup()
  File "/home/vagrant/.pyenv/versions/venv/lib/python3.6/site-packages/django/__init__.py", line 24, in setup
    apps.populate(settings.INSTALLED_APPS)
  File "/home/vagrant/.pyenv/versions/venv/lib/python3.6/site-packages/django/apps/registry.py", line 112, in populate
    app_config.import_models()
  File "/home/vagrant/.pyenv/versions/venv/lib/python3.6/site-packages/django/apps/config.py", line 198, in import_models
    self.models_module = import_module(models_module_name)
  File "/home/vagrant/.pyenv/versions/venv/lib/python3.6/importlib/__init__.py", line 126, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "<frozen importlib._bootstrap>", line 978, in _gcd_import
  File "<frozen importlib._bootstrap>", line 961, in _find_and_load
  File "<frozen importlib._bootstrap>", line 950, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 655, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 678, in exec_module
  File "<frozen importlib._bootstrap>", line 205, in _call_with_frames_removed
  File "/home/vagrant/django/myproject/myproject/boards/models.py", line 10, in <module>
    class Topic(models.Model):
  File "/home/vagrant/django/myproject/myproject/boards/models.py", line 13, in Topic
    board = models.ForeignKey(Board, related_name='topics')
TypeError: __init__() missing 1 required positional argument: 'on_delete'

調べたところDjango 2.0からはForeignKeyon_deleteが必須になったのでmodels.pyを以下のように編集し直す。
on_delete=models.CASCADEは削除時に関連する要素も削除する設定。ドキュメントによるとその他の値としてはPROTECT, SET_NULLなどがあるようだ。

from django.db import models
from django.contrib.auth.models import User

class Board(models.Model):
    name = models.CharField(max_length=30, unique=True)
    description = models.CharField(max_length=100)


class Topic(models.Model): 
    subject = models.CharField(max_length=255)
    last_updated = models.DateTimeField(auto_now_add=True)
    board = models.ForeignKey(Board, on_delete=models.CASCADE, related_name='topics')
    starter = models.ForeignKey(User, on_delete=models.CASCADE, related_name='topics')


class Post(models.Model):
    message = models.TextField(max_length=4000)
    topic = models.ForeignKey(Topic, on_delete=models.CASCADE, related_name='posts')
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(null=True)
    created_by = models.ForeignKey(User, on_delete=models.CASCADE, related_name='posts')
    updated_by = models.ForeignKey(User, on_delete=models.CASCADE, null=True, related_name='+')

makemigrationsを実行したところ今度は無事に成功。
boards/migrations/0001_initial.pyというファイルが作成される。このファイルは手動で微調整したいというとき以外は中身は気にしなくてよいとのこと。

$ python manage.py makemigrations boards
Migrations for 'boards':
  boards/migrations/0001_initial.py
    - Create model Board
    - Create model Post
    - Create model Topic
    - Add field topic to post
    - Add field updated_by to post

実際にデータベース(デフォルトではSQLite)に反映する前にsqlmigrateでどのようなSQLが実行されるかを確認してみる。

$ python manage.py sqlmigrate boards 0001
BEGIN;
--
-- Create model Board
--
CREATE TABLE "boards_board" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "name" varchar(30) NOT NULL UNIQUE, "description" varchar(100) NO
T NULL);

 省略

CREATE INDEX "boards_post_created_by_id_0b841038" ON "boards_post" ("created_by_id");
CREATE INDEX "boards_post_topic_id_f477c024" ON "boards_post" ("topic_id");
CREATE INDEX "boards_post_updated_by_id_76d3c48f" ON "boards_post" ("updated_by_id");
COMMIT;

migrateコマンドでデータベースに反映する。

$ python manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, boards, 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 auth.0009_alter_user_last_name_max_length... OK
  Applying boards.0001_initial... OK
  Applying sessions.0001_initial... OK

これでモデルの作成は完了。 boards以外のauth, adminなどはよく使われるためデフォルトで付属しているアプリケーション。
sqlite3でテーブルが作成されていることも確認できる。

$ sqlite3 db.sqlite3
SQLite version 3.7.17 2013-05-20 00:56:22
Enter ".help" for instructions
Enter SQL statements terminated with a ";"
sqlite> .tables
auth_group                  boards_post
auth_group_permissions      boards_topic
auth_permission             django_admin_log
auth_user                   django_content_type
auth_user_groups            django_migrations
auth_user_user_permissions  django_session
boards_board
sqlite> .schema boards_board
CREATE TABLE "boards_board" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "name" varchar(30) NOT NULL UNIQUE, "description" varchar(100) NOT NULL);
sqlite> .schema boards_post
CREATE TABLE "boards_post" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "message" text NOT NULL, "created_at" datetime NOT NULL, "updated_at" datetime NULL, "created_by_id" integer NOT NULL REFERENCES "auth_user" ("id") DEFERRABLE INITIALLY DEFERRED, "topic_id" integer NOT NULL REFERENCES "boards_topic" ("id") DEFERRABLE INITIALLY DEFERRED, "updated_by_id" integer NULL REFERENCES "auth_user" ("id") DEFERRABLE INITIALLY DEFERRED);
CREATE INDEX "boards_post_created_by_id_0b841038" ON "boards_post" ("created_by_id");
CREATE INDEX "boards_post_topic_id_f477c024" ON "boards_post" ("topic_id");
CREATE INDEX "boards_post_updated_by_id_76d3c48f" ON "boards_post" ("updated_by_id");
sqlite> .schema boards_topic
CREATE TABLE "boards_topic" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "subject" varchar(255) NOT NULL, "last_updated" datetime NOT NULL, "board_id" integer NOT NULL REFERENCES "boards_board" ("id") DEFERRABLE INITIALLY DEFERRED, "starter_id" integer NOT NULL REFERENCES "auth_user" ("id") DEFERRABLE INITIALLY DEFERRED);
CREATE INDEX "boards_topic_board_id_4462fe82" ON "boards_topic" ("board_id");
CREATE INDEX "boards_topic_starter_id_60e98681" ON "boards_topic" ("starter_id");

また、showmigrationsを実行すると現在のマイグレーションの状態が確認できる。

$ python manage.py showmigrations
admin
 [X] 0001_initial
 [X] 0002_logentry_remove_auto_add
auth
 [X] 0001_initial
 [X] 0002_alter_permission_name_max_length
 [X] 0003_alter_user_email_max_length
 [X] 0004_alter_user_username_opts
 [X] 0005_alter_user_last_login_null
 [X] 0006_require_contenttypes_0002
 [X] 0007_alter_validators_add_error_messages
 [X] 0008_alter_user_username_max_length
 [X] 0009_alter_user_last_name_max_length
boards
 [X] 0001_initial
contenttypes
 [X] 0001_initial
 [X] 0002_remove_content_type_name
sessions
 [X] 0001_initial


まとめ

  • 掲示板アプリのモデルを作成
  • INSTALLED_APPSでプロジェクトにアプリケーションを追加
  • Django 2.0からはForeignKeyon_deleteが必須
  • makemigrationsで変更をファイル形式で保存
  • sqlmigrateで実行されるSQLを確認
  • migrateでデータベースにテーブルを作成
  • showmigrationsでマイグレーションの状態を確認