はじめに
「Djangoだったら社内に有識者がたくさんいる」って言ってたじゃないですかっー!!!!
フューチャー Advent Calendar 2019 10日目の記事です。
今年のアドベントカレンダーはガチネタ多すぎて怖いわ...
今回は、golangとかrailsとか書いてた私が、
初Pythonで突貫でシステム構築することになり、ハマったことを公開していきます。
※なお、本記事で出てくるsqlは一部formatしてます
1行で
Djangoのmodelはdefault等の設定をDBに反映してくれない!
前提環境
環境は、Python Django入門 (1)を(6)の記事まで順にこなした前提とします。
公式リファレンスじゃないけど良記事でした。
出来上がったものがhttps://github.com/kakky/mybook20 においてあるのも嬉しいですね。
(以下検証はこのコミットで実施してます。https://github.com/kakky/mybook20/commit/82e741652bfd7f82f5c0bc601e04b7585632d266)
上記をcloneしてきた場合は、事前準備として、以下を実行しておきましょう。
$ python manage.py migrate
$ python manage.py createsuperuser
$ python manage.py runserver
ここで躓いた場合は、前提となっている記事をご参照ください。
上記コマンドが書いてあるのはこの記事です。
Python Django入門 (3)
本題
Djangoのmodelはdefault等の設定をDBに反映してくれない
準備
cms/models.py
のclass Book
を以下のように変更しましょう。
class Book(models.Model):
"""書籍"""
name = models.CharField('書籍名', max_length=255, unique=True)
publisher = models.CharField('出版社', max_length=255)
page = models.IntegerField('ページ数', blank=True, default=0)
on_sale = models.BooleanField('販売中', default=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return self.name
created_at
とupdated_at
を追加し、insert, update時にそれぞれ現在時刻を埋め、on_sale
を追加しデフォルト値をTrueにしておきます。
さらに、name
に unique=True
を付け足しています。
なお、全てのカラムはデフォルトで、null=False
となっており、not null制約の挙動となります。
modelを変更したので、以下のようにmigrationファイルを作成します。
初期値を埋めろと怒られるので、適当にtimezone.now
としておきます。
$ python manage.py makemigrations
You are trying to add the field 'created_at' with 'auto_now_add=True' to book without a default; the database needs something to populate existing rows.
1) Provide a one-off default now (will be set on all existing rows)
2) Quit, and let me add a default in models.py
Select an option: 1
Please enter the default value now, as valid Python
You can accept the default 'timezone.now' by pressing 'Enter' or you can provide another value.
The datetime and django.utils.timezone modules are available, so you can do e.g. timezone.now
Type 'exit' to exit this prompt
[default: timezone.now] >>> timezone.now
migrationをかけます。
python manage.py migrate
動作確認
http://127.0.0.1:8000/admin/ に移動し、Bookを追加します。(詳細省略)
DBの中身も見てみましょう。
$ sqlite> select * from cms_book;
id|name|page|created_at|on_sale|updated_at|publisher
1|悪魔の寵児|100|2019-12-09 13:03:29.102973|1|2019-12-09 13:03:29.132151|角川文庫
created_at
updated_at入っており、良さそうです。※on_sale
は1でTrueを表します。
ではここで、DBに直接insertしてみましょう。
sqlite> insert into cms_book(name, publisher, page) values('悪魔の手毬唄', '集英社', 200);
Error: NOT NULL constraint failed: cms_book.created_at
なぜか、現在時刻が自動的に入るはずのcreated_at
がnot null制約に違反していると怒られます。
そこで、created_at
, updated_at
をCURRENT_TIMESTAMPで埋めてみます。
sqlite> insert into cms_book(
name,
publisher,
page,
created_at,
updated_at
)
values(
'悪魔の手毬唄',
'集英社',
200,
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP
)
;
Error: NOT NULL constraint failed: cms_book.on_sale
今度は、default値を指定したはずのon_saleがnullだと怒られます。
仕方ないので、on_sale
も値を明示的に指定してやります。
sqlite> insert into cms_book(
name,
publisher,
page,
on_sale,
created_at,
updated_at
)
values(
'悪魔の手毬唄',
'集英社',
200,
0,
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP
)
;
sqlite> select * from cms_book;
id|name|page|created_at|on_sale|updated_at|publisher
1|悪魔の寵児|100|2019-12-09 13:03:29.102973|1|2019-12-09 13:03:29.132151|角川文庫
2|悪魔の手毬唄|200|2019-12-09 13:12:17|0|2019-12-09 13:12:17|集英社
今度はうまくinsert出来ました。
なぜ、default
や、auto_now_add
, auto_now
を設定したカラムでNOT NULL constraint
が発生したのでしょうか?
順に見ていきます。
sqlite> .schema cms_book
CREATE TABLE IF NOT EXISTS "cms_book"(
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"name" varchar(255) NOT NULL UNIQUE,
"page" integer NOT NULL,
"created_at" datetime NOT NULL,
"on_sale" bool NOT NULL,
"updated_at" datetime NOT NULL,
"publisher" varchar(255) NOT NULL
)
;
テーブル定義の時点では、既に、default
とauto_now
系は落ちてしまっています。
migrationファイルと生成されたsqlを確認します。
operations = [
migrations.AddField(
model_name='book',
name='created_at',
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
preserve_default=False,
),
migrations.AddField(
model_name='book',
name='on_sale',
field=models.BooleanField(default=True, verbose_name='販売中'),
),
migrations.AddField(
model_name='book',
name='updated_at',
field=models.DateTimeField(auto_now=True),
),
migrations.AlterField(
model_name='book',
name='name',
field=models.CharField(max_length=255, unique=True, verbose_name='書籍名'),
),
migrations.AlterField(
model_name='book',
name='publisher',
field=models.CharField(max_length=255, verbose_name='出版社'),
),
]
$ python manage.py showmigrations
admin
[X] 0001_initial
(省略)
cms
[X] 0001_initial
[X] 0002_auto_20191209_2202
(省略)
$ python manage.py sqlmigrate cms 0002_auto_20191209_2202
BEGIN;
(省略)
--
-- Alter field publisher on book
--
CREATE TABLE "new__cms_book"(
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"name" varchar(255) NOT NULL UNIQUE,
"page" integer NOT NULL,
"created_at" datetime NOT NULL,
"on_sale" bool NOT NULL,
"updated_at" datetime NOT NULL,
"publisher" varchar(255) NOT NULL
)
;
INSERT INTO "new__cms_book"(
"id",
"name",
"page",
"created_at",
"on_sale",
"updated_at",
"publisher"
)
SELECT
"id",
"name",
"page",
"created_at",
"on_sale",
"updated_at",
"publisher"
FROM
"cms_book"
;
DROP TABLE "cms_book"
;
ALTER TABLE "new__cms_book" RENAME TO "cms_book"
;
COMMIT
;
migrationファイルの方では、default=True
のように表記が残っているのが確認出来ます.
生成されるsqlの方は長いので省略しましたが、(あとなんでわざわざcreate, drop, alterしてるのかは知らんが)
注目いただきたいのは以下の部分です。
CREATE TABLE "new__cms_book"(
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"name" varchar(255) NOT NULL UNIQUE,
"page" integer NOT NULL,
"created_at" datetime NOT NULL,
"on_sale" bool NOT NULL,
"updated_at" datetime NOT NULL,
"publisher" varchar(255) NOT NULL
)
見事に、default
値やauto_now
が落ちてしまっています。
ちなみに、この挙動はsqliteだけでなくpostgresql等でも同様の挙動でした。
解決策
当初、ググってもなかなか見つからなかったのですが、この現象についてはこちらのstackoverflowで言及されていました。
https://stackoverflow.com/questions/53706125/django-default-at-database
意訳すると、「modelとDBスキーマは別モン。not nullとuniqueは適用してやるけどdefaultは無理やわ!」...ってことらしいです。
1つのDBを複数のアプリケーションから参照・更新したり、手パッチ当てたりする場合は少なくないと思うのですが、Djangoユーザは困らないんでしょうかね...
回避策としては、modelから生成されたDDLではなく、地道にERDからDDL(生SQL)を起こすのが良さそうです。
最後に
これだけ普及しているFWでこんなに実用性に問題のある挙動が許されているのか...と驚愕しました。
トラブル解決には、生のSQLが読めるなど、低レイヤーな部分も抑えておくことがより大事だと再認しました。