如何将模型从一个django应用程序迁移到新的模型中?


126

我有一个带有四个模型的django应用。我现在意识到这些模型之一应该在单独的应用程序中。我确实已经安装了South来进行迁移,但是我认为这不是它可以自动处理的事情。如何将其中一种模型从旧应用迁移到新模型?

另外,请记住,我将需要将此过程重复进行,以便可以迁移生产系统等。


Answers:


184

如何向南迁移。

假设我们有两个应用程序:通用和专用:

myproject/
|-- common
|   |-- migrations
|   |   |-- 0001_initial.py
|   |   `-- 0002_create_cat.py
|   `-- models.py
`-- specific
    |-- migrations
    |   |-- 0001_initial.py
    |   `-- 0002_create_dog.py
    `-- models.py

现在,我们想将模型common.models.cat移至特定的应用程序(精确地移至specific.models.cat)。首先在源代码中进行更改,然后运行:

$ python manage.py schemamigration specific create_cat --auto
 + Added model 'specific.cat'
$ python manage.py schemamigration common drop_cat --auto
 - Deleted model 'common.cat'

myproject/
|-- common
|   |-- migrations
|   |   |-- 0001_initial.py
|   |   |-- 0002_create_cat.py
|   |   `-- 0003_drop_cat.py
|   `-- models.py
`-- specific
    |-- migrations
    |   |-- 0001_initial.py
    |   |-- 0002_create_dog.py
    |   `-- 0003_create_cat.py
    `-- models.py

现在我们需要编辑两个迁移文件:

#0003_create_cat: replace existing forward and backward code
#to use just one sentence:

def forwards(self, orm):
    db.rename_table('common_cat', 'specific_cat') 

    if not db.dry_run:
        # For permissions to work properly after migrating
        orm['contenttypes.contenttype'].objects.filter(
            app_label='common',
            model='cat',
        ).update(app_label='specific')

def backwards(self, orm):
    db.rename_table('specific_cat', 'common_cat')

    if not db.dry_run:
        # For permissions to work properly after migrating
        orm['contenttypes.contenttype'].objects.filter(
            app_label='specific',
            model='cat',
        ).update(app_label='common')

#0003_drop_cat:replace existing forward and backward code
#to use just one sentence; add dependency:

depends_on = (
    ('specific', '0003_create_cat'),
)
def forwards(self, orm):
    pass
def backwards(self, orm):
    pass

现在,这两个应用程序迁移都知道变化了,生活也变得轻松了一些:-)在迁移之间设置这种关系是成功的关键。现在,如果您这样做:

python manage.py migrate common
 > specific: 0003_create_cat
 > common: 0003_drop_cat

将同时进行迁移,并且

python manage.py migrate specific 0002_create_dog
 < common: 0003_drop_cat
 < specific: 0003_create_cat

会向下迁移。

请注意,对于架构升级,我使用了通用应用程序,对于降级,我使用了特定应用程序。那是因为这里的依赖关系是如何工作的。


1
哇谢谢。自从问了这个问题后,我就独自学习了南方,但是我相信这将极大地帮助其他人。
2009年

11
您可能还需要迁移django_content_type表中的数据。
spookylukey 2011年

1
真的很棒@Potr指南。我很好奇,难道不应该 orm['contenttypes.contenttype'].objects.filter 在它的后部0003_create_cat吗?我也想分享一个秘诀。如果您有索引,则也需要对其进行修改。就我而言,它们是唯一的索引,所以我的前锋看起来像这样:db.delete_unique('common_cat', ['col1']) db.rename_table('common_cat', 'specific_cat') db.delete_unique('specific_cat', ['col1'])
Brad Pitcher

2
为了访问orm['contenttypes.contenttype'],您还需要--freeze contenttypesschemamigration命令中添加选项。
加里

1
在我的情况下(Django 1.5.7和South 1.0)..我必须键入python manage.py schemamigration specific create_cat --auto --freeze common才能从常见应用访问cat模型。
geoom 2015年

35

要基于Potr Czachur答案,涉及ForeignKeys的情况将更为复杂,应对方式应稍有不同。

(以下示例以当前答案中引用的commonspecific应用为基础)。

# common/models.py

class Cat(models.Model):
    # ...

