South のハマりどころ -同じ番号のmigrationファイルが出来た場合-

はじめに
この記事は 2012 Pythonアドベントカレンダー 12日目の記事です。
South を使ってると幾つかハマるポイントがあります。
Pythonプロフェッショナルプログラミングにも少し書いているのですが、書ききれなかったものとか、それ以降で感じたことなどを記しておきたいなーと思ってたところアドベントカレンダーの募集をしていたのでちょうど良い機会かと筆を取っています。


同じ番号のmigrationファイルが出来る
複数人で同じDjangoアプリに対してモデルの変更作業を行うと同じ番号のmigrationファイルが出来ることがあります。

具体的には、こんなモデルに

class Example(models.Model):
    text = models.TextField()

以下のような修正をしてschemamigrationを行うと

class Example(models.Model):
    text = models.TextField()
    int = models.IntegerField(default=0) # 追加

このようなmigrationファイルが出来るのですが

# 0002_auto__add_field_example_int.py
class Migration(SchemaMigration):                                                                                                                                                   
    def forwards(self, orm):                                                                                                                                                                
        # Adding field 'Example.int'
        db.add_column('example_example', 'int', self.gf('django.db.models.fields.IntegerField')(default=0))

    # backwardsは省略

    models = {
        'example.example': {
            'Meta': {'object_name': 'Example'},
            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
            'int': ('django.db.models.fields.IntegerField', [], {'default':0}),
            'text': ('django.db.models.fields.TextField', [], {})
        },
    }

別の人が同じモデルに別の修正を加えてschemamigrationを行うと

class Example(models.Model):
    text = models.TextField()
    time = models.DateTimeField(null=True) # 追加

このような別のmigrationファイルが出来ます

# 0002_auto__add_field_example_time.py
class Migration(SchemaMigration):                                                                                                                                                   
    def forwards(self, orm):                                                                                                                                                                
        # Adding field 'Example.time'
        db.add_column('example_example', 'time', self.gf('django.db.models.fields.DateTimeField')(null=True))

    # backwardsは省略

    models = {
        'example.example': {
            'Meta': {'object_name': 'Example'},
            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
            'time': ('django.db.models.fields.DateTimeField', [], {'null':'True'}),
            'text': ('django.db.models.fields.TextField', [], {})
        },
    }


同じ番号のmigrationファイルが出来たら何が困るのか
同じ番号のmigrationファイルが存在してもmigrateする上では--mergeオプションの使用を示唆されることがあるくらいで何も問題ないのですが、その後のschemamigrationで問題が発生します。

intの修正とtimeの修正とをマージして再度修正をしてschemamigrationをしてみると

class Example(models.Model):
    text = models.TextField()
    int = models.IntegerField(default=0)
    time = models.DateTimeField(null=True) # timeの修正はマージされているものとする
    bool = models.BooleanField(default=True) # 追加

このようなmigrationファイルが出来ます。

# 0003_auto__add_field_example_bool.py
class Migration(SchemaMigration):                                                                                                                                                   
    def forwards(self, orm):                                                                                                                                                                
        # Adding field 'Example.bool'
        db.add_column('example_example', 'bool', self.gf('django.db.models.fields.BooleanField')(default=True))

        # Adding field 'Example.int'
        # 0002_auto__add_field_example_int.py で追加したはずなのに再度出てくる
        db.add_column('example_example', 'int', self.gf('django.db.models.fields.IntegerField')(default=0)) 

    # backwardsは省略

    models = {
        'example.example': {
            'Meta': {'object_name': 'Example'},
            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
            'int': ('django.db.models.fields.IntegerField', [], {'default':0}),
            'text': ('django.db.models.fields.TextField', [], {})
        },
    }

このmigrationファイルだと追加したはずのintが更に追加されているので、migrateが出来ません。
何故このようなことが起こるかというと、schemamigrationでは最新のmigrationファイル(数字が大きい場合はソート順が下のもの)のmodelsの内容とモデルの構造とを比べて差分をmigrationファイルとしているためです。
今回は 0002_auto__add_field_example_time.py のmodels に int がないため、schemamigrationでintが再度追加されてしまいました。


こういう時はどうすれば良いか
解決策は2通りあります。
一つは同じ番号のmigrationファイルがあった場合はschemamigration前にmigrationファイルのmodelsを修正してモデルの内容と合わせる方法です。

# 0002_auto__add_field_example_time.py
class Migration(SchemaMigration):                                                                                                                                                   

    # forwards, backwardsは省略

    models = {
        'example.example': {
            'Meta': {'object_name': 'Example'},
            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
            'int': ('django.db.models.fields.IntegerField', [], {'default':0}), # これを追加する
            'time': ('django.db.models.fields.DateTimeField', [], {'null':'True'}),
            'text': ('django.db.models.fields.TextField', [], {})
        },
    }

もう一つは、schemamigration後にmigrationファイルのforwardsとbackwardsから重複した処理を削除するという方法です。

# 0003_auto__add_field_example_bool.py
class Migration(SchemaMigration):                                                                                                                                                   
    def forwards(self, orm):                                                                                                                                                                
        # Adding field 'Example.bool'
        db.add_column('example_example', 'bool', self.gf('django.db.models.fields.BooleanField')(default=True))

        # Adding field 'Example.int'
        # これを削除する
        # db.add_column('example_example', 'int', self.gf('django.db.models.fields.IntegerField')(default=0)) 

    # backwards, modelsは省略

どっちでも良いのですが、schemamigration前には気付けないことが多いので僕はもっぱら後者で対応しています。


さいごに
複数人で作業する場合に一番ハマりやすいのはこれだと思います。
他にも幾つかそういうのあるので気が向いたら書いていこうと思いますが期待しないでください。
明日は @takanory です。