Skip to main content

迁移

迁移是Django传播对模型所做的更改(添加字段,删除模型等)到数据库模式的方式。它们的设计大多是自动的,但你需要知道何时进行迁移,何时运行它们以及可能遇到的常见问题。

命令

有几个命令将用于与迁移和Django处理数据库模式交互:

  • migrate,负责应用和取消应用迁移。

  • makemigrations,负责根据您对模型所做的更改创建新的迁移。

  • sqlmigrate,它显示迁移的SQL语句。

  • showmigrations,其中列出了项目的迁移及其状态。

您应该将迁移视为数据库模式的版本控制系统。 makemigrations 负责将模型更改打包为单个迁移文件(类似于提交),migrate 负责将这些更改应用到数据库。

每个应用程序的迁移文件都位于该应用程序中的“migrations”目录中,并设计为提交并作为其代码库的一部分进行分发。你应该在开发机器上进行一次,然后在同事的机器,暂存机器,最终在生产机器上运行相同的迁移。

注解

可以通过修改 MIGRATION_MODULES 设置来覆盖包含基于每个应用程序的迁移的包的名称。

迁移将以相同的方式在同一数据集上运行,并产生一致的结果,这意味着在开发和分级中看到的是,在相同的情况下,正是在生产中将发生什么。

Django将对你的模型或字段进行任何更改迁移(即使是不影响数据库的选项),因为它可以正确重建字段的唯一方法是在历史记录中进行所有更改,您可能需要这些选项一些数据迁移(例如,如果您已设置自定义验证器)。

后端支持

Django附带的所有后端都支持迁移,以及任何第三方后端,如果他们已经编程支持模式更改(通过 SchemaEditor 类完成)。

但是,一些数据库比其他数据库在模式迁移方面更有能力;一些注意事项如下。

PostgreSQL

PostgreSQL是所有数据库中最能够支持的模式;唯一的警告是添加具有默认值的列将导致表的完全重写,其时间与其大小成比例。

因此,建议您始终使用 null=True 创建新列,因为它们将立即添加。

MySQL

MySQL缺少对模式更改操作的事务的支持,这意味着如果迁移无法应用,您将必须手动取消批准更改才能再次尝试(不可能回滚到更早的点)。

此外,MySQL将为每个模式操作完全重写表,通常需要与表中的行数成正比的时间来添加或删除列。在较慢的硬件上,这可能比每分钟一百万行更糟 - 向表中添加几列只有几百行可能会锁定您的网站超过十分钟。

最后,MySQL对列,表和索引的名称长度具有相对较小的限制,以及对索引涵盖的所有列的组合大小的限制。这意味着在其他后端可能的索引将无法在MySQL下创建。

SQLite

SQLite具有非常少的内置模式更改支持,因此Django尝试通过以下方式模拟它:

  • 使用新模式创建新表

  • 复制数据

  • 删除旧的表

  • 重命名新表以匹配原始名称

这个过程一般效果很好,但它可能很慢,偶尔会出现问题。不建议您在生产环境中运行和迁移SQLite,除非您非常了解风险及其限制;支持Django的设计允许开发人员在其本地机器上使用SQLite开发不太复杂的Django项目,而不需要一个完整的数据库。

工作流

使用迁移很简单。对模型进行更改 - 例如,添加字段并删除模型 - 然后运行 makemigrations:

$ python manage.py makemigrations
Migrations for 'books':
  books/migrations/0003_auto.py:
    - Alter field author on book

您的模型将被扫描并与迁移文件中当前包含的版本进行比较,然后将写出一组新的迁移。确保阅读输出,看看 makemigrations 认为你已经改变 - 这不是完美的,对于复杂的变化,它可能不是检测到你的期望。

获得新的迁移文件后,应将其应用到数据库,以确保它们按预期工作:

$ python manage.py migrate
Operations to perform:
  Apply all migrations: books
Running migrations:
  Rendering model states... DONE
  Applying books.0003_auto... OK

一旦应用迁移,提交迁移并将模型作为单个提交更改为版本控制系统 - 这样,当其他开发人员(或您的生产服务器)检出代码时,他们将获得模型的更改并伴随着迁移的同时。

如果您希望为迁移提供有意义的名称,而不是生成的名称,则可以使用 makemigrations --name 选项:

$ python manage.py makemigrations --name changed_my_model your_app_label

版本控制

由于迁移存储在版本控制中,因此您偶尔会遇到以下情况:您和另一个开发人员同时提交迁移到同一个应用程序,导致两个迁移具有相同的编号。

别担心,这些数字只供开发人员参考,Django只关心每个迁移都有不同的名称。迁移指定文件中依赖的其他迁移(包括在同一应用中的早期迁移),因此可以检测同一应用的两个新迁移是否未订购。

当这种情况发生时,Django会提示你并给你一些选项。如果它认为它足够安全,它会提供自动线性化两个迁移为你。如果没有,你必须自己进入和修改迁移 - 不要担心,这不难,并在下面的 迁移文件 解释更多。

依赖

虽然迁移是基于应用程序的,但是模型所包含的表和关系太复杂,无法一次为一个应用程序创建。当您进行需要运行其他操作的迁移时,例如,您在 books 应用程序中将 ForeignKey 添加到您的 authors 应用程序中 - 生成的迁移将包含对 authors 中迁移的依赖。

这意味着当您运行迁移时,authors 迁移首先运行,并创建 ForeignKey 引用的表,然后迁移使 ForeignKey 列随后运行并创建约束。如果没有发生这种情况,迁移将尝试创建 ForeignKey 列,而不引用现有的表,并且您的数据库会抛出错误。

此依赖关系行为会影响大多数将操作限制为单个应用程序的迁移操作。限制到单个应用程序(在 makemigrationsmigrate)是一个尽力而为的承诺,而不是保证;任何其他应用程序需要用于获取依赖关系正确将是。

迁移文件

迁移存储为磁盘格式,此处称为“迁移文件”。这些文件实际上只是普通的Python文件,具有商定的对象布局,以声明样式编写。

基本迁移文件如下所示:

from django.db import migrations, models

class Migration(migrations.Migration):

    dependencies = [("migrations", "0001_initial")]

    operations = [
        migrations.DeleteModel("Tribble"),
        migrations.AddField("Author", "rating", models.IntegerField(default=0)),
    ]

Django在加载迁移文件(作为Python模块)时寻找的是 django.db.migrations.Migration 的一个子类,称为 Migration。然后它检查此对象的四个属性,大多数时间只使用其中的两个:

  • dependencies,这是一个依赖的迁移列表。

  • operationsOperation 类的列表,定义了此迁移的作用。

操作是关键;它们是一组声明性指令,它们告诉Django需要做什么模式更改。 Django扫描它们并构建所有应用程序的所有模式更改的内存中表示,并使用它来生成使模式更改的SQL。

内存结构也用于计算模型和迁移的当前状态之间的差异; Django运行所有的更改,按顺序,在一个内存中的模型集,以找出您的模型的上次运行 makemigrations 的状态。然后使用这些模型来比较您的 models.py 文件中的那些,以计算出您的更改。

您应该很少(如果曾经)需要手动编辑迁移文件,但如果需要,完全可以手动编写它们。一些更复杂的操作不是自动检测的,只能通过手写迁移提供,所以不要害怕如果你必须编辑它们。

自定义字段

您不能在未升级 TypeError 的情况下修改已迁移的自定义字段中的位置参数的数量。旧迁移将使用旧签名调用修改的 __init__ 方法。所以如果你需要一个新的参数,请创建一个关键字参数,并在构造函数中添加一些东西,比如 assert 'argument_name' in kwargs

模型管理者

您可以选择将管理器序列化为迁移,并使其可用于 RunPython 操作。这通过在管理器类上定义 use_in_migrations 属性来完成:

class MyManager(models.Manager):
    use_in_migrations = True

class MyModel(models.Model):
    objects = MyManager()

如果使用 from_queryset() 函数动态生成管理器类,则需要继承生成的类以使其可导入:

class MyManager(MyBaseManager.from_queryset(CustomQuerySet)):
    use_in_migrations = True

class MyModel(models.Model):
    objects = MyManager()

请参阅迁移中有关 历史模型 的注意事项,以了解随之而来的影响。

初始迁移

Migration.initial
New in Django 1.9.

应用程序的“初始迁移”是创建该应用程序表的第一个版本的迁移。通常一个应用程序只有一个初始迁移,但在某些情况下,复杂的模型相互依赖它可能有两个或更多。