class Toy(models.Model):
    belongs_to = models.ForeignKey(Cat)
    # ...

然后将更改为

# common/models.py

from specific.models import Cat

class Toy(models.Model):
    belongs_to = models.ForeignKey(Cat)
    # ...

# specific/models.py

class Cat(models.Model):
    # ...

跑步

./manage.py schemamigration common --auto
./manage.py schemamigration specific --auto # or --initial

会产生以下迁移(我有意忽略Django ContentType更改-有关如何处理的信息,请参阅前面引用的答案):

# common/migrations/0009_auto__del_cat.py

class Migration(SchemaMigration):
    def forwards(self, orm):
        db.delete_table('common_cat')
        db.alter_column('common_toy', 'belongs_to_id', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['specific.Cat']))

    def backwards(self, orm):
        db.create_table('common_cat', (
            # ...
        ))
        db.alter_column('common_toy', 'belongs_to_id', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['common.Cat']))

# specific/migrations/0004_auto__add_cat.py

class Migration(SchemaMigration):
    def forwards(self, orm):
        db.create_table('specific_cat', (
            # ...
        ))

    def backwards(self, orm):
        db.delete_table('specific_cat')

如您所见,必须更改FK才能引用新表。我们需要添加的依赖,使我们知道其中的迁移将应用(因此,在我们尝试将FK添加到它的表将存在)的顺序,但我们也需要确保向后滚动的作品也因为在依赖性适用于相反的方向

# common/migrations/0009_auto__del_cat.py

class Migration(SchemaMigration):

    depends_on = (
        ('specific', '0004_auto__add_cat'),
    )

    def forwards(self, orm):
        db.alter_column('common_toy', 'belongs_to_id', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['specific.Cat']))

    def backwards(self, orm):
        db.rename_table('specific_cat', 'common_cat')
        db.alter_column('common_toy', 'belongs_to_id', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['common.Cat']))

# specific/migrations/0004_auto__add_cat.py

class Migration(SchemaMigration):
    def forwards(self, orm):
        db.rename_table('common_cat', 'specific_cat')

    def backwards(self, orm):
        pass

根据South的文档depends_on将确保在向前迁移时0004_auto__add_cat先运行, 而向后迁移时相反。如果我们不进行回滚,则在尝试迁移ForeignKey时回滚将失败,因为该表引用的表将不存在。0009_auto__del_cat db.rename_table('specific_cat', 'common_cat')specificcommon

希望这比现有解决方案更接近“现实世界”的情况,并且有人会发现这会有所帮助。干杯!


此答案中的固定来源省略了更新内容类型的行,这些行出现在Potr Czachur的答案中。这可能会产生误导。
Shai Berger 2014年

@ShaiBerger我专门解决了这一问题:“我故意忽略了Django ContentType的更改-有关如何处理的内容,请参见前面引用的答案。”
MattBriançon2014年

9

模型与应用程序的耦合不是很紧密,因此移动非常简单。Django在数据库表的名称中使用应用程序名称,因此,如果要移动应用程序,则可以通过SQL ALTER TABLE语句重命名数据库表,或者甚至更简单-只需使用模型类的db_table参数Meta来引用旧名称。

如果到目前为止,您在代码中的任何地方都使用过ContentTypes或通用关系,则可能要重命名app_label指向正在移动的模型的contenttype的,以便保留现有的关系。

当然,如果您根本没有要保留的任何数据,那么最简单的操作就是完全删除数据库表并./manage.py syncdb再次运行。


2
我该如何向南迁移?
09年

4

这是对Potr出色解决方案的又一修复。将以下内容添加到specific / 0003_create_cat

depends_on = (
    ('common', '0002_create_cat'),
)

除非设置了此依赖关系,否则South不能保证common_cat在运行specific / 0003_create_cat时该表存在,这会向django.db.utils.OperationalError: no such table: common_cat您抛出错误。

除非明确设置依赖关系否则South会按字典顺序进行迁移。由于common之前说到specific所有common的迁移将表重命名之前,所以它可能不会通过Potr所示的原来的例子重现得到运行。但是,如果您重命名commonapp2specificapp1则会遇到此问题。


