Djangoフレームワークにおけるmigrations機能の学習ノート（３）

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

```python: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を見てみると：

```python: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なフィールドを追加するために、３つのステップが必要です。[オフィシャルサイト参照](https://docs.djangoproject.com/en/1.11/howto/writing-migrations/#migrations-that-add-unique-fields)
### 概要
前提：モデルファイルに対応するフィールドを追加しました。
1. モデルの変更に対応したmigrationsファイルを作成、フィールドの属性をuniqueからnullableに変更します。
2. 空白migratioinsファイルを作成、レガシーデータにデファルト値をつけます。
3. 空白migrationsファイルを作成、フィールドの属性をuniqueに戻します。
### 詳細
Step1:前記のように、既にArticleモデルにフィールドが追加され、migrationsファイル0003_article_article_id.pyも作成されています。
したがって、migrationsファイル0003_article_article_id.pyを修正します。

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

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

```python: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フィールドが予想通り実装できました。