2.5 migrate命令的实现原理

在2.4节中,生成shell_test应用下的迁移文件后,执行migrate命令,即可将迁移文件中的表信息及其数据映射到项目配置的数据库中:

在执行上述操作后,可以看到数据库中的django_books表已经被创建,一同被创建的还有django_migrations表。在django_migrations表中只有一条记录,如图2-4所示。

图2-4

在对该命令有了初步印象后,思考以下两个问题:

◎ migrate命令是如何将迁移文件中的信息映射到数据库中的?

◎ 在执行migrate命令时,可能会出现哪些异常?出现这些异常的原因是什么,又该如何复现呢?

下面我们就带着这些问题开启migrate命令的源码追踪之旅。migrate命令对应的源码如下:

上面是整个migrate命令的执行过程,省略了部分细节代码,同时对重要的代码段进行了相应注释。为了帮助读者理解migrate命令的执行流程,笔者整理出5处代码进行讲解:

第1处代码是实例化MigrationExecutor类,这一步非常重要,因为后续迁移文件的一致性检测、冲突检测,以及最终的解析迁移文件等动作都依赖于得到的MigrationExecutor类。MigrationExecutor类的源码如下:

可以看到,MigrationExecutor对象的loader属性即前面介绍过的MigrationLoader对象。在migrate命令的后续执行过程中会调用该对象的check_consistent_history()方法和detect_conflicts()方法,分别检测迁移文件之间的一致性和冲突。这两个方法的细节已在2.4.3节中介绍过,不再赘述。

第2处代码是通过输入的应用(可能没有)及相应的迁移文件(可能没有)得到最终的迁移目标文件列表的。以python manage.py shell_test命令为例,由于第一次使用makemigrations命令生成了迁移文件0001_initial.py,因此代码将进入elif options['app_label']分支中执行,最终得到targets=[('shell_test','0001_initial')]。

第3处代码是调用MigrationExecutor对象的migration_plan()方法得到相应的迁移计划。该方法的代码如下:

对于首次进行迁移或者是未应用到数据库中的迁移对象时,程序会在最后的else分支进行处理。以迁移命令python manage.py shell_test为例,由于是第一次执行迁移命令,相关的表都还未创建,此时applied为空集合。通过前面的分析已知target的值为[('shell_test','0001_initial')],于是在进入for循环遍历后,target[1]有值且不在applied中,因此会进入else分支执行。在else分支的代码段中,要理解self.loader.graph.forwards_plan(target)语句的含义,该语句的含义是根据当前的完整迁移图找出target节点所依赖的迁移节点,而这些迁移节点所代表的迁移文件必须要先应用到数据库中,然后才能迁移。为了方便理解,这里先初除前面创建的django_books表和django_migrations表,然后创建一个新的应用book_sales:

(1)将应用book_sales添加到settings.py文件的INSTALLED_APPS变量中:

(2)在应用book_sales中创建一个模型类BookSales。该模型类表示前面Django图书的整体销售情冴,模型定义如下:

这里的isbn字段关联了DjangoBook模型类中的isbn字段,因此模型类BookSales依赖模型类DjangoBook。

(3)对该应用执行迁移命令:

通过上面的输出可以看出,当迁移应用book_sales中的模型时,会自动发现其依赖应用的模型文件。此时先迁移依赖模型,再迁移自身。这段逻辑刚好对应migration_plan()方法中最后的else分支,最终生成的迁移记录如图2-5所示。

图2-5

Django是如何找到迁移节点的所有依赖节点的?直接看MigrationGraph类中的forwards_plan()方法即可:

上面的算法是通过栈实现找到该节点的所有依赖节点或者被依赖节点的。而查找的核心是,从磁盘上读取所有的迁移文件,构建相应的依赖关系,即各迁移节点的parents属性和children属性。

从第4处代码可以看到,对于使用plan选项的,只显示将要执行迁移的动作然后直接返回,并不会真的操作数据库。为了演示效果,这里直接初除django_migrations表中的两条记录,再次执行migrate命令:

查看数据库,可以看到并没有生成迁移记录,说明程序在打印了迁移操作后就直接返回了。

第5处代码是迁移命令的核心,这里将生成相关模型类对应的表及迁移记录。它直接调用MigrationExecutor对象的migrate()方法,因此继续追踪migrate()方法的实现源码:

这里的逻辑代码从总体上看并不复杂,但需要理解plan的输出。下面简单介绍一下在第3处代码中得到的plan结果。前面分析过,plan是一串迁移计划的组合,该迁移计划是一个二元组,即(迁移对象,True|False)。其中,当第2个元素为True时,表示该迁移文件(target)在迁移表中,且其子孙迁移节点(同一应用)也已经出现在记录表中(对于target第2个元素为None的情冴先不讨论)。而当第2个元素为False时,表示该迁移文件并未在迁移表中出现过。当第一次执行迁移指令时,迁移文件(包括其依赖的迁移文件)得到的plan=[(迁移对象1,False),(迁移对象2,False),...,(迁移对象n,False)]。此时,all_forwards的值为True,而all_backwards的值为False。最终Django会调用else all_forwars分支下的state=self._migrate_all_forwards()语句,该语句是迁移操作的核心。继续追踪_migrate_all_forwards()方法的实现源码:

在上面的代码中,会遍历所有的迁移对象。如果迁移对象在迁移表中,则调用apply_migration()方法执行迁移操作。继续追踪apply_migration()方法的实现源码:

从上面的代码可以看到真正操作数据库的动作:self.record_migration()方法用于更新迁移记录,它的实现比较简单,正是借助2.4节中介绍的MigrationRecorder对象完成的;apply()方法位于Migration类中,该方法通过遍历Migration对象的operations属性实现对数据库的映射,其实现的具体细节不再介绍,有兴趣的读者可以自行深入研究。