实际上,对于Potr的示例而言,这不是问题。它使用特定的迁移进行重命名,而公共迁移则依赖于特定的迁移。如果首先运行specific,那就很好。如果首先运行common,则依赖项将在其之前进行特定的运行。就是说,我在执行此操作时交换了顺序,因此重命名在普通情况下发生,而具体情况下发生了依赖性,然后您需要像上面描述的那样更改依赖性。
EmilStenström2014年

1
我不同意你的看法。从我的观点来看,该解决方案应该是健壮的,并且可以在不取悦它的情况下正常工作。如果您是从新的db和syncdb / migrate开始的,则原始解决方案将不起作用。我的提案已解决。不管怎样,波特的回答为我节省了很多时间,对他表示
敬意

不这样做可能会使测试也失败(在创建他们的测试数据库时,它们似乎总是在向南迁移)。我以前做过类似的事情。
伊荷尔(

4

自从我回到这里几次并决定将其正式化以来,我目前已经确定了该流程。

这最初是基于 Potr Czachur的答案MattBriançon 的答案(使用南0.8.4)构建的

步骤1.发现子外键关系

# Caution: This finds OneToOneField and ForeignKey.
# I don't know if this finds all the ways of specifying ManyToManyField.
# Hopefully Django or South throw errors if you have a situation like that.
>>> Cat._meta.get_all_related_objects()
[<RelatedObject: common:toy related to cat>,
 <RelatedObject: identity:microchip related to cat>]

因此,在这种扩展情况下,我们发现了另一个相关模型,例如:

# Inside the "identity" app...
class Microchip(models.Model):

    # In reality we'd probably want a ForeignKey, but to show the OneToOneField
    identifies = models.OneToOneField(Cat)

    ...

步骤2.创建迁移

# Create the "new"-ly renamed model
# Yes I'm changing the model name in my refactoring too.
python manage.py schemamigration specific create_kittycat --auto

# Drop the old model
python manage.py schemamigration common drop_cat --auto

# Update downstream apps, so South thinks their ForeignKey(s) are correct.
# Can skip models like Toy if the app is already covered
python manage.py schemamigration identity update_microchip_fk --auto

步骤3.源代码控制:提交更改。

如果您遇到合并冲突(例如队友在更新的应用程序上编写迁移),则使此过程更具可重复性。

步骤4.在迁移之间添加依赖关系。

基本上create_kittycat取决于一切的当前状态,然后一切都取决于create_kittycat

# create_kittycat
class Migration(SchemaMigration):

    depends_on = (
        # Original model location
        ('common', 'the_one_before_drop_cat'),

        # Foreign keys to models not in original location
        ('identity', 'the_one_before_update_microchip_fk'),
    )
    ...


# drop_cat
class Migration(SchemaMigration):

    depends_on = (
        ('specific', 'create_kittycat'),
    )
    ...


# update_microchip_fk
class Migration(SchemaMigration):

    depends_on = (
        ('specific', 'create_kittycat'),
    )
    ...

步骤5.我们要进行的表重命名更改。

# create_kittycat
class Migration(SchemaMigration):

    ...

    # Hopefully for create_kittycat you only need to change the following
    # 4 strings to go forward cleanly... backwards will need a bit more work.
    old_app = 'common'
    old_model = 'cat'
    new_app = 'specific'
    new_model = 'kittycat'

    # You may also wish to update the ContentType.name,
    # personally, I don't know what its for and
    # haven't seen any side effects from skipping it.

    def forwards(self, orm):

        db.rename_table(
            '%s_%s' % (self.old_app, self.old_model),
            '%s_%s' % (self.new_app, self.new_model),
        )

        if not db.dry_run:
            # For permissions, GenericForeignKeys, etc to work properly after migrating.
            orm['contenttypes.contenttype'].objects.filter(
                app_label=self.old_app,
                model=self.old_model,
            ).update(
                app_label=self.new_app,
                model=self.new_model,
            )

        # Going forwards, should be no problem just updating child foreign keys
        # with the --auto in the other new South migrations

    def backwards(self, orm):

        db.rename_table(
            '%s_%s' % (self.new_app, self.new_model),
            '%s_%s' % (self.old_app, self.old_model),
        )

        if not db.dry_run:
            # For permissions, GenericForeignKeys, etc to work properly after migrating.
            orm['contenttypes.contenttype'].objects.filter(
                app_label=self.new_app,
                model=self.new_model,
            ).update(
                app_label=self.old_app,
                model=self.old_model,
            )

        # Going backwards, you probably should copy the ForeignKey
        # db.alter_column() changes from the other new migrations in here
        # so they run in the correct order.
        #
        # Test it! See Step 6 for more details if you need to go backwards.
        db.alter_column('common_toy', 'belongs_to_id', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['common.Cat']))
        db.alter_column('identity_microchip', 'identifies_id', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['common.Cat']))


