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

Posted

技术标签:

【中文标题】如何将模型从一个 django 应用程序迁移到一个新应用程序中?【英文标题】:How do I migrate a model out of one django app and into a new one? 【发布时间】:2010-11-18 11:47:51 【问题描述】:

我有一个 django 应用程序,里面有四个模型。我现在意识到这些模型之一应该在一个单独的应用程序中。我确实为迁移安装了南,但我不认为这是它可以自动处理的东西。如何将其中一个模型从旧应用迁移到新应用中?

另外,请记住,我需要这是一个可重复的过程,以便我可以迁移生产系统等。

【问题讨论】:

对于 django 1.7 及更高版本,请参阅***.com/questions/25648393/… 【参考方案1】:

如何使用南迁移。

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

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

会向下迁移。

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

【讨论】:

您可能还需要迁移 django_content_type 表中的数据。 如果您将模型从内部项目中的应用程序移动到外部应用程序(其他用户将期望对其进行初始迁移),您还可以在 drop_cat 中进行重命名并伪造新应用中的初始迁移。 非常棒的指南@Potr。我很好奇,0003_create_cat 后面的部分不应该也有orm['contenttypes.contenttype'].objects.filter 行吗?另外我想分享一个技巧。如果你有索引,它们也需要修改。就我而言,它们是唯一索引,所以我的转发看起来像这样:db.delete_unique('common_cat', ['col1'])db.rename_table('common_cat', 'specific_cat')db.delete_unique('specific_cat', ['col1']) 为了访问orm['contenttypes.contenttype'],您还需要在schemamigration 命令中添加--freeze contenttypes 选项。 就我而言(Django 1.5.7 和 South 1.0).. 我必须输入 python manage.py schemamigration specific create_cat --auto --freeze common 才能从通用应用程序访问 cat 模型。【参考方案2】:

以Potr Czachur 的answer 为基础,涉及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 documentation,depends_on 将确保0004_auto__add_cat0009_auto__del_cat 之前运行向前迁移时,但在向后迁移时相反的顺序。如果我们将db.rename_table('specific_cat', 'common_cat') 留在specific 回滚中,则在尝试迁移ForeignKey 时common 回滚将失败,因为表引用的表不存在。

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

【讨论】:

此答案中的固定来源省略了更新内容类型的行,这些行存在于 Potr Czachur 的答案中。这可能会产生误导。 @ShaiBerger 我专门解决了这个问题:“我故意忽略 Django ContentType 更改——请参阅之前引用的答案以了解如何处理。”【参考方案3】:

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

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

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

【讨论】:

南迁如何做到这一点?【参考方案4】:

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

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

除非设置此依赖项,否则 South 将不保证在运行 specific/0003_create_cat 时存在 common_cat 表,从而向您抛出 django.db.utils.OperationalError: no such table: common_cat 错误。

South 在lexicographical order 中运行迁移,除非明确设置了依赖关系。由于common 出现在specific 之前,所有common 的迁移将在表重命名之前运行,因此它可能不会在Potr 显示的原始示例中重现。但是如果你将common 重命名为app2 并将specific 重命名为app1 就会遇到这个问题。

【讨论】:

这实际上不是 Potr 的例子的问题。它使用特定的迁移来重命名,而普通的迁移则依赖于特定的迁移。如果先运行特定的,那很好。如果先运行 common ,则依赖项将在其之前运行特定的。也就是说,我在执行此操作时交换了顺序,因此重命名是共同发生的,并且依赖项是特定的,然后您需要像上面描述的那样更改依赖项。 我不能同意你的看法。从我的角度来看,解决方案应该是健壮的,并且可以在不试图取悦它的情况下工作。如果您从新数据库和同步数据库/迁移开始,原始解决方案将不起作用。我的建议解决了它。不管怎样,Port 的回答为我节省了很多时间,向他致敬 :) 不这样做也可能导致测试失败(在创建测试数据库时,它们似乎总是运行完整的南迁移)。我以前做过类似的事情。很好的捕获Ihor :)【参考方案5】:

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

这最初是建立在 Potr Czachur's answer 和Matt Briançon's answer, 使用南 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>

【讨论】:

【参考方案6】:

所以使用上面@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';")

【讨论】:

【参考方案7】:

我将对 Daniel Roseman 在他的回答中提出的其中一件事给出更明确的版本...

如果您只是更改已移动模型的 db_table Meta 属性以指向现有表名(而不是如果您删除并执行 syncdb Django 会给它的新名称),那么您可以避免复杂的南迁。例如:

原文:

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

搬家后:

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

现在您只需进行数据迁移以更新MyModeldjango_content_type 表中的app_label,您应该一切顺利...

运行./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()

【讨论】:

以上是关于如何将模型从一个 django 应用程序迁移到一个新应用程序中?的主要内容,如果未能解决你的问题,请参考以下文章

如何将自定义用户模型迁移到 Django 中的不同应用程序?

如何将 Django 模型从 mysql 迁移到 sqlite(或在任何两个数据库系统之间)?

将Django模型从一个应用程序移动到另一个[重复]

Django:如何从 ManyToMany 迁移到 ForeignKey?

如何使用 Python 迁移将 Heroku Django 应用程序的新模型表添加到远程 Heroku Postgres?

如何重命名 Django 应用程序并将数据从一个应用程序迁移到另一个应用程序