初始迁移在迁移类上标记有 initial = True 类属性。如果未找到 initial 类属性,则迁移将被视为“初始”,如果它是应用程序中的第一次迁移(即,如果它与同一应用中的任何其他迁移没有依赖关系)。

当使用 migrate --fake-initial 选项时,将特别处理这些初始迁移。对于创建一个或多个表(CreateModel 操作)的初始迁移,Django检查所有这些表是否已在数据库中存在,并且假如 - 如果是,应用迁移。类似地,对于添加一个或多个字段(AddField 操作)的初始迁移,Django检查数据库中是否已存在所有相应的列,如果是,则使用假应用迁移。没有 --fake-initial,初始迁移与任何其他迁移没有区别。

历史一致性

如前所述,您可能需要在连接两个开发分支时手动线性化迁移。在编辑迁移依赖关系时,您可能无意中创建了一个不一致的历史状态,其中已应用迁移,但其某些依赖关系尚未应用。这是一个强烈的指示,依赖关系是不正确的,所以Django将拒绝运行迁移或进行新的迁移,直到它被修复。当使用多个数据库时,您可以使用 数据库路由器allow_migrate() 方法来控制哪些数据库 makemigrations 检查一致的历史记录。

Changed in Django 1.10:

添加了迁移一致性检查。在1.10.1中添加了基于数据库路由器的检查。

将迁移添加到应用

将迁移添加到新应用程序是很简单的 - 它们已经预先配置为接受迁移,因此只要在进行了一些更改后运行 makemigrations

如果您的应用程序已经有模型和数据库表,并且还没有迁移(例如,您是根据以前的Django版本创建的),则需要将其转换为使用迁移;这是一个简单的过程:

$ python manage.py makemigrations your_app_label

这将为您的应用程序进行新的初始迁移。现在,运行 python manage.py migrate --fake-initial,Django将检测到您有一个初始迁移 and,它想要创建的表已经存在,并将标记为已应用迁移。 (没有 migrate --fake-initial 标志,命令会错误出来,因为它想要创建的表已经存在)。

注意,这只工作给予两件事:

  • 你没有改变你的模型,因为你制作了他们的表。要使迁移工作,您必须进行初始迁移 first,然后进行更改,因为Django将更改与迁移文件进行比较,而不是数据库。

  • 您没有手动编辑数据库 - Django将无法检测到您的数据库与您的模型不匹配,只有在迁移尝试修改这些表时,您才会收到错误。

历史模型

当您运行迁移时,Django使用存储在迁移文件中的模型的历史版本。如果你使用 RunPython 操作编写Python代码,或者如果你的数据库路由器上有 allow_migrate 方法,你将暴露给这些版本的模型。

因为不可能序列化任意Python代码,所以这些历史模型不会有你定义的任何自定义方法。然而,他们将有相同的领域,关系,经理(限于那些与 use_in_migrations = True)和 Meta 选项(也版本化,因此它们可能不同于您当前的)。

警告

这意味着,当您在迁移中访问对象时,您不会有对对象调用的自定义 save() 方法,并且您不会有任何自定义构造函数或实例方法。计划适当!

upload_tolimit_choices_to 等字段选项中的函数引用以及具有 use_in_migrations = True 的管理器的模型管理器声明在迁移中被序列化,因此只要存在引用它们的迁移,就需要保持函数和类。任何 自定义模型字段 也需要保留,因为这些是直接通过迁移导入的。

此外,模型的基类只存储为指针,因此只要存在包含对它们的引用的迁移,就必须始终保持基类。在正面,这些基类的方法和管理器通常继承,所以如果你绝对需要访问这些,你可以选择将它们移动到超类。

要删除旧引用,可以使用 壁球迁移,如果没有多个引用,则将它们复制到迁移文件中。

删除模型字段时的注意事项

类似于上一节中介绍的“对历史函数的引用”注意事项,如果在旧迁移中引用了自定义模型字段,则从项目或第三方应用中移除自定义模型字段将会导致问题。

为了帮助这种情况,Django提供了一些模型字段属性来帮助使用 系统检查框架 的模型字段弃用。

system_check_deprecated_details 属性添加到模型字段,类似于以下内容:

