Django 自带一个成熟的 ORM,提供了数据库结构迁移的功能。通过两个命令可以很方便地执行表结构更新:
python manage.py makemigrations
生成迁移的 Python 脚本;python manage.py migrate
将脚本转换成 SQL 来执行,并在数据库保存已经执行过的 migrations;
但是这种方便的方法在大公司基本上都没办法直接使用,因为一般是不允许直接让你从笔记本去使用具有 ALTER 权限的数据库用户连接数据库的。有些公司是有在线的平台,只能通过平台提交数据库变更,有些是只能 DBA 来执行数据库变更。
在这种情况下我们就无法使用 Django 自带的 migrations 的功能了。那么有哪些可选的替代方案呢?
第一种是自己手写 SQL 建表,来对应代码的结构声明。之前公司在 Java 中的方案就是这种,有很多弊端。ORM 很多时候都有 length 之类的定义,这种手动对应很容易让两边不同步,没有那边是 source of the truth. 不过好像大家的忍受能力比较高,比较喜欢这种手工工作。
第二种是直接使用声明式编程。意思就是你只要声明你需要什么 table 就可以了。其实 Django 的方案就可以认为是一种声明式的。不过“声明式数据迁移”的本质是语言无关的,你写两张 create table,这个工具就可以产生从 table 1 迁移到 table 2 的 alter SQL. 不过我只听说过,没有见过落地的这种工具。
退一步,有 1st1 的公司 EdgeDB 和 Prisma 实现了一种通过 DSL 来完成的声明式,思想和 Django 是差不多的,声明式的数据库结构迁移。
第三种,就是只适用于 Django 的方案。Django 自带了一个命令,sqlmigrate, 可以从 migrations 的文件生成 SQL 语句。
1 2 3 4 5 |
$ python manage.py sqlmigrate experiment 0037 -- -- Add field comment to action -- ALTER TABLE `actions_tab` ADD COLUMN `comment` longtext NULL; |
但是,还有一个问题是怎么知道哪些 migrations 执行过了,哪些没有执行过。
在原生的 Django 方案中,这个问题是通过在数据库的一张表存储 migration 的文件名来解决的。
1 2 3 4 5 6 7 8 9 10 |
mysql root@localhost:chaoslab_local> select * from django_migrations where app='experiment' limit 5; +----+------------+---------------------------+----------------------------+ | id | app | name | applied | +----+------------+---------------------------+----------------------------+ | 23 | experiment | 0001_initial | 2021-03-11 02:30:10.004294 | | 24 | experiment | 0002_action_action_name | 2021-03-11 02:30:10.089988 | | 25 | experiment | 0003_auto_20210126_0311 | 2021-03-11 02:30:10.250364 | | 26 | experiment | 0004_execution | 2021-03-11 02:30:10.285593 | | 27 | experiment | 0005_execution_experiment | 2021-03-11 02:30:10.343724 | +----+------------+---------------------------+----------------------------+ |
我们也可以通过命令查询。
1 2 3 4 5 6 7 |
$ python manage.py showmigrations experiment experiment [X] 0001_initial [X] 0002_action_action_name [X] 0003_auto_20210126_0311 [X] 0004_execution [X] 0005_execution_experiment |
在每次进行 python manage.py migrate
命令的时候,Django 就去查询数据哪些 migrations 是已经完成了的,然后只执行没有执行的。
由于无法直接连接生产环境的数据库,我们就需要其他的方法来找到没有执行的 migrations.
这里我使用的方法是通过代码来记录:
- 部署的时候,通过和上一次代码的 diff, 就可以找到新生成的 migrations, 执行这些 migrations;
- 每次部署代码,都执行所有没有执行过的 migrations;
这样,migrations 代码就可以作为 source of the truth.
步骤如下:
- 先通过 git 命令找出改动的 migrations 文件;
- 处理文件名,解析成 app migration 的格式;
- 通过
sqlmigrate
命令生成 SQL,以及回滚的 SQL; - 得到 SQL 执行文件,去执行。
这个流程应该在多数的公司都可以行得通了。
使用 Makefile 可以写成以下的脚本:
1 2 3 4 5 6 |
live-migrate-sql: git diff --name-only $(LAST_TAG) > migrate_sql_changed_files.txt rg '(.*)/migrations/(0.*).py' migrate_sql_changed_files.txt -r '$$1 $$2' > migrate_sql_changed_apps_migrations.txt cat migrate_sql_changed_apps_migrations.txt | xargs -t -n2 python manage.py sqlmigrate > database-migrate-from-$(LAST_TAG).sql cat migrate_sql_changed_apps_migrations.txt | xargs -t -n2 python manage.py sqlmigrate --backwards > database-revert-to-$(LAST_TAG).sql rm -rf migrate_sql_*.txt |
执行的效果:
1 2 3 4 5 6 7 8 9 10 11 12 |
$ make live-migrate-sql LAST_TAG=v0.5.6 git diff --name-only v0.5.6 > migrate_sql_changed_files.txt rg '(.*)/migrations/(0.*).py' migrate_sql_changed_files.txt -r '$1 $2' >> migrate_sql_changed_apps_migrations.txt cat migrate_sql_changed_apps_migrations.txt | xargs -t -n2 python manage.py sqlmigrate >> database-migrate-from-v0.5.6.sql python manage.py sqlmigrate experiment 0029_auto_20210414_1431 python manage.py sqlmigrate experiment 0030_auto_20210414_1434 python manage.py sqlmigrate experiment 0031_experimenttarget python manage.py sqlmigrate experiment 0032_auto_20210414_1452 python manage.py sqlmigrate experiment 0033_auto_20210414_1736 python manage.py sqlmigrate experiment 0034_executiongroup_run_by_user python manage.py sqlmigrate experiment 0035_auto_20210418_1547 ... |
生成的数据库文件如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
head database-migrate-from-v0.5.6.sql -n 20 -- -- Alter field experiment on action -- -- -- Alter field experiment on execution -- ALTER TABLE `executons_tab` MODIFY `experiment_id` integer NULL; -- -- Create model ExecutionGroup -- CREATE TABLE `execution_group_tab` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, `created` datetime(6) NOT NULL, `updated` datetime(6) NOT NULL, `experiment_id` integer NOT NULL); -- -- Add field execution_group to execution -- ALTER TABLE `executons_tab` ADD COLUMN `execution_group_id` integer NULL; CREATE INDEX `execution_group_tab_experiment_id_3fc52699` ON `execution_group_tab` (`experiment_id`); CREATE INDEX `executons_tab_execution_group_id_6defed77` ON `executons_tab` (`execution_group_id`); |
几点要注意的事情:
- 以前我习惯通过 migrations 来做数据迁移,但是现在这种形式显然是无法为数据迁移生成 SQL 的。所以数据迁移只能通过 SQL 来做了。不过问题也不大,我写 SQL 的功力已经提高了,大部分的逻辑都可以通过 SQL 写出来;
- 注意 database constrains 的问题。Django 要有办法为 contrains 命名,所以原则上要提供一个数据连接让 Django 知道现在有哪些 contrains 存在了。我们是禁用了外键约束的,如果你用的话,要注意名字的重复问题。
This requires an active database connection, which it will use to resolve constraint names; this means you must generate the SQL against a copy of the database you wish to later apply it on.
好了,到此为止,就可以愉快地使用 Django 了。
有外键的话migration之间的依赖关系也要注意,最好的办法可能还是用一个有权限的测试数据库来记录migrations
是的,也这么想过。
使用一个和生产一样的数据库来存储 django_migrations 的记录,然后每次针对这个库应用 migration。