【django】マイグレーションツールの使い方

django1.7から標準装備された、マイグレーション機能のやり方をまとめました。また、ユニーク制約付きのカラムを追加する方法や、エラーが発生した場合の復旧方法などもまとめてあります。

今までdjangoのマイグレーションツールといえばsouthでしたが、django1.7以降では動かないそうので、それ以降は素直に標準装備されたマイグレーションツール機能を使っていきましょう。

今回使用したPythonのバージョンは3.4.3、djangoのバージョンは1.8.2です。

準備

この記事では、サンプルのアプリケーションを用いながらマイグレーションの使い方を説明していきます。作成するプロジェクトはmigration_sampleで、作成するアプリはmyappとします。

また、プロジェクトは/tmp/以下に作ることにします。

サンプル用の環境構築

pyvenvやvirtualenvなどを使って、サンプル用の環境を構築します。また、最新版のdjangoもインストールしておきます。

$ pyvenv-3.4 /tmp/venv/
$ source /tmp/venv/bin/activate
$ pip install django

プロジェクトとアプリケーションの作成

djangoのプロジェクトとアプリケーションを作成します。また、settings.pyに、作成したアプリケーションを追加しておきます。

$ cd /tmp
$ django-admin startproject migration_sample
$ cd migration_sample
$ ./manage.py startapp myapp
$ vi migration_sample/settings.py

settings.pyの編集内容

 INSTALLED_APPS = (
     'django.contrib.admin',
     'django.contrib.auth',
     'django.contrib.contenttypes',
     'django.contrib.sessions',
     'django.contrib.messages',
     'django.contrib.staticfiles',
+    'myapp',
 )

models.pyの作成

models.pyを開き、Sample1クラスとnameフィールドを追加します。

$ vi myapp/models.py

models.pyの内容

# coding: utf-8
from django.db import models


class Sample1(models.Model):
    name = models.CharField(max_length=64)

初回マイグレーションの実行

マイグレーションを実行し、テーブルを作成します。マイグレーションは、manage.pyを使ってmakemigrationsmigrateで実行できます。ただ、実行するSQLを確認するために、makemigrationsの後はsqlmigrate app_label migrationnameで確認してからmigrateしていきます。

$ ./manage.py makemigrations
Migrations for 'myapp':
  0001_initial.py:
    - Create model Sample1

$ ./manage.py sqlmigrate myapp 0001
BEGIN;
CREATE TABLE "myapp_sample1" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "name" varchar(64) NOT NULL);

COMMIT;

$ ./manage.py migrate
Operations to perform:
  Synchronize unmigrated apps: staticfiles, messages
  Apply all migrations: contenttypes, admin, auth, myapp, sessions
Synchronizing apps without migrations:
  Creating tables...
    Running deferred SQL...
  Installing custom SQL...
Running migrations:
  Rendering model states... DONE
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying myapp.0001_initial... OK
  Applying sessions.0001_initial... OK

これで、myapp_sampleテーブルと、djangoに必要なテーブル類が作成されました。

作成されたテーブルの確認

$ sqlite3 db.sqlite3
sqlite> .schema myapp_sample1
CREATE TABLE "myapp_sample1" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "name" varchar(64) NOT NULL);

2回目以降のマイグレーション

2回目以降も、同じような手順で実行できます。

  1. models.pyの編集
  2. 「manage.py makemigrations」の実行
  3. (必須では無いけれども)「manage.py sqlmigrate」の実行
  4. 「manage.py migrate」の実行

2回目では、myapp_sample1テーブルに、textカラムを追加してみます。

models.pyの編集

models.pyを開き、textフィールドを追加します。

# coding: utf-8
from django.db import models


class Sample1(models.Model):
    name = models.CharField(max_length=64)
    text = models.TextField(null=True)

マイグレーションの実行

$ ./manage.py makemigrations
Migrations for 'myapp':
  0002_sample1_text.py:
    - Add field text to sample1

$ ./manage.py sqlmigrate myapp 0002
BEGIN;
CREATE TABLE "myapp_sample1__new" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "text" text NULL, "name" varchar(64) NOT NULL);
INSERT INTO "myapp_sample1__new" ("id", "text", "name") SELECT "id", NULL, "name" FROM "myapp_sample1";
DROP TABLE "myapp_sample1";
ALTER TABLE "myapp_sample1__new" RENAME TO "myapp_sample1";

COMMIT;

$ ./manage.py migrate
Operations to perform:
  Synchronize unmigrated apps: messages, staticfiles
  Apply all migrations: admin, myapp, contenttypes, sessions, auth
Synchronizing apps without migrations:
  Creating tables...
    Running deferred SQL...
  Installing custom SQL...
Running migrations:
  Rendering model states... DONE
  Applying myapp.0002_sample1_text... OK

追加されたカラムの確認