class IPAddressField(Field):
    system_check_deprecated_details = {
        'msg': (
            'IPAddressField has been deprecated. Support for it (except '
            'in historical migrations) will be removed in Django 1.9.'
        ),
        'hint': 'Use GenericIPAddressField instead.',  # optional
        'id': 'fields.W900',  # pick a unique ID for your field.
    }

在您选择的折旧期后(Django本身的字段的两个或三个功能版本),将 system_check_deprecated_details 属性更改为 system_check_removed_details 并更新字典类似于:

class IPAddressField(Field):
    system_check_removed_details = {
        'msg': (
            'IPAddressField has been removed except for support in '
            'historical migrations.'
        ),
        'hint': 'Use GenericIPAddressField instead.',
        'id': 'fields.E900',  # pick a unique ID for your field.
    }

您应该保留字段在数据库迁移(如 __init__()deconstruct()get_internal_type())中操作所需的方法。只要引用此字段的任何迁移存在,就保留此存根字段。例如,在压缩迁移并删除旧迁移后,您应该可以完全删除该字段。

数据迁移

除了更改数据库模式,您还可以使用迁移来更改数据库中的数据,如果需要,还可以与模式一起使用。

更改数据的迁移通常称为“数据迁移”;它们最好写成单独的迁移,与您的模式迁移一起。

Django不能为你自动生成数据迁移,就像它对模式迁移一样,但是写它们并不困难。 Django中的迁移文件由 操作 组成,用于数据迁移的主要操作是 RunPython

首先,使一个空的迁移文件,你可以工作(Django会把文件放在正确的地方,建议一个名字,并为你添加依赖项):

python manage.py makemigrations --empty yourappname

然后,打开文件;它应该看起来像这样:

# -*- coding: utf-8 -*-
# Generated by Django A.B on YYYY-MM-DD HH:MM
from __future__ import unicode_literals

from django.db import migrations, models

class Migration(migrations.Migration):
    initial = True

    dependencies = [
        ('yourappname', '0001_initial'),
    ]

    operations = [
    ]

现在,所有你需要做的是创建一个新的函数,并有 RunPython 使用它。 RunPython 期望可调用作为其参数,它接受两个参数 - 第一个是 应用程序注册表,其中加载了所有模型的历史版本,以匹配迁移所在的历史记录,第二个是 SchemaEditor,您可以使用手动实现数据库模式更改(但要小心,这样做可能会混淆迁移自动检测!)

让我们写一个简单的迁移,填充我们的新 name 字段与 first_namelast_name 的组合值(我们来到了我们的感觉,意识到,不是每个人都有名和姓)。所有我们需要做的是使用历史模型和迭代的行:

# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.db import migrations, models

def combine_names(apps, schema_editor):
    # We can't import the Person model directly as it may be a newer
    # version than this migration expects. We use the historical version.
    Person = apps.get_model("yourappname", "Person")
    for person in Person.objects.all():
        person.name = "%s %s" % (person.first_name, person.last_name)
        person.save()

class Migration(migrations.Migration):
    initial = True

    dependencies = [
        ('yourappname', '0001_initial'),
    ]

    operations = [
        migrations.RunPython(combine_names),
    ]

一旦完成,我们可以正常运行 python manage.py migrate,数据迁移将与其他迁移一起运行。

您可以向 RunPython 传递第二个可调用项,以便在向后迁移时运行您希望执行的任何逻辑。如果省略此可调用项,向后迁移将引发异常。

从其他应用访问模型

在编写使用来自迁移所在应用程序以外的应用程序的模型的 RunPython 函数时,迁移的 dependencies 属性应包含所涉及的每个应用程序的最新迁移,否则您可能会收到类似于以下错误的错误:LookupError: No installed app with label 'myappname' 以使用 apps.get_model()RunPython 函数中检索模型。

在下面的示例中,我们在 app1 中有一个迁移,需要使用 app2 中的模型。我们不关心 move_m1 的细节,除了它需要从两个应用程序访问模型的事实。因此,我们添加了一个依赖关系,指定 app2 的最后一次迁移:

class Migration(migrations.Migration):

    dependencies = [
        ('app1', '0001_initial'),
        # added dependency to enable using models from app2 in move_m1
        ('app2', '0004_foobar'),
    ]

    operations = [
        migrations.RunPython(move_m1),
    ]

更高级的迁移

