如何在两个Django应用之间移动模型(Django 1.7)

157 投票
13 回答
44653 浏览
提问于 2025-04-19 14:57

大约一年前,我开始了一个项目,像所有新手开发者一样,我并没有太关注项目的结构。不过现在我在使用Django的过程中,发现我的项目布局,特别是我的模型,结构实在是糟糕透了。

我的模型主要集中在一个应用里,实际上这些模型应该分散到各自的应用中。我曾经尝试用south来解决这个问题,把模型移动到不同的应用里,但因为涉及到外键等问题,我发现这非常棘手和困难。

不过现在因为Django 1.7引入了内置的迁移支持,难道现在有更好的方法来处理这个问题吗?

13 个回答

15

我是怎么做到的(在Django==1.8和Postgres上测试过,可能也适用于1.7)

情况

app1.YourModel

但你想把它改成:

app2.YourModel

  1. 把app1里的YourModel(代码)复制到app2。
  2. 在app2.YourModel里添加以下内容:

    Class Meta:
        db_table = 'app1_yourmodel'
    
  3. 运行命令:$ python manage.py makemigrations app2

  4. 这时会在app2里生成一个新的迁移文件(比如0009_auto_something.py),里面有一个migrations.CreateModel()的语句,把这个语句移动到app2的初始迁移文件里(比如0001_initial.py),就像它一直在那样。然后删除刚刚创建的迁移文件=0009_auto_something.py。

  5. 就像你假装app2.YourModel一直存在一样,现在要从你的迁移中删除app1.YourModel的存在。也就是说:把CreateModel的语句注释掉,以及你之后用到的每一个调整或数据迁移。

  6. 当然,项目中所有对app1.YourModel的引用都要改成app2.YourModel。此外,别忘了在迁移中所有可能指向app1.YourModel的外键也要改成app2.YourModel。

  7. 现在如果你运行命令:$ python manage.py migrate,什么都没变;即使你运行$ python manage.py makemigrations,也没有检测到新内容。

  8. 最后一步:从app2.YourModel中删除Class Meta,然后运行命令:$ python manage.py makemigrations app2 && python manage.py migrate app2(如果你查看这个迁移文件,你会看到类似这样的内容:)

        migrations.AlterModelTable(
        name='yourmodel',
        table=None,
    ),
    

table=None,意味着它会使用默认的表名,在这个例子中就是app2_yourmodel。

  1. 完成,数据已保存。

附注:在迁移过程中,它会发现app1.yourmodel的内容类型已经被删除,可以被删除。你可以选择“是”,但前提是你不再使用它。如果你非常依赖这个内容类型的外键保持完整,暂时不要回答“是”或“否”,而是手动进入数据库,删除app2.yourmodel的内容类型,并把app1.yourmodel的内容类型重命名为app2.yourmodel,然后再继续选择“否”。

16

我在手动编写迁移时总是很紧张(正如Ozan的回答所要求的),所以下面的方法结合了Ozan和Michael的策略,以尽量减少手动编码的工作量:

  1. 在移动任何模型之前,先运行 makemigrations,确保你在一个干净的基础上工作。
  2. 把模型的代码从 app1 移动到 app2
  3. 按照@Michael的建议,我们使用“新”模型上的 db_table 元数据选项,将新模型指向旧的数据库表:

    class Meta:
        db_table = 'app1_yourmodel'
    
  4. 运行 makemigrations。这会在 app2 中生成 CreateModel,在 app1 中生成 DeleteModel。从技术上讲,这些迁移指的是完全相同的表,并会删除(包括所有数据)并重新创建该表。

  5. 实际上,我们并不想(也不需要)对表做任何事情。我们只需要让Django相信这个变化已经发生。根据@Ozan的回答,SeparateDatabaseAndState 中的 state_operations 标志可以做到这一点。所以我们将所有的 migrations 条目在两个迁移文件中都用 SeparateDatabaseAndState(state_operations=[...]) 包裹起来。例如:

    operations = [
        ...
        migrations.DeleteModel(
            name='YourModel',
        ),
        ...
    ]
    

    变成

    operations = [
        migrations.SeparateDatabaseAndState(state_operations=[
            ...
            migrations.DeleteModel(
                name='YourModel',
            ),
            ...
        ])
    ]
    
  6. 你还需要确保新的“虚拟” CreateModel 迁移依赖于任何实际创建或更改原始表的迁移。例如,如果你的新迁移是 app2.migrations.0004_auto_<date>(用于 Create)和 app1.migrations.0007_auto_<date>(用于 Delete),最简单的做法是:

    • 打开 app1.migrations.0007_auto_<date>,复制它的 app1 依赖项(例如 ('app1', '0006...'),)。这是 app1 中“紧接着前”的迁移,应该包括所有实际模型构建逻辑的依赖。
    • 打开 app2.migrations.0004_auto_<date>,将你刚复制的依赖项添加到它的 dependencies 列表中。

