7
4

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 5 years have passed since last update.

Djangoフレームワークにおけるmigrations機能の学習ノート(3)

Last updated at Posted at 2017-10-24

前の二本のノートでは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(一意的に識別できるようにします)
    要件通りにモデルファイルを変更すると:
models.py
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を見てみると:

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つのステップが必要です。オフィシャルサイト参照

概要

前提:モデルファイルに対応するフィールドを追加しました。

  1. モデルの変更に対応したmigrationsファイルを作成、フィールドの属性をuniqueからnullableに変更します。
  2. 空白migratioinsファイルを作成、レガシーデータにデファルト値をつけます。
  3. 空白migrationsファイルを作成、フィールドの属性をuniqueに戻します。

詳細

Step1:前記のように、既にArticleモデルにフィールドが追加され、migrationsファイル0003_article_article_id.pyも作成されています。
したがって、migrationsファイル0003_article_article_id.pyを修正します。

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)値を新しいコラムに入れます

0004_auto_20170718_0901.py
# -*- 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)に戻します。これで新しく作成されたデータレコードのコラムを制限できるようになります。

0005_auto_20170718_0901.py
# -*- 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フィールドが予想通り実装できました。

7
4
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
7
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?