![深入理解Django:框架内幕与实现原理](https://wfqqreader-1252317822.image.myqcloud.com/cover/14/43738014/b_43738014.jpg)
2.4.2 迁移相关的基础类与方法
MigrationRecorder类
在该类中定义了迁移表(django_migrations)的模型类及若干操作该表的方法,如检查该表在数据库中是否存在(has_table())、查询迁移记录(applied_migrations())等。该类的完整实现如下:
![](https://epubservercos.yuewen.com/7EA2B2/23020638109733406/epubprivate/OEBPS/Images/42188_61_1.jpg?sign=1738819187-zoZTHSwWsn4sloLbF2MFq1PAEBfQo1mF-0-35a2a050db8d7ea2e068d3c83a9e78fd)
![](https://epubservercos.yuewen.com/7EA2B2/23020638109733406/epubprivate/OEBPS/Images/42188_62_1.jpg?sign=1738819187-XZV3pEH2CPzEyMlALKfSS3nqzAepAFH1-0-00c90b3c3e335c7ca7f6c7d3d908c9f7)
上面的代码比较简单,主要涉及Django中模型类的简单操作,在第3章中我们将完整解读Django内置的ORM框架代码,厘清这些模型类操作语句背后的逻辑。这里先记住对模型的增初改查操作即可。
在上述源码中,在MigrationRecorder类的内部定义了一个模型类,映射的表名为django_migrations。在初始化方法中必须传入对应数据库的连接信息(connection),才能知道操作的迁移表位于哪个数据库中。接下来定义对该表进行增初改查操作的方法,具体如下:
![](https://epubservercos.yuewen.com/7EA2B2/23020638109733406/epubprivate/OEBPS/Images/42188_62_2.jpg?sign=1738819187-uDDwAovpUmYcrFvcVo6jt8ktLzVI0T6u-0-90a650f73b1f6ab0961ff0743076b2a5)
![](https://epubservercos.yuewen.com/7EA2B2/23020638109733406/epubprivate/OEBPS/Images/42188_63_1.jpg?sign=1738819187-qsAindEp8daLmCsszB1sMxn6Or7GSGhQ-0-d209ef7717c6c8e212c9759b7dcac031)
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)的定义如下:
![](https://epubservercos.yuewen.com/7EA2B2/23020638109733406/epubprivate/OEBPS/Images/42188_64_1.jpg?sign=1738819187-YZJpUd3TaVpmg7IWkAxpwI00VLIEO2s7-0-9975333d02bf8f5f176d30d97d8da7c4)
![](https://epubservercos.yuewen.com/7EA2B2/23020638109733406/epubprivate/OEBPS/Images/42188_65_1.jpg?sign=1738819187-b5AG4ypWRTW1ASf8g578L8TdgwzxklBn-0-58f696ebd69a15f8a395844f597f1d32)
上面关于节点的定义非常简单,值(key)、父辈(parents)和子孙(children)三个参数就代表了一个节点。MigrationGraph类的实现如下:
![](https://epubservercos.yuewen.com/7EA2B2/23020638109733406/epubprivate/OEBPS/Images/42188_65_2.jpg?sign=1738819187-jl7wN6f2JBSBZspoVzs5Ilwu0xc5ucqk-0-56f56108534dd0120cd256982e2a6c7c)
![](https://epubservercos.yuewen.com/7EA2B2/23020638109733406/epubprivate/OEBPS/Images/42188_66_1.jpg?sign=1738819187-01ywXrc70B68XXjtMjKNM3UCh0zP7ALD-0-39fc133e3f8f96dd3dda075a245f3276)
![](https://epubservercos.yuewen.com/7EA2B2/23020638109733406/epubprivate/OEBPS/Images/42188_67_1.jpg?sign=1738819187-GT5vSRpT2TgaMaBruhmyidB2D6jvDFCK-0-60320ace1641bf32a924c6e0088a1ae9)
![](https://epubservercos.yuewen.com/7EA2B2/23020638109733406/epubprivate/OEBPS/Images/42188_68_1.jpg?sign=1738819187-2BjgFQ17CfktoSbzgyKK5HlfgLAIF3k7-0-4fd8e8649e3fb1aeed72c7936ed72b9e)
注意,在MigrationGraph类中,大部分方法均是操作node_map和nodes这两个属性值。接下来我们通过手工构建数据来操作该类。
(1)创建4个Migration对象,它们同属于shell_test应用:
![](https://epubservercos.yuewen.com/7EA2B2/23020638109733406/epubprivate/OEBPS/Images/42188_68_2.jpg?sign=1738819187-kmbJ0JRuppVSccGILAl8GZ5ULvv8kWWt-0-f1bb51a5278be30809e907d787645460)
(2)创建MigrationGraph对象,并将上面创建的Migration对象添加到MigrationGraph对象中:
![](https://epubservercos.yuewen.com/7EA2B2/23020638109733406/epubprivate/OEBPS/Images/42188_68_3.jpg?sign=1738819187-xv6SAmaDZgp4xdPlK47L4LHfYBrGiUiE-0-02671fa205c4d0d1504d6bc35a103cd9)
注意,添加节点方法(add_node())的第1个参数使用了一个二元组,这其实是由在Node类中定义的魔法函数__repr__()决定的。add_node()方法会将第一个参数实例化Node类,而该参数值会赋给实例化后的Node对象的key属性。从Node类的__repr__()方法中可以看到,在输出Node对象时,会用到key属性值的第一个和第二个元素,因此key属性值必须是包含两个元素以上的数组或者元组。Node类中__repr__()方法的源码如下:
![](https://epubservercos.yuewen.com/7EA2B2/23020638109733406/epubprivate/OEBPS/Images/42188_69_1.jpg?sign=1738819187-PCe0oHBCRrpimzTTVN5zIoHSMqGGKILU-0-ca430a2ded458d565a56826ffe0e6615)
(3)使用add_dependency()方法构建依赖关系:
![](https://epubservercos.yuewen.com/7EA2B2/23020638109733406/epubprivate/OEBPS/Images/42188_69_2.jpg?sign=1738819187-KBfFGi1pWSBnqLrF2TENMyIOSPFIEjNm-0-6b5ed29cc3f50205aaec25b0d6f57dae)
(4)调用MigrationGraph对象的root_nodes()方法和leaf_nodes()方法:
![](https://epubservercos.yuewen.com/7EA2B2/23020638109733406/epubprivate/OEBPS/Images/42188_69_3.jpg?sign=1738819187-kYzUgOPe9IuVPNAfBNukVOprMX8aY31H-0-9ecc541a638347b33155e0f6df170bf3)
从结果来看,好像没有初掉仸何节点。下面根据其源码来解释,以root_nodes()为例:
![](https://epubservercos.yuewen.com/7EA2B2/23020638109733406/epubprivate/OEBPS/Images/42188_69_4.jpg?sign=1738819187-29UbdHAPQh9bdNm99Bn8oVnRD2yuW2ts-0-421c8edb36c9e9d6c0e1040039f150fe)
![](https://epubservercos.yuewen.com/7EA2B2/23020638109733406/epubprivate/OEBPS/Images/42188_70_1.jpg?sign=1738819187-4YP9rsPjL3lzYlyaKT7k3j2WfwvwKX3Z-0-7f76a398b52b41e3c1e7770d5b71d036)
可以看到,root_nodes()方法是遍历所有的node并对其进行刞断,把符合根节点条件的加入roots列表中,最后返回排序后的roots列表。因此,刞断是否为root节点的核心就在上面代码的if刞断中:
![](https://epubservercos.yuewen.com/7EA2B2/23020638109733406/epubprivate/OEBPS/Images/42188_70_2.jpg?sign=1738819187-c7zyhdu3ERFdbsH7W83N0ayuuIJTpDy4-0-7751cfcb5351c2c24372285dfbba4cba)
if刞断可以拆成2个条件组合。条件2需要输入app参数,确保查找的root节点是本应用内的节点。由于这里没有传入app参数,所以条件2直接为True。对于条件1,node表示当前搜索节点,而key表示该节点中的一个父节点,都是Node对象。而node[0]和key[0]的含义由Node类中的魔法函数__getitem__()决定:
![](https://epubservercos.yuewen.com/7EA2B2/23020638109733406/epubprivate/OEBPS/Images/42188_70_3.jpg?sign=1738819187-rrhGK5viflA4sKaDXXpGz0gzGFGrprjL-0-57e1f06cbe44a1a285ec87c943a19d43)
由代码可知,node[0]的值最终为Node对象中key属性的第1个元素,请看下面的操作示例:
![](https://epubservercos.yuewen.com/7EA2B2/23020638109733406/epubprivate/OEBPS/Images/42188_70_4.jpg?sign=1738819187-HE8nkHwH3fBeN7JvrJYqZc1OhqnUqoVC-0-408ed2755a7081c1dbaff057eddf8c09)
接着分析条件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参数如下:
![](https://epubservercos.yuewen.com/7EA2B2/23020638109733406/epubprivate/OEBPS/Images/42188_71_1.jpg?sign=1738819187-8ELfQXVqvZFe2YGqbQpFotMrswoVOnJF-0-3561c1d036dfcb81b7795e37ff6f761c)
从上面的代码可以看到,给MigrationGraph对象添加节点的key其实是应用名。因此,上面的root_nodes()方法和leaf_nodes()方法获取的是同一个应用中没有依赖的节点。在清楚了上面现象的起因后,再换另一个MigrationGraph对象进行测试:
![](https://epubservercos.yuewen.com/7EA2B2/23020638109733406/epubprivate/OEBPS/Images/42188_72_1.jpg?sign=1738819187-IRevMwbwn5nSbOjrEmDVq47UuklCDorL-0-c4d9d12bb0356cdf2fa025b01507f16c)
这时再调用root_nodes()方法和leaf_nodes()方法,能否得到想要的结果?接下来介绍两个稍微复杂的方法,即remove_replaced_nodes()方法和remove_replacement_node()方法:
![](https://epubservercos.yuewen.com/7EA2B2/23020638109733406/epubprivate/OEBPS/Images/42188_72_2.jpg?sign=1738819187-LHr5z9TaHiaZZynT2gvfzwAL27zV9Pww-0-ed938c475e77c463a9d07bababcbe9d0)
可以看到,在调用MigrationGraph对象的remove_replaced_nodes(self,replacement,replaced)方法后,replaced中的节点将全部被移除,而其包含的父节点及子孙节点都将被转移到replacement节点上,该逻辑可以直接从源码中分析得到。而remove_replacement_node(self,replacement,replaced)方法则是上一个方法的反过程,它会移除所有节点中与replacement节点有关的信息,然后将其子节点(注意,看源码没有处理replacement节点的父节点信息)重新添加到replaced节点集合中。为了更好地演示这个方法,下面新建一个MigrationGraph对象并添加节点及其依赖:
![](https://epubservercos.yuewen.com/7EA2B2/23020638109733406/epubprivate/OEBPS/Images/42188_72_3.jpg?sign=1738819187-TDZoCbgnx1kbo0nesnPQvCdle4m2T5n4-0-afe47076ff7216a2f07599fa326f8dd4)
![](https://epubservercos.yuewen.com/7EA2B2/23020638109733406/epubprivate/OEBPS/Images/42188_73_1.jpg?sign=1738819187-DqymToWyiEUJ4YGHhem1oez8mFNi2nAh-0-e1ccb4ee169d099655eaf23e54f44db3)
结合示例及源码分析可知,remove_replacement_node(self,replacement,replaced)方法的执行逻辑是:移除MigrationGraph类中所有与replacement节点有关的信息,同时将所有涉及replacement节点的地方全部重新设置为replaced节点。
MigrationLoader类
MigrationLoader类的源码实现如下:
![](https://epubservercos.yuewen.com/7EA2B2/23020638109733406/epubprivate/OEBPS/Images/42188_73_2.jpg?sign=1738819187-2ZY27FnVru94R7mSiHbRO42iYm9n9gFi-0-976a0e6625dc608d1a3f30b86bf4c569)
![](https://epubservercos.yuewen.com/7EA2B2/23020638109733406/epubprivate/OEBPS/Images/42188_74_1.jpg?sign=1738819187-FHJMmsDc8Cho0JeRxCQPA8tfRSPAUzjt-0-5ce8843c19e79d5bd234e26d2d673858)
![](https://epubservercos.yuewen.com/7EA2B2/23020638109733406/epubprivate/OEBPS/Images/42188_75_1.jpg?sign=1738819187-8XA1IihkZtYbRyTqXqfGVgbV1E1DaL1T-0-129cc9ee2ac7acf626dae2e3b64e562c)
在MigrationLoader类的初始化方法中会调用build_graph()方法(load=True)去构造所有迁移文件的关联图,这一步非常重要。在build_graph()方法中,会在一开始就调用load_disk()方法来加载本地的迁移文件并更新到属性disk_migrations中。下面通过测试来看看这些方法的输出结果:
![](https://epubservercos.yuewen.com/7EA2B2/23020638109733406/epubprivate/OEBPS/Images/42188_75_2.jpg?sign=1738819187-bgVNAbtSp5hJ6PtYsQWf1kxRUKAmoKTl-0-c89e30b9e0c468efa0e03b5392c9ade6)
![](https://epubservercos.yuewen.com/7EA2B2/23020638109733406/epubprivate/OEBPS/Images/42188_76_1.jpg?sign=1738819187-Dc4AUyVlfKKq9ZE8sgF1U2e9FKRJdhW8-0-2427878069bdf9e453b01ed1a9ad9ace)
load_disk()方法的源码实现如下:
![](https://epubservercos.yuewen.com/7EA2B2/23020638109733406/epubprivate/OEBPS/Images/42188_76_2.jpg?sign=1738819187-TcvIjyEeGBmqFUeJKtnMLEkeo6lKksIN-0-c7dba367b63419d063a64f907f8aeddf)
![](https://epubservercos.yuewen.com/7EA2B2/23020638109733406/epubprivate/OEBPS/Images/42188_77_1.jpg?sign=1738819187-kqUShioTc9Qj1kIuDVMPCx5JpBdma1Yb-0-31d7b9f1e0d6cba54d56e81a1d4d70a3)
下面对load_disk()方法进行拆解,先厘清第1个for循环语句的含义,操作示例如下:
![](https://epubservercos.yuewen.com/7EA2B2/23020638109733406/epubprivate/OEBPS/Images/42188_77_2.jpg?sign=1738819187-aC8Z9lNpfRIMDQxIwIkaf0BthAvbnqkx-0-b333b202a536451839ab0ecdba342691)
从上面的代码中不难看出,for循环中的module_name其实就是应用的迁移模块路径。从这里也可以知道Django框架中各应用的默认的迁移文件位置。以auth应用为例,其默认的迁移文件位置如图2-3所示。
![](https://epubservercos.yuewen.com/7EA2B2/23020638109733406/epubprivate/OEBPS/Images/42188_78_1.jpg?sign=1738819187-XsdCMux1FALlUXTR7716xmS9Z9w9xnKM-0-1f05c893879c6682b6bc3f6e4f382412)
图2-3
继续执行load_disk()方法中for循环语句的后半段,以django.contrib.auth.migrations为例:
![](https://epubservercos.yuewen.com/7EA2B2/23020638109733406/epubprivate/OEBPS/Images/42188_78_2.jpg?sign=1738819187-umWo7QgkGXVsreoAEH1N61P1bnLsW1jd-0-64069d77ead9015f868a849d701cb633)
这里再一次用到了pkgutil模块。通过pkgutil.iter_modules()方法可以找到migration_names路径下的所有迁移文件(过滤掉以~或者_开头的文件)。
load_disk()方法的最后一部分就是遍历找到迁移文件并导入该迁移文件,同时得到该迁移文件中定义的迁移对象,并将该迁移对象记录到对象的disk_migrations属性中:
![](https://epubservercos.yuewen.com/7EA2B2/23020638109733406/epubprivate/OEBPS/Images/42188_79_1.jpg?sign=1738819187-RcCQPoFmj7RyssfnmpxDV4nbZyzFvizF-0-112043ae670e2ae3fb36f56769f4f61b)
接着看MigrationLoader对象加载得到的MigrationGraph对象,它是通过调用build_graph()方法得到的。先来看手工测试结果:
![](https://epubservercos.yuewen.com/7EA2B2/23020638109733406/epubprivate/OEBPS/Images/42188_79_2.jpg?sign=1738819187-PVxqNrwYH12WOgT3lSR6ZD8OVKQ7zd6i-0-d4d6239a4d9aab4538d2dd20cc16af37)
这些依赖结果都可以从具体迁移文件的Migration类中得到。下面分别查看上面代码中涉及的三个迁移文件:
![](https://epubservercos.yuewen.com/7EA2B2/23020638109733406/epubprivate/OEBPS/Images/42188_79_3.jpg?sign=1738819187-TIAQ49rbJThwreng5RuYDWd2oL8ZaPUS-0-bd9c389b6c9dee3419d2b7751893b48a)
![](https://epubservercos.yuewen.com/7EA2B2/23020638109733406/epubprivate/OEBPS/Images/42188_80_1.jpg?sign=1738819187-p062xlSDLTmbWiPSGzsgJi7czlKdYk4Q-0-84620f023053f42d0f705a0735670159)
前面两个比较好理解:('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值如下:
![](https://epubservercos.yuewen.com/7EA2B2/23020638109733406/epubprivate/OEBPS/Images/42188_80_2.jpg?sign=1738819187-3agHpECvz0oNC5FkosOMU9yOK9NHv08b-0-f2b90d889dd223be7cae62252a128689)
直接在shell命令行中执行如下语句:
![](https://epubservercos.yuewen.com/7EA2B2/23020638109733406/epubprivate/OEBPS/Images/42188_80_3.jpg?sign=1738819187-pVwNHs15Q7FNElmxEVumemFq5DsE5mFw-0-5c9e33906e10c939ff479318067ee70f)
从结果可知,('admin','0001_initial')还依赖('auth','0001_initial'),于是就有了前面迁移节点的parents和children属性值。
![](https://epubservercos.yuewen.com/7EA2B2/23020638109733406/epubprivate/OEBPS/Images/42188_80_4.jpg?sign=1738819187-0HxbGRGTw9iTYpYnXBimtGfifXLusf05-0-1245a0249fe0e800135ffe07c81c57ba)
![](https://epubservercos.yuewen.com/7EA2B2/23020638109733406/epubprivate/OEBPS/Images/42188_81_1.jpg?sign=1738819187-zrPf4as9m1K9wByMo1FGrbgCNBTTGHOD-0-cd37afd515f221db5fc5be86b2401e8b)
![](https://epubservercos.yuewen.com/7EA2B2/23020638109733406/epubprivate/OEBPS/Images/42188_82_1.jpg?sign=1738819187-ENy5Kaf5PbIV9T2r5mBLQPHWPR6Vt2MA-0-c4c3700e76a4aceebc358c94f1bf4dca)
在上面的build_graph()方法中省略了对迁移类(Migration)中replacements属性的处理。在默认的迁移文件及shell_test应用的迁移类中,并不涉及replacements属性值:
![](https://epubservercos.yuewen.com/7EA2B2/23020638109733406/epubprivate/OEBPS/Images/42188_82_2.jpg?sign=1738819187-vXBNVg2VPUkS6euzA85pBI1B5ZCZyQzN-0-1d023b6ff6358eb5ebc0863cf20690b2)
此外,在build_graph()方法的最后调用了MigrationGraph对象中的两个方法:validate_consistency()和ensure_not_cyclic()。前一个方法的实现比较简单,就是检查是否有dummy节点,有则直接抛错;后一个方法的含义是确保迁移图的节点之间不存在循环依赖关系。下面给出一个简单循环关系的示例,最后调用ensure_not_cyclic()方法抛出异常:
![](https://epubservercos.yuewen.com/7EA2B2/23020638109733406/epubprivate/OEBPS/Images/42188_82_3.jpg?sign=1738819187-b2IyhcAOgNLnp1xFoeTBu5RCxCwfT2UO-0-e9da6da6170787121276114fb26b739f)
![](https://epubservercos.yuewen.com/7EA2B2/23020638109733406/epubprivate/OEBPS/Images/42188_83_1.jpg?sign=1738819187-uBBeRlncqyfttUUxIvA3cBiVce0oB20G-0-390ee8928cc8834cfe2ad6bb5410d875)
在掌握了前面这些基础知识后,就可以正式追踪makemigrations命令和migrate命令了。