如果你有 ForeignKey 关系指向你要移动的模型,上面的步骤可能不适用。这是因为:

  • 对于 ForeignKey 的更改,依赖项不会自动创建
  • 我们不想将 ForeignKey 的更改包裹在 state_operations 中,所以我们需要确保它们与表操作分开。

注意:Django 2.2 添加了一个警告(models.E028),这会破坏这种方法。你可以尝试使用 managed=False 来解决,但我没有测试过。

“最小”操作集根据情况不同,但以下步骤应该适用于大多数/所有 ForeignKey 迁移:

  1. 复制 模型从 app1app2,设置 db_table,但不要更改任何 FK 引用。
  2. 运行 makemigrations 并将所有 app2 的迁移包裹在 state_operations 中(见上文)
    • 如上所述,在 app2CreateTable 中添加对最新 app1 迁移的依赖
  3. 将所有的 FK 引用指向新模型。如果你没有使用字符串引用,将旧模型移动到 models.py 的底部(不要删除它),这样它就不会与导入的类冲突。
  4. 运行 makemigrations,但不要将任何内容包裹在 state_operations 中(FK 更改应该实际发生)。在所有 ForeignKey 迁移(即 AlterField)中添加对 app2CreateTable 迁移的依赖(你需要这个列表用于下一步,所以要记住它们)。例如:

    • 找到包含 CreateModel 的迁移,例如 app2.migrations.0002_auto_<date>,并复制该迁移的名称。
    • 找到所有对该模型有 ForeignKey 的迁移(例如,通过搜索 app2.YourModel 找到类似的迁移:

      class Migration(migrations.Migration):
      
          dependencies = [
              ('otherapp', '0001_initial'),
          ]
      
          operations = [
              migrations.AlterField(
                  model_name='relatedmodel',
                  name='fieldname',
                  field=models.ForeignKey(... to='app2.YourModel'),
              ),
          ]
      
    • CreateModel 迁移添加为依赖:

      class Migration(migrations.Migration):
      
          dependencies = [
              ('otherapp', '0001_initial'),
              ('app2', '0002_auto_<date>'),
          ]  
      
  5. app1 中移除模型。

  6. 运行 makemigrations,并将 app1 的迁移包裹在 state_operations 中。
    • 为上一步中的所有 ForeignKey 迁移(即 AlterField)添加依赖(可能包括 app1app2 中的迁移)。
    • 当我构建这些迁移时,DeleteTable 已经依赖于 AlterField 迁移,所以我不需要手动强制执行(即 AlterDelete 之前)。

到此为止,Django 就可以正常工作了。新模型指向旧表,Django 的迁移已经让它相信一切都已正确迁移。一个重要的注意事项(来自@Michael的回答)是,新的 ContentType 会为新模型创建。如果你通过 ForeignKey 链接到内容类型,你需要创建一个迁移来更新 ContentType 表。

我想在自己完成后清理一下(元数据选项和表名),所以我使用了以下步骤(来自@Michael):

  1. 移除 db_table 元数据条目。
  2. 再次运行 makemigrations 以生成数据库重命名。
  3. 编辑最后这个迁移,确保它依赖于 DeleteTable 迁移。虽然看起来不应该是必要的,因为 Delete 应该是纯逻辑的,但如果我不这样做,就会遇到错误(例如 app1_yourmodel 不存在)。
30

我遇到了同样的问题。Ozan的回答对我帮助很大,但不幸的是还不够。实际上,我有几个外键(ForeignKey)链接到我想移动的模型。经过一番折腾,我找到了解决办法,所以决定分享出来,帮大家节省时间。

你需要多做两个步骤:

  1. 在开始之前,把所有链接到TheModel的外键(ForeignKey)改成整数字段(IntegerField)。然后运行python manage.py makemigrations
  2. 完成Ozan的步骤后,再把你的外键改回来:把IntegerField()换成ForeignKey(TheModel)。然后再次进行迁移(python manage.py makemigrations)。接着你就可以进行迁移操作了,它应该能正常工作(python manage.py migrate)。

希望这能帮到你。当然,记得在本地测试一下再在生产环境中使用,以免出现意外情况哦 :)