myapp_sample1テーブルにtextカラムが追加されていることを確認します。

$ sqlite3 db.sqlite3
sqlite> .schema myapp_sample1
CREATE TABLE "myapp_sample1" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "name" varchar(64) NOT NULL, "text" text NULL);

NOT NULL制約付きのカラムを追加

先ほど追加したtextにはnull=Trueを設定していました。ここをnull=Falseにしたり、そもそもnullを設定していないカラムを追加(や変更)する場合は、マイグレーション時に初期値を指定する必要があります。

ここでは、その初期値を指定する例を紹介します。

null=Falseにしても、default=''のようにデフォルト値を指定していればマイグレーション時には聞かれません。

models.pyの修正

models.pyを開き、noteフィールドを追加します。

# coding: utf-8
from django.db import models


class Sample1(models.Model):
    name = models.CharField(max_length=64)
    text = models.TextField(null=True)
    note = models.CharField(max_length=128, null=False)

makemigrationsを実行

今まで通り、makemigrationsを実行します。すると、以下のように選択肢が表示されます。

$ ./manage.py makemigrations
You are trying to add a non-nullable field 'note' to sample1 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)
 2) Quit, and let me add a default in models.py
Select an option:

ここでは例として、1番を指定して、対話式にデフォルト値を決定してみます。なお、デフォルト値はわかりやすいようにDUMMYとしておきます。

 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()
>>> 'DUMMY'
Migrations for 'myapp':
  0003_sample1_note.py:
    - Add field note to sample1

実行されるSQLを確認

デフォルト値付きのマイグレーションを作成したら、中身を確認してみます。そうすると、既存のレコードに追加されるnoteカラムが、DUMMYになることを確認できます。

なお、ここで指定したデフォルト値は、あくまでも既存のレコードに設定される値です。なので、テーブル構造自体とは無関係です。

$ ./manage.py sqlmigrate myapp 0003
BEGIN;
CREATE TABLE "myapp_sample1__new" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "note" varchar(128) NOT NULL, "name" varchar(64) NOT NULL, "text" text NULL);
INSERT INTO "myapp_sample1__new" ("text", "note", "id", "name") SELECT "text", 'DUMMY', "id", "name" FROM "myapp_sample1";
DROP TABLE "myapp_sample1";
ALTER TABLE "myapp_sample1__new" RENAME TO "myapp_sample1";

COMMIT;

migrateを実行

migrateを実行してスキーマを確認すると、DUMMYなどどこにも出てこないことを確認できます。

$ ./manage.py migrate
$ sqlite3 db.sqlite3
sqlite> .schema myapp_sample1
CREATE TABLE "myapp_sample1" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "name" varchar(64) NOT NULL, "text" text NULL, "note" varchar(128) NOT NULL);

レコードを確認すると、noteカラムにDUMMYが指定されていることがわかります。※レコードは前もって登録しておきました。

sqlite> select note from myapp_sample1;
DUMMY

TIPS

マイグレーション名を確認する方法

manage.py sqlmigrateを実行するときは、アプリ名とマイグレーション名が必要になります。このマイグレーション名は、showmigrationsmigrate -lで確認できます。

個人的には、django1.8から追加されたshowmigrationsの方が好みです。その理由は、昔migrate -lで確認しようとしたときに、間違えてmigrateコマンドを打って冷や汗をかいたことがあるからです。。。

ユニーク制約付きのカラムを追加するときの注意点

既存のレコードがある状態で、ユニーク制約付きでかつNOT NULL制約が追加カラムを追加しようとするとエラーが発生します。

まあ、エラーが発生するのはALTER TABLEで追加するときと同じで、マイグレーション特有のものではありません。ただ、知らないとハマる可能性があるので、対応方法を残しておきます。

エラーが発生する例

例えば、myapp_sample1テーブルにslugカラムを追加してみます。なお、myapp_sample1テーブルには数件のレコードが存在するものとします。

# coding: utf-8
from django.db import models


class Sample1(models.Model):
    name = models.CharField(max_length=64)
    text = models.TextField(null=True)
    note = models.CharField(max_length=128, null=False)
    slug = models.SlugField(unique=True)

この状態でマイグレーションを実施するとエラーが発生します。

makemigrations
$ ./manage.py makemigrations
You are trying to add a non-nullable field 'slug' to sample1 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)
 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()
>>> ''
Migrations for 'myapp':
  0004_sample1_slug.py:
    - Add field slug to sample1
sqlmigrate
$ ./manage.py sqlmigrate myapp 0004
BEGIN;
CREATE TABLE "myapp_sample1__new" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "slug" varchar(50) NOT NULL UNIQUE, "name" varchar(64) NOT NULL, "text" text NULL, "note" varchar(128) NOT NULL);
INSERT INTO "myapp_sample1__new" ("id", "note", "text", "slug", "name") SELECT "id", "note", "text", '', "name" FROM "myapp_sample1";
DROP TABLE "myapp_sample1";
ALTER TABLE "myapp_sample1__new" RENAME TO "myapp_sample1";

