16
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Python初心者がDjangoでハマったこと

Last updated at Posted at 2019-12-09

はじめに

「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.pyclass Bookを以下のように変更しましょう。

cms/models.py
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_atupdated_atを追加し、insert, update時にそれぞれ現在時刻を埋め、on_saleを追加しデフォルト値をTrueにしておきます。

さらに、nameunique=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を追加します。(詳細省略)

success.png

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
)
;

テーブル定義の時点では、既に、defaultauto_now系は落ちてしまっています。
migrationファイルと生成されたsqlを確認します。

cms/migrations/0002_auto_20191209_2202.py
    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が読めるなど、低レイヤーな部分も抑えておくことがより大事だと再認しました。

16
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?