# drop_cat
class Migration(SchemaMigration):

    ...

    def forwards(self, orm):
        # Remove the db.delete_table(), if you don't at Step 7 you'll likely get
        # "django.db.utils.ProgrammingError: table "common_cat" does not exist"

        # Leave existing db.alter_column() statements here
        db.alter_column('common_toy', 'belongs_to_id', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['specific.KittyCat']))

    def backwards(self, orm):
        # Copy/paste the auto-generated db.alter_column()
        # into the create_kittycat migration if you need backwards to work.
        pass


# update_microchip_fk
class Migration(SchemaMigration):

    ...

    def forwards(self, orm):
        # Leave existing db.alter_column() statements here
        db.alter_column('identity_microchip', 'identifies_id', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['specific.KittyCat']))

    def backwards(self, orm):
        # Copy/paste the auto-generated db.alter_column()
        # into the create_kittycat migration if you need backwards to work.
        pass

第6步。仅当您需要Backwards()才能工作并让KeyError向后运行时。

# the_one_before_create_kittycat
class Migration(SchemaMigration):

    # You many also need to add more models to South's FakeORM if you run into
    # more KeyErrors, the trade-off chosen was to make going forward as easy as
    # possible, as that's what you'll probably want to do once in QA and once in
    # production, rather than running the following many times:
    #
    # python manage.py migrate specific <the_one_before_create_kittycat>

    models = {
        ...
        # Copied from 'identity' app, 'update_microchip_fk' migration
        u'identity.microchip': {
            'Meta': {'object_name': 'Microchip'},
            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
            'identifies': ('django.db.models.fields.related.OneToOneField', [], {to=orm['specific.KittyCat']})
        },
        ...
    }

第7步。进行测试-对我有用的功能可能不足以满足您的实际生活情况:)

python manage.py migrate

# If you need backwards to work
python manage.py migrate specific <the_one_before_create_kittycat>

3

因此,使用上述@Potr的原始响应在South 0.8.1和Django 1.5.1上对我不起作用。我在下面发布对我有用的东西,希望对其他人有帮助。

from south.db import db
from south.v2 import SchemaMigration
from django.db import models

class Migration(SchemaMigration):

    def forwards(self, orm):
        db.rename_table('common_cat', 'specific_cat') 

        if not db.dry_run:
             db.execute(
                "update django_content_type set app_label = 'specific' where "
                " app_label = 'common' and model = 'cat';")

    def backwards(self, orm):
        db.rename_table('specific_cat', 'common_cat')
            db.execute(
                "update django_content_type set app_label = 'common' where "
                " app_label = 'specific' and model = 'cat';")

1

我将给出丹尼尔·罗斯曼(Daniel Roseman)在回答中建议的其中一项内容的更明确的版本...

如果仅更改db_table模型的Meta属性,然后将其指向现有的表名(如果您删除并执行,Django会使用新的名称代替它syncdb),则可以避免复杂的South迁移。例如:

原版的:

# app1/models.py
class MyModel(models.Model):
    ...

移动后:

# app2/models.py
class MyModel(models.Model):
    class Meta:
        db_table = "app1_mymodel"

现在,你只需要做一个数据迁移到更新app_labelMyModeldjango_content_type表中,你应该是好去...

运行,./manage.py datamigration django update_content_type然后编辑South为您创建的文件:

def forwards(self, orm):
    moved = orm.ContentType.objects.get(app_label='app1', model='mymodel')
    moved.app_label = 'app2'
    moved.save()

def backwards(self, orm):
    moved = orm.ContentType.objects.get(app_label='app2', model='mymodel')
    moved.app_label = 'app1'
    moved.save()
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.