COMMIT;
migrate実行
$ ./manage.py migrate
Operations to perform:
...
...
django.db.utils.IntegrityError: UNIQUE constraint failed: myapp_sample1__new.slug

回避策の例

このエラーは、以下のように進めれば回避可能です。

  1. slugカラムのUNIQUE制約を外してマイグレーションを実行します。
  2. 既存のレコードのslugカラムを重複をなくして登録します。
  3. slugカラムにUNIQUE制約を付けて、再度マイグレーションを実行します。

また、エラーが発生してしまった状態から復旧する場合は、最初にエラーが発生するマイグレーション「myapp/migrations/0004_sample1_slug.py」を削除しておきます。

エラーが発生したマイグレーションを削除
$ rm myapp/migrations/0004_sample1_slug.py
ユニーク制約を外してマイグレーション実行
# coding: utf-8
from django.db import models


class Sample1(models.Model):
    name = models.CharField(max_length=64)
    text = models.TextField(null=True)
    note = models.CharField(max_length=128, null=False)
    slug = models.SlugField(unique=False)
$ ./manage.py makemigrations
...
The datetime and django.utils.timezone modules are available, so you can do e.g. timezone.now()
>>> ''
$ ./manage.py migrate
既存のレコードのslugの重複をなくす
$ sqlite3 db.sqlite3
sqlite> BEGIN;
sqlite> UPDATE myapp_sample1 SET slug = 'slug-' || id;
sqlite> COMMIT;

sqlite> SELECT slug FROM myapp_sample1;
slug-1
slug-2

※2015/06/12 UPDATE文を簡略化しました。

ユニーク制約を付けてマイグレーション実行
# coding: utf-8
from django.db import models


class Sample1(models.Model):
    name = models.CharField(max_length=64)
    text = models.TextField(null=True)
    note = models.CharField(max_length=128, null=False)
    slug = models.SlugField(unique=True)
$ ./manage.py makemigrations
Migrations for 'myapp':
  0005_auto_20150610_0305.py:
    - Alter field slug on sample1

$ ./manage.py sqlmigrate myapp 0005
BEGIN;
CREATE TABLE "myapp_sample1__new" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "slug" varchar(50) NOT NULL UNIQUE, "name" varchar(64) NOT NULL, "text" text NULL, "note" varchar(128) NOT NULL);
INSERT INTO "myapp_sample1__new" ("note", "id", "text", "name", "slug") SELECT "note", "id", "text", "name", "slug" FROM "myapp_sample1";
DROP TABLE "myapp_sample1";
ALTER TABLE "myapp_sample1__new" RENAME TO "myapp_sample1";

COMMIT;

$ ./manage.py migrate
Operations to perform:
  Synchronize unmigrated apps: messages, staticfiles
  Apply all migrations: admin, contenttypes, auth, myapp, sessions
Synchronizing apps without migrations:
  Creating tables...
    Running deferred SQL...
  Installing custom SQL...
Running migrations:
  Rendering model states... DONE
  Applying myapp.0005_auto_20150610_0305... OK

マイグレーションが成功しました!

マイグレーションが失敗する場合は...

マイグレーションが失敗するようになり、うんともすんともいわない状態になった経験があります。その時の、復旧方法を紹介します。

あくまでわたしが実行した復旧方法であって、正式なものかどうかはわかりません。

モデルとDBを同じ状態にする

まずは、モデルとDBを同じ状態にします。例えば、モデルにフィールドを追加してマイグレーションが失敗するようになった場合は、そのフィールドをコメントアウトしておきます。

マイグレーションを削除する

今あるマイグレーションを削除します。例えば、myappならば、myapp/migrations/以下を全て削除します。

$ rm myapp/migrations/*

--fake付きでマイグレーションを実行する

次に、makemigrations → migrateと実行していきます。ただし、既にモデルとDBは同じ状態になっているので、実際にはマイグレーションが走らせたくありません。そのため、migrateの際に--fakeを指定します。

$ ./manage.py makemigrations
Migrations for 'myapp':
  0001_initial.py:
    - Create model Sample1

$ ./manage.py migrate --fake
  Synchronize unmigrated apps: messages, staticfiles
  Apply all migrations: admin, auth, contenttypes, myapp, sessions
Synchronizing apps without migrations:
  Creating tables...
    Running deferred SQL...
  Installing custom SQL...
Running migrations:
  No migrations to apply.

私の場合はこれで復旧しました。後は、通常通りにモデルを修正して、マイグレーションを実行していきます。

この記事が役に立った場合、シェアしていただけると励みになります!!