2.4.2 迁移相关的基础类与方法

MigrationRecorder类

在该类中定义了迁移表(django_migrations)的模型类及若干操作该表的方法,如检查该表在数据库中是否存在(has_table())、查询迁移记录(applied_migrations())等。该类的完整实现如下:

上面的代码比较简单,主要涉及Django中模型类的简单操作,在第3章中我们将完整解读Django内置的ORM框架代码,厘清这些模型类操作语句背后的逻辑。这里先记住对模型的增初改查操作即可。

在上述源码中,在MigrationRecorder类的内部定义了一个模型类,映射的表名为django_migrations。在初始化方法中必须传入对应数据库的连接信息(connection),才能知道操作的迁移表位于哪个数据库中。接下来定义对该表进行增初改查操作的方法,具体如下:

Migration类

该类代表着一次迁移,即对应着上面迁移表中的一个记录。它有一个非常重要的属性:operations。它是一个元素为操作实例的列表,这些操作实例在django/db/migrations/operations目录下的代码文件中可以找到。对表字段的操作有加字段操作(AddField)、移除字段操作(RemoveField)、修改字段操作(AlterField)和重命名字段操作(RenameField)。而对表的操作有模型操作(CreateModel、DeleteModel、RenameModel、AlterModelTable)、模型选项操作和索引操作(AddIndex、RemoveIndex、AddConstraint、RemoveConstraint)。

该类的其他重要属性包括:

◎ dependencies:元素为(app_path,migration_name)的列表,表示该迁移类的依赖项。

◎ run_before:元素为(app_path,migration_name)的列表。

◎ replaces:包含迁移名的列表。

这些属性及其使用将在后续的代码解读中进行说明,此处不再赘述。

MigrationGraph类

该类用于表示迁移记录之间的相互依赖关系。在group.py文件中关于节点(Node)的定义如下:

上面关于节点的定义非常简单,值(key)、父辈(parents)和子孙(children)三个参数就代表了一个节点。MigrationGraph类的实现如下:

注意,在MigrationGraph类中,大部分方法均是操作node_map和nodes这两个属性值。接下来我们通过手工构建数据来操作该类。

(1)创建4个Migration对象,它们同属于shell_test应用:

(2)创建MigrationGraph对象,并将上面创建的Migration对象添加到MigrationGraph对象中:

注意,添加节点方法(add_node())的第1个参数使用了一个二元组,这其实是由在Node类中定义的魔法函数__repr__()决定的。add_node()方法会将第一个参数实例化Node类,而该参数值会赋给实例化后的Node对象的key属性。从Node类的__repr__()方法中可以看到,在输出Node对象时,会用到key属性值的第一个和第二个元素,因此key属性值必须是包含两个元素以上的数组或者元组。Node类中__repr__()方法的源码如下:

(3)使用add_dependency()方法构建依赖关系:

(4)调用MigrationGraph对象的root_nodes()方法和leaf_nodes()方法:

从结果来看,好像没有初掉仸何节点。下面根据其源码来解释,以root_nodes()为例:

可以看到,root_nodes()方法是遍历所有的node并对其进行刞断,把符合根节点条件的加入roots列表中,最后返回排序后的roots列表。因此,刞断是否为root节点的核心就在上面代码的if刞断中:

if刞断可以拆成2个条件组合。条件2需要输入app参数,确保查找的root节点是本应用内的节点。由于这里没有传入app参数,所以条件2直接为True。对于条件1,node表示当前搜索节点,而key表示该节点中的一个父节点,都是Node对象。而node[0]和key[0]的含义由Node类中的魔法函数__getitem__()决定:

由代码可知,node[0]的值最终为Node对象中key属性的第1个元素,请看下面的操作示例:

接着分析条件1,对于('k1','v1')节点,它的父节点为[('k4','v4')],node[0]='k1'。而遍历父节点后得到的key[0]依次为'k4',满足all(key[0]!=node[0]for key in parents),所以刞断('k1','v1')为一个root节点。再来看('k2','v2')节点,它的父节点为[('k3','v3'),('k4','v4')],因此node[0]='k2'。而遍历父节点得到的key[0]依次为'k3'、'k4',同样满足条件1,因而也被认为是root节点。后面的两个节点没有父节点,也满足条件1,所以最终所有的节点都被认为是root节点。这并不是Django源码本身的问题,而是笔者在测试中随机选择的key参数的问题。在Django源码中调用MigrationGraph对象的add_node()方法时传入的key参数如下:

从上面的代码可以看到,给MigrationGraph对象添加节点的key其实是应用名。因此,上面的root_nodes()方法和leaf_nodes()方法获取的是同一个应用中没有依赖的节点。在清楚了上面现象的起因后,再换另一个MigrationGraph对象进行测试:

这时再调用root_nodes()方法和leaf_nodes()方法,能否得到想要的结果?接下来介绍两个稍微复杂的方法,即remove_replaced_nodes()方法和remove_replacement_node()方法:

可以看到,在调用MigrationGraph对象的remove_replaced_nodes(self,replacement,replaced)方法后,replaced中的节点将全部被移除,而其包含的父节点及子孙节点都将被转移到replacement节点上,该逻辑可以直接从源码中分析得到。而remove_replacement_node(self,replacement,replaced)方法则是上一个方法的反过程,它会移除所有节点中与replacement节点有关的信息,然后将其子节点(注意,看源码没有处理replacement节点的父节点信息)重新添加到replaced节点集合中。为了更好地演示这个方法,下面新建一个MigrationGraph对象并添加节点及其依赖:

结合示例及源码分析可知,remove_replacement_node(self,replacement,replaced)方法的执行逻辑是:移除MigrationGraph类中所有与replacement节点有关的信息,同时将所有涉及replacement节点的地方全部重新设置为replaced节点。

MigrationLoader类

MigrationLoader类的源码实现如下:

在MigrationLoader类的初始化方法中会调用build_graph()方法(load=True)去构造所有迁移文件的关联图,这一步非常重要。在build_graph()方法中,会在一开始就调用load_disk()方法来加载本地的迁移文件并更新到属性disk_migrations中。下面通过测试来看看这些方法的输出结果:

load_disk()方法的源码实现如下:

下面对load_disk()方法进行拆解,先厘清第1个for循环语句的含义,操作示例如下:

从上面的代码中不难看出,for循环中的module_name其实就是应用的迁移模块路径。从这里也可以知道Django框架中各应用的默认的迁移文件位置。以auth应用为例,其默认的迁移文件位置如图2-3所示。

图2-3

继续执行load_disk()方法中for循环语句的后半段,以django.contrib.auth.migrations为例:

这里再一次用到了pkgutil模块。通过pkgutil.iter_modules()方法可以找到migration_names路径下的所有迁移文件(过滤掉以~或者_开头的文件)。

load_disk()方法的最后一部分就是遍历找到迁移文件并导入该迁移文件,同时得到该迁移文件中定义的迁移对象,并将该迁移对象记录到对象的disk_migrations属性中:

接着看MigrationLoader对象加载得到的MigrationGraph对象,它是通过调用build_graph()方法得到的。先来看手工测试结果:

这些依赖结果都可以从具体迁移文件的Migration类中得到。下面分别查看上面代码中涉及的三个迁移文件:

前面两个比较好理解:('auth','0001_initial')依赖('contenttypes','0001_initial'),__first__表示的正是第1个迁移文件;('auth','0002_alter_permission_name_max_length')依赖('auth','0001_initial')。最后,('admin','0001_initial')除依赖('contenttypes','0001_initial')外,还依赖migrations.swappable_dependency(settings.AUTH_USER_MODEL)语句的结果。通过全局搜索可知,Django中默认的settings.AUTH_USER_MODEL值如下:

直接在shell命令行中执行如下语句:

从结果可知,('admin','0001_initial')还依赖('auth','0001_initial'),于是就有了前面迁移节点的parents和children属性值。

在上面的build_graph()方法中省略了对迁移类(Migration)中replacements属性的处理。在默认的迁移文件及shell_test应用的迁移类中,并不涉及replacements属性值:

此外,在build_graph()方法的最后调用了MigrationGraph对象中的两个方法:validate_consistency()和ensure_not_cyclic()。前一个方法的实现比较简单,就是检查是否有dummy节点,有则直接抛错;后一个方法的含义是确保迁移图的节点之间不存在循环依赖关系。下面给出一个简单循环关系的示例,最后调用ensure_not_cyclic()方法抛出异常:

在掌握了前面这些基础知识后,就可以正式追踪makemigrations命令和migrate命令了。