前の二本のノートではdjangoのmigrations機能について色々勉強しました。今回では運用上役立つmigrationsのトラブルシューティングについて勉強しましょう。
開発環境 |
---|
Mac OS:Sierra |
python2.7.10 |
django1.11.2 |
mysql5.7.18 |
レガシーデータがある状態に一意的なフィールドを追加
問題説明:サイトを一定時間運用した後、データが作成されました。ここでモデルに新たに一意な(unique, non-nullable)フィールドを追加します。モデルをデータベースに反映するために、レガシーデータ(既にあるデータ)にデフォルトな値を与える必要があります。しかし、複数のデータレコードに同じデフォルト値をつけることは一意性ルールに違反し、エラーを起こします。デベロップ環境でしたら、データベースとmigrationファイルを全部削除し、再度データベースを作成してもいいですが、もっとグレースフルな方法でこの問題を解決しましょう。
問題再現
Djangoのシェルに入ってデモデータを作成します。
python manage.py shell
>>> from polls.models import Category, Article
>>> c = Category(name="game")
>>> c.save()
>>> a1 = Article(title="play Game1", text="Game1 is amazing!", category=c, image_url = "http://url1")
>>> a1.save()
>>> a2 = Article(title="play Game2", text="Game2 is good!", category=c, image_url = "http://url2")
>>> a2.save()
>>> a3 = Article(title="play Game3", text="Game3 is dump!", category=c, image_url = "http://url3")
>>> a3.save()
mysqlのクライアントで作成されたデータが確認できます(まずプロジェクトのデータベースを指定します)。
まずはブロッグのカテゴリーデータ:
mysql> select * from polls_category;
+----+------+
| id | name |
+----+------+
| 1 | game |
+----+------+
1 row in set (0.00 sec)
次に文章のデータ
mysql> select * from polls_article;
+----+------------+-------------------+-------------+-------------+
| id | title | text | category_id | image_url |
+----+------------+-------------------+-------------+-------------+
| 1 | play Game1 | Game1 is amazing! | 1 | http://url1 |
| 2 | play Game2 | Game2 is good! | 1 | http://url2 |
| 3 | play Game3 | Game3 is dump! | 1 | http://url3 |
+----+------------+-------------------+-------------+-------------+
3 rows in set (0.00 sec)
ここでArticleモデルにArticleを一意に識別できるようなarticle_idフィールドを追加します(自動作成されたprimary keyとは違うものです)。このフィールドは以下の要件を満たしています。
- non-nullable(識別子ゆえ、nullは許されない、デフォルトの値が必要です)
- unique(一意的に識別できるようにします)
要件通りにモデルファイルを変更すると:
class Article(models.Model):
title = models.CharField(max_length=100)
text = models.CharField(max_length=1000)
category = models.ForeignKey(Category, on_delete=models.CASCADE)
image_url = models.URLField(max_length=200, default='toBeImplement')
article_id = models.UUIDField(default=uuid.uuid4(), unique=True)
(Articleクラスしか表示していません)
migrationsファイルを作成:
python manage.py makemigrations polls
成功
Migrations for 'polls':
polls/migrations/0003_article_article_id.py
- Add field article_id to article
データベースに反映させようと:
python manage.py migrate polls
エラーが出ます(部分ログ)
...
django.db.utils.IntegrityError: (1062, "Duplicate entry '3059fd8259254f0693ce8d91cf1198cc' for key 'article_id'")
エラーを解明するために、migrationsファイル0003_article_article_id.pyを見てみると:
# -*- coding: utf-8 -*-
# Generated by Django 1.11.2 on 2017-07-18 08:25
from __future__ import unicode_literals
from django.db import migrations, models
import uuid
class Migration(migrations.Migration):
dependencies = [
('polls', '0002_article_image_url'),
]
operations = [
migrations.AddField(
model_name='article',
name='article_id',
field=models.UUIDField(default=uuid.UUID('3059fd82-5925-4f06-93ce-8d91cf1198cc'), unique=True),
),
]
エラーが出る理由は既にあるデータ(Article)レコードにarticle_idを追加するとき、同じデフォルト値を与えてしまい、unique要件に違反したからです。
修正方法
レガシーデータがある場合、non-nullableかつuniqueなフィールドを追加するために、3つのステップが必要です。オフィシャルサイト参照
概要
前提:モデルファイルに対応するフィールドを追加しました。
- モデルの変更に対応したmigrationsファイルを作成、フィールドの属性をuniqueからnullableに変更します。
- 空白migratioinsファイルを作成、レガシーデータにデファルト値をつけます。
- 空白migrationsファイルを作成、フィールドの属性をuniqueに戻します。
詳細
Step1:前記のように、既にArticleモデルにフィールドが追加され、migrationsファイル0003_article_article_id.pyも作成されています。
したがって、migrationsファイル0003_article_article_id.pyを修正します。
# -*- coding: utf-8 -*-
# Generated by Django 1.11.2 on 2017-07-18 08:25
from __future__ import unicode_literals
from django.db import migrations, models
import uuid
class Migration(migrations.Migration):
dependencies = [
('polls', '0002_article_image_url'),
]
operations = [
migrations.AddField(
model_name='article',
name='article_id',
field=models.UUIDField(default=uuid.UUID('3059fd82-5925-4f06-93ce-8d91cf1198cc'), null=True),
),
]
つまりclass Migration->operations->migrations.AddField()の中の"unique=True"を"null=True"に変更します。これでレガシーデータの新しいコラムに同じデフォルト値を入れられます。
Step2:空白なmigrationsファイルを作成します。
python manage.py makemigrations polls --empty
ここでレガシーデータの各レコードに違う(unique)値を新しいコラムに入れます
# -*- coding: utf-8 -*-
# Generated by Django 1.11.2 on 2017-07-18 09:01
from __future__ import unicode_literals
from django.db import migrations
import uuid
def gen_uuid(apps, schema_editor):
Article = apps.get_model('polls', 'Article')
for row in Article.objects.all():
row.article_id = uuid.uuid4()
row.save(update_fields=['article_id'])
class Migration(migrations.Migration):
dependencies = [
('polls', '0003_article_article_id'),
]
operations = [
migrations.RunPython(gen_uuid, reverse_code=migrations.RunPython.noop)
]
Step3:最後に、再度空白migrationsファイルを作成し、コラムの属性を一意な(unique)に戻します。これで新しく作成されたデータレコードのコラムを制限できるようになります。
# -*- coding: utf-8 -*-
# Generated by Django 1.11.2 on 2017-07-18 09:01
from __future__ import unicode_literals
from django.db import migrations, models
import uuid
class Migration(migrations.Migration):
dependencies = [
('polls', '0004_auto_20170718_0901'),
]
operations = [
migrations.AlterField(
model_name='article',
name='article_id',
field=models.UUIDField(default=uuid.uuid4, unique=True),
),
]
migrationsファイルの用意ができましたら、モデル及びレガシーデータへの変更を反映させます。
(a_site) IT-02085-M:m_Migrate ruofan.ye$ python manage.py migrate polls
Operations to perform:
Apply all migrations: polls
Running migrations:
Applying polls.0003_article_article_id... OK
Applying polls.0004_auto_20170718_0901... OK
Applying polls.0005_auto_20170718_0901... OK
成功、さらにデータベースを見てみると
mysql> select * from polls_article;
+----+------------+-------------------+-------------+-------------+----------------------------------+
| id | title | text | category_id | image_url | article_id |
+----+------------+-------------------+-------------+-------------+----------------------------------+
| 1 | play Game1 | Game1 is amazing! | 1 | http://url1 | 7347eab9e4df47eb989de30fc6baeec9 |
| 2 | play Game2 | Game2 is good! | 1 | http://url2 | c4bac1cc1d3242aa934c9cb16f1cf5a9 |
| 3 | play Game3 | Game3 is dump! | 1 | http://url3 | e6094b7b24fc46e7a59a7b90aaca298c |
+----+------------+-------------------+-------------+-------------+----------------------------------+
3 rows in set (0.00 sec)
テーブル、レガシーデータに無事新しいコラムを追加できました。
新しいArticleデータを作成して:
>>> a4 = Article(title="play Game4", text="Game4 is new-feeling!", category=c, image_url = "http://url4")
>>> a4.save()
データベースを見てみると:
mysql> select * from polls_article;
+----+------------+-----------------------+-------------+-------------+----------------------------------+
| id | title | text | category_id | image_url | article_id |
+----+------------+-----------------------+-------------+-------------+----------------------------------+
| 1 | play Game1 | Game1 is amazing! | 1 | http://url1 | 7347eab9e4df47eb989de30fc6baeec9 |
| 2 | play Game2 | Game2 is good! | 1 | http://url2 | c4bac1cc1d3242aa934c9cb16f1cf5a9 |
| 3 | play Game3 | Game3 is dump! | 1 | http://url3 | e6094b7b24fc46e7a59a7b90aaca298c |
| 4 | play Game4 | Game4 is new-feeling! | 1 | http://url4 | 0f1fcf070c544784b2a11ba2a4661cd7 |
+----+------------+-----------------------+-------------+-------------+----------------------------------+
4 rows in set (0.00 sec)
article_idフィールドが予想通り実装できました。