如果您对更高级的迁移操作感兴趣,或者希望能够编写自己的迁移操作,请参阅 迁移操作参考写迁移 的“how-to”。

挤压迁移

鼓励你自由迁徙,不必担心你有多少;迁移代码被优化以一次处理几百个而没有大的减速。然而,最终你会想从几百个迁移回到几个,这就是挤压的地方。

压缩是将现有的许多迁移集减少到仍然表示相同更改的一个(或有时几个)迁移的行为。

Django通过采取所有现有的迁移,提取它们的 Operation 并将它们全部顺序,然后在它们上运行优化器来尝试并减少列表的长度 - 例如,它知道 CreateModelDeleteModel 取消彼此出来,它知道 AddField 可以滚动成 CreateModel

一旦操作序列被尽可能减少 - 可能的数量取决于模型的紧密程度,如果你有任何 RunSQLRunPython 操作(不能通过优化) - Django然后将它写回到一组新的迁移文件。

这些文件被标记为说他们替换以前被压缩的迁移,以便它们可以与旧的迁移文件共存,并且Django将根据您在历史记录中的位置智能地切换。如果您仍然部分地通过您压缩的迁移集,它将继续使用它们,直到它到达结束,然后切换到压缩的历史记录,而新的安装将只使用新的压缩迁移,并跳过所有的旧那些。

这使您可以压缩,而不是混乱当前生产中尚未完全更新的系统。建议的过程是压缩,保留旧文件,提交和释放,等待所有系统升级到新版本(或如果您是第三方项目,只是确保您的用户升级版本,而不跳过任何) ,然后删除旧的文件,提交并做第二个版本。

备份所有这一切的命令是 squashmigrations - 只是通过它的应用程序标签和迁移名称,你想挤压,它会工作:

$ ./manage.py squashmigrations myapp 0004
Will squash the following migrations:
 - 0001_initial
 - 0002_some_change
 - 0003_another_change
 - 0004_undo_something
Do you wish to proceed? [yN] y
Optimizing...
  Optimized from 12 operations to 7 operations.
Created new squashed migration /home/andrew/Programs/DjangoTest/test/migrations/0001_squashed_0004_undo_somthing.py
  You should commit this migration but leave the old ones in place;
  the new migration will be used for new installs. Once you are sure
  all instances of the codebase have applied the migrations you squashed,
  you can delete them.

注意,Django中的模型相互依赖性可能变得非常复杂,压缩可能导致迁移不会运行; (在这种情况下,您可以再次尝试使用 --no-optimize,虽然您也应该报告问题),或与 CircularDependencyError,在这种情况下您可以手动解决它。

要手动解析 CircularDependencyError,请将循环依赖关系循环中的一个ForeignKeys拆分为单独的迁移,并使用它移动其他应用程序的依赖关系。如果您不确定,请问当您从模型中创建全新的迁移时,makemigrations如何处理这个问题。在将来的Django版本中,squashmigrations将被更新以尝试自己解决这些错误。

一旦你压缩了你的迁移,你应该提交它与它替换的迁移,并将此更改分发到应用程序的所有正在运行的实例,确保他们运行 migrate 以将更改存储在他们的数据库中。

然后,您必须通过以下方式将压缩迁移转换为正常迁移:

  • 删除它替换的所有迁移文件。

  • 更新所有依赖于已删除迁移的迁移,以取决于已缩减的迁移。

  • 删除压缩迁移的 Migration 类中的 replaces 属性(这是Django如何解释它是一个被压缩的迁移)。

注解

一旦您压制了迁移,您就不应该重新压缩被压缩的迁移,直到您完全转换为正常迁移为止。

序列化值

迁移只是包含模型的旧定义的Python文件 - 因此,要编写它们,Django必须获取模型的当前状态并将它们序列化到一个文件中。

虽然Django可以序列化大多数东西,但有一些事情,我们不能序列化成一个有效的Python表示 - 没有Python标准如何将值转换回代码(repr() 仅适用于基本值, t指定导入路径)。