379

这件事可以通过使用 migrations.SeparateDatabaseAndState 来比较简单地完成。基本上,我们通过一个数据库操作来重命名表,同时进行两个状态操作,一个是把模型从旧应用的历史中移除,另一个是把它添加到新应用的历史中。

从旧应用中移除

python manage.py makemigrations old_app --empty

在迁移文件中:

class Migration(migrations.Migration):

    dependencies = []

    database_operations = [
        migrations.AlterModelTable('TheModel', 'newapp_themodel')
    ]

    state_operations = [
        migrations.DeleteModel('TheModel')
    ]

    operations = [
        migrations.SeparateDatabaseAndState(
            database_operations=database_operations,
            state_operations=state_operations)
    ]

添加到新应用

首先,把模型复制到新应用的 model.py 文件中,然后:

python manage.py makemigrations new_app

这样会生成一个迁移文件,里面只有一个简单的 CreateModel 操作。然后把这个操作放在一个 SeparateDatabaseAndState 操作里,这样我们就不会试图重新创建这个表。同时,还要把之前的迁移作为依赖项包含进来:

class Migration(migrations.Migration):

    dependencies = [
        ('old_app', 'above_migration')
    ]

    state_operations = [
        migrations.CreateModel(
            name='TheModel',
            fields=[
                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
            ],
            options={
                'db_table': 'newapp_themodel',
            },
            bases=(models.Model,),
        )
    ]

    operations = [
        migrations.SeparateDatabaseAndState(state_operations=state_operations)
    ]
32

我把之前的回答删掉了,因为可能会导致数据丢失。正如ozan提到的,我们可以在每个应用里创建两个迁移文件。下面的评论是针对我之前的回答。

第一个迁移文件是为了从第一个应用中移除模型。

$ python manage.py makemigrations old_app --empty

接着,编辑迁移文件,加入这些操作。

class Migration(migrations.Migration):

    database_operations = [migrations.AlterModelTable('TheModel', 'newapp_themodel')]

    state_operations = [migrations.DeleteModel('TheModel')]

    operations = [
      migrations.SeparateDatabaseAndState(
        database_operations=database_operations,
        state_operations=state_operations)
    ]

第二个迁移文件依赖于第一个迁移文件,并在第二个应用中创建新的表。在把模型代码移动到第二个应用后,

$ python manage.py makemigrations new_app 

再把迁移文件编辑成这样。

class Migration(migrations.Migration):

    dependencies = [
        ('old_app', 'above_migration')
    ]

    state_operations = [
        migrations.CreateModel(
            name='TheModel',
            fields=[
                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
            ],
            options={
                'db_table': 'newapp_themodel',
            },
            bases=(models.Model,),
        )
    ]

    operations = [
        migrations.SeparateDatabaseAndState(state_operations=state_operations)
    ]

撰写回答