Djangoはモデルの内容が変更されても、makemigrations
してmigrate
すればDBにモデルの変更を反映できるので楽でいいですね。しかし、ちょっとユニーク制約をもったカラムを追加しようとしたら簡単にはいかないケースに遭遇したのでメモを残しておきます。
遭遇したケース
こんなモデルが最初にあったとします。
from django.db import models class Product(models.Model): name = models.CharField(verbose_name='商品名', max_length=100) price = models.IntegerField(verbose_name='価格') def __str__(self): return '{}: {}'.format(self.id, self.name)
また、すでにエントリが2件存在しています。
In [6]: Product.objects.all() Out[6]: <QuerySet [<Product: 1: prod1>, <Product: 2: prod2>]>
これに以下の様にユニークでNOT NULLな属性を追加しようとします。
from django.db import models class Product(models.Model): name = models.CharField(verbose_name='商品名', max_length=100) price = models.IntegerField(verbose_name='価格') # 新しく追加したい product_no = models.IntegerField(verbose_name='商品番号', unique=True) def __str__(self): return '{}: {}'.format(self.id, self.name)
何が起きるのか
とりあえずmakemigrations
してみると、当然ですが既存データへに設定されるデフォルト値の指定を求められます。
(django111) denzownoMacBook-Pro:sampledjango denzow$ python manage.py makemigrations app1 You are trying to add a non-nullable field 'product_code' to product without a default; we can't do that (the database needs something to populate existing rows). Please select a fix: 1) Provide a one-off default now (will be set on all existing rows with a null value for this column) 2) Quit, and let me add a default in models.py
とりあえず固定で999
とかイレてみます。
(django111) denzownoMacBook-Pro:sampledjango denzow$ python manage.py makemigrations app1 You are trying to add a non-nullable field 'product_code' to product without a default; we can't do that (the database needs something to populate existing rows). Please select a fix: 1) Provide a one-off default now (will be set on all existing rows with a null value for this column) 2) Quit, and let me add a default in models.py Select an option: 1 Please enter the default value now, as valid Python The datetime and django.utils.timezone modules are available, so you can do e.g. timezone.now Type 'exit' to exit this prompt >>> 999 Migrations for 'app1': app1/migrations/0003_product_product_code.py - Add field product_code to product
とりあえずマイグレーションファイルは作成されました。migrate
してみます。
(django111) denzownoMacBook-Pro:sampledjango denzow$ python manage.py migrate Operations to perform: Apply all migrations: admin, app1, auth, contenttypes, sessions Running migrations: Applying app1.0003_product_product_code...Traceback (most recent call last): File "/Users/denzow/miniconda3/envs/django111/lib/python3.6/site-packages/django/db/backends/utils.py", line 65, in execute return self.cursor.execute(sql, params) File "/Users/denzow/miniconda3/envs/django111/lib/python3.6/site-packages/django/db/backends/sqlite3/base.py", line 328, in execute return Database.Cursor.execute(self, query, params) sqlite3.IntegrityError: UNIQUE constraint failed: app1_product.product_code : :
あっけなく死にました。あたりまえですが、すでに存在する2行に対していずれも999を指定するような処理になりますので、2行目に999を設定した時点でユニーク制約に抵触してしまうからです。実際に生成されているマイグレーションファイルも以下の様になっています。
class Migration(migrations.Migration): dependencies = [ ('app1', '0002_auto_20171223_0442'), ] operations = [ migrations.AddField( model_name='product', name='product_code', field=models.CharField(default=999, max_length=100, unique=True, verbose_name='商品コード'), preserve_default=False, ), ]
Djangoのmodelのdefault
は関数を指定することもできますので、以下のようにuuidを生成させるようにしてみます。
from __future__ import unicode_literals import uuid from django.db import migrations, models def get_uuid(): """ uuidを戻す """ uuid_ = uuid.uuid4() print('\n### set default', uuid_) return uuid_ class Migration(migrations.Migration): dependencies = [ ('app1', '0002_auto_20171223_0442'), ] operations = [ migrations.AddField( model_name='product', name='product_no', # デフォルトをuuidに指定している field=models.CharField(default=get_uuid, max_length=100, unique=True, verbose_name='商品コード'), preserve_default=False, ), ]
これなら動きそうなので、migrate
してみます。
(django111) denzownoMacBook-Pro:sampledjango denzow$ python manage.py migrate Operations to perform: Apply all migrations: admin, app1, auth, contenttypes, sessions Running migrations: Applying app1.0003_product_product_code... ### set default 4cb51acd-2b6b-41ae-bfb0-084d90ec969e ★★ Traceback (most recent call last): File "/Users/denzow/miniconda3/envs/django111/lib/python3.6/site-packages/django/db/backends/utils.py", line 65, in execute return self.cursor.execute(sql, params) File "/Users/denzow/miniconda3/envs/django111/lib/python3.6/site-packages/django/db/backends/sqlite3/base.py", line 328, in execute return Database.Cursor.execute(self, query, params) sqlite3.IntegrityError: UNIQUE constraint failed: app1_product.product_code
あっけなくUNIQUE constraint failed
で死にました。★の箇所をみると、get_uuid
が実行されているようですが、2行あるにも関わらず1回しか実行されていません。実はmigrate
時はdefaultに指定した関数は一度しか評価されない様になっているのです。そのため、関数を指定しても既存行には同じ値が入るので回避できません。
結局どうしろと
実はDjangoのマニュアルにずばりこのケースの話が書いてあります。
内容を簡単にまとめると以下を実行するマイグレーションファイルを作成するというものです。
unique=True
ではなくnull=True
を指定して列を追加- 既存列に一意な値を設定
- 追加した列に
unique=True
を追加
実際にやってみましょう。ドキュメントでは空のマイグレーションファイルを複数作り、上述の3ステップごとのマイグレーションファイルを作る流れになっていますが、まとめることもできますのでそちらで対応します。
まずマイグレーションファイルを作ります。
(django111) denzownoMacBook-Pro:sampledjango denzow$ python manage.py makemigrations app1 You are trying to add a non-nullable field 'product_no' to product without a default; we can't do that (the database needs something to populate existing rows). Please select a fix: 1) Provide a one-off default now (will be set on all existing rows with a null value for this column) 2) Quit, and let me add a default in models.py Select an option: 1 Please enter the default value now, as valid Python The datetime and django.utils.timezone modules are available, so you can do e.g. timezone.now Type 'exit' to exit this prompt >>> 999 Migrations for 'app1': app1/migrations/0003_product_product_no.py - Add field product_no to product
作成されたapp1/migrations/0003_product_product_no.py
を開き編集します。
# -*- coding: utf-8 -*- # Generated by Django 1.11 on 2017-12-23 05:38 from __future__ import unicode_literals from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('app1', '0002_auto_20171223_0442'), ] operations = [ migrations.AddField( model_name='product', name='product_code', field=models.CharField(default=999, max_length=100, null=True, verbose_name='商品コード'), # null=Trueに書き換え preserve_default=False, ), ]
これで、null値はそのままになるのでエラーはでません。続いて既存の行に商品コードを追加する処理set_default_code
を追加します。
+def set_default_code(apps, schema_editor): + """ + 既存の行に対して商品コードを C-商品名というルールで追加する関数 + """ + product_model = apps.get_model('app1', 'Product') + for row in product_model.objects.all(): + row.product_code = 'C-{}'.format(row.name) + row.save() class Migration(migrations.Migration): dependencies = [ ('app1', '0002_auto_20171223_0442'), ] operations = [ migrations.AddField( model_name='product', name='product_code', field=models.CharField(max_length=100, null=True, verbose_name='商品コード'), # null=Trueとして列追加 preserve_default=False, ), + migrations.RunPython(set_default_code, reverse_code=migrations.RunPython.noop), # 既存行に一意なデータ追加 ]
これで既存の行はユニーク制約に違反しない状態のデータに変更されます。最後はproduct_code
の列にunique=True
の属性を追加します。
class Migration(migrations.Migration): dependencies = [ ('app1', '0002_auto_20171223_0442'), ] operations = [ migrations.AddField( model_name='product', name='product_code', field=models.CharField(max_length=100, null=True, verbose_name='商品コード'), # null=Trueとして列追加 preserve_default=False, ), migrations.RunPython(set_default_code, reverse_code=migrations.RunPython.noop), # 既存行に一意なデータ追加 + migrations.AlterField( + model_name='product', + name='product_code', + field=models.CharField(max_length=100, unique=True, null=False, blank=False, verbose_name='商品コード'), # ユニーク制約を追加 + preserve_default=False, + ), ]
では実際に実行してみます。
(django111) denzownoMacBook-Pro:sampledjango denzow$ python manage.py migrate Operations to perform: Apply all migrations: admin, app1, auth, contenttypes, sessions Running migrations: Applying app1.0003_product_product_code... OK
無事に実行できたようです。データを確認してみます。
In [2]: for row in Product.objects.all(): ...: print(row.name, row.product_code) ...: prod1 C-prod1 prod2 C-prod2
新しい商品にすでに存在するC-prod2
を指定してみます。
In [2]: Product(name='prod3', price=300, product_code='C-prod2').save() --------------------------------------------------------------------------- IntegrityError Traceback (most recent call last) ~/miniconda3/envs/django111/lib/python3.6/site-packages/django/db/backends/utils.py in execute(self, sql, params) 64 else: ---> 65 return self.cursor.execute(sql, params) 66 ~/miniconda3/envs/django111/lib/python3.6/site-packages/django/db/backends/sqlite3/base.py in execute(self, query, params) 327 query = self.convert_query(query) --> 328 return Database.Cursor.execute(self, query, params) 329 IntegrityError: UNIQUE constraint failed: app1_product.product_code
ちゃんとユニーク制約で弾いてくれました。これで無事に作業が完了です。