Django可以序列化以下内容:

  • intlongfloatboolstrunicodebytesNone

  • listsettupledict

  • datetime.datedatetime.timedatetime.datetime 实例(包括时区感知的实例)

  • decimal.Decimal 实例

  • enum.Enum 实例

  • 具有可序列化 funcargskeywords 值的 functools.partial 实例。

  • 包装可序列化值的 LazyObject 实例。

  • 任何Django字段

  • 任何函数或方法引用(例如 datetime.datetime.today)(必须在模块的顶级作用域中)

  • 任何类引用(必须在模块的顶级作用域中)

  • 使用自定义 deconstruct() 方法(见下文

Changed in Django 1.9:

添加了对 functools.partialLazyObject 实例的序列化支持。

Changed in Django 1.10:

添加了对 enum.Enum 的序列化支持。

Django只能在Python 3上序列化以下内容:

  • 在类体内使用的未绑定方法(见下文)

Django不能序列化:

  • 嵌套类

  • 任意类实例(例如 MyClass(4.3, 5.7)

  • Lambdas

由于 __qualname__ 只在Python 3中引入,Django只能序列化Python 3上的以下模式(在类体中使用的未绑定方法),并且将无法序列化对Python 2的引用:

class MyModel(models.Model):

    def upload_to(self):
        return "something dynamic"

    my_file = models.FileField(upload_to=upload_to)

如果您使用的是Python 2,我们建议您将方法upload_to和类似的参数接受callables(例如 default)居住在主模块体,而不是类体。

添加 deconstruct() 方法

你可以让Django通过给类 deconstruct() 方法序列化你自己的自定义类实例。它不需要参数,应该返回一个三元组的元素 (path, args, kwargs)

  • path 应该是类的Python路径,类名作为最后一部分包括(例如,myapp.custom_things.MyClass)。如果你的类在模块的顶层不可用,那么它是不可序列化的。

  • args 应该是一个传递给您的类’__init__ 方法的位置参数的列表。这个列表中的所有内容都应该是可序列化的。

  • kwargs 应该是传递给您的类’__init__ 方法的关键字参数的dict。每个值本身都是可序列化的。

注解

此返回值不同于 deconstruct() 方法 自定义字段,它返回四个项的元组。

Django将写出这个值作为你的类的实例化与给定的参数,类似于它写出Django字段的引用的方式。

为了防止每次运行 makemigrations 时都创建新的迁移,您还应该将 __eq__() 方法添加到装饰类中。这个函数将被Django的迁移框架调用来检测状态之间的变化。

只要你的类的构造函数的所有参数都是可序列化的,你就可以使用 django.utils.deconstruct@deconstructible 类装饰器来添加 deconstruct() 方法:

from django.utils.deconstruct import deconstructible

@deconstructible
class MyCustomClass(object):

    def __init__(self, foo=1):
        self.foo = foo
        ...

    def __eq__(self, other):
        return self.foo == other.foo

装饰器添加逻辑来捕获和保存参数到它们的构造函数中,然后在调用deconstruct()时返回这些参数。

支持Python 2和3

为了生成支持Python 2和3的迁移,在模型和字段(例如 verbose_namerelated_name 等)中使用的所有字符串文字必须在Python 2和3中都通过测试字符串或文本(unicode)字符串而不是Python 2中的字节和Python 3中的文本,未标记的字符串文字的默认情况。)否则,在Python 3下运行 makemigrations 将会生成虚假的新迁移,将所有这些字符串属性转换为文本。

实现这个的最简单的方法是遵循Django的 Python 3移植指南 中的建议,并确保所有的模块都以 from __future__ import unicode_literals 开头,以便所有未标记的字符串文字总是unicode,而不管Python版本。当你添加到一个应用程序与现有的迁移生成Python 2,你下一次运行的 makemigrations 在Python 3可能会产生许多更改,因为它将所有的bytestring属性转换为文本字符串;这是正常的,应该只发生一次。

支持多个Django版本

如果您是使用模型的第三方应用程序的维护者,则可能需要提供支持多个Django版本的迁移。在这种情况下,应始终运行 makemigrations 与您希望支持的最低的Django版本

迁移系统将根据与其他Django相同的策略保持向后兼容性,因此在Django X.Y上生成的迁移文件应该在Django X.Y + 1上保持不变。然而,迁移系统不承诺向前兼容性。可能会添加新功能,并且使用较新版本的Django生成的迁移文件可能无法在旧版本上运行。

参见

迁移操作参考

涵盖模式操作API,特殊操作和编写自己的操作。

写作迁移“how-to”

说明如何为可能遇到的不同方案构建和写入数据库迁移。