Django多数据库历险记(番外)

前情提要

这篇文章基于前两篇文章Django多数据库历险记(一)Django多数据库历险记(二),将继续讲述关于Django多数据库的历险记。
好吧其实这篇文章的内容已经和Django多数据库没有太大关系了……只是为了和前两篇文章的命名保持一致才取了这个标题。

秘技:避免物理外键约束

document.webp

对于数据库层面的物理外键,国内互联网上的声音普遍一致,那就是不推荐使用(比如这个知乎问题),公司的DBA也持这个态度。既然如此,就应该想办法在执行Migrate操作时避免产生物理外键了。
幸运的时,从很早开始(不晚于Django1.8)Django就提供了直接的手段来避免产生物理外键:db_constraint属性。从Django DB模块的源码(schema.py)中可以看出,Django在创建Model、添加/修改Field时都会通过db_constraint属性的值来决定是否加入物理外键约束。实验一下(省略内容见上一篇文章):

1
2
3
4
5
6
7
# app_1/models.py
# ... 略

class ChildModel1(models.Model):
name = models.CharField(max_length=255)
parent1 = models.ForeignKey("Model1", on_delete=models.CASCADE, db_constraint=False)
parent2 = models.ForeignKey("app_2.Model2", on_delete=models.CASCADE, db_constraint=False)
1
2
3
4
5
6
7
8
9
10
11
$ python manage.py makemigrations
Migrations for 'app_1':
app_1/migrations/0002_childmodel1.py
- Create model ChildModel1
$
$ python manage.py migrate app_1 --database db_1
Operations to perform:
Apply all migrations: app_1
Running migrations:
Applying app_2.0001_initial... OK
Applying app_1.0002_childmodel1... OK
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
mariadb root@127.0.0.1:db_1> select * from django_migrations
+----+-------+------------------+----------------------------+
| id | app | name | applied |
+----+-------+------------------+----------------------------+
| 1 | app_1 | 0001_initial | 2020-xx-xx xx:xx:xx.xxxxxx |
| 3 | app_2 | 0001_initial | 2020-xx-xx xx:xx:xx.xxxxxx |
| 3 | app_1 | 0002_childmodel1 | 2020-xx-xx xx:xx:xx.xxxxxx |
+----+-------+------------------+----------------------------+
mariadb root@127.0.0.1:db_1> show tables
+-------------------+
| Tables_in_db_1 |
+-------------------+
| app_1_childmodel1 |
| app_1_model1 |
| django_migrations |
+-------------------+
mariadb root@127.0.0.1:db_1> show create table app_1_model1
CREATE TABLE `app_1_childmodel1` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`parent1_id` int(11) NOT NULL,
`parent2_id` int(11) NOT NULL,
PRIMARY KEY (`id`),
KEY `app_1_childmodel1_parent1_id_8dfbb0b1` (`parent1_id`),
KEY `app_1_childmodel1_parent2_id_db0fdb2e` (`parent2_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci

秘技:migrations转SQL语句

sqlmigrate.webp

在某些特殊场景下(比如处于安全考量),Django自带的migrate命令可能不被允许运行。如果此时还想使用Django的数据迁移功能,则可以使用sqlmigrate命令来将创建好的迁移方案转换为SQL语句。比如:

1
2
3
4
5
6
7
$ python manage.py sqlmigrate app_1 0001_initial --database db_1
BEGIN;
--
-- Create model Model1
--
CREATE TABLE `app_1_model1` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, `name` varchar(255) NOT NULL);
COMMIT;

不过这个命令并一次只能转换一条迁移方案,所以我们可以参考魔改一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# my_sqlmigrate.py
#!/usr/bin/env python
import os

import django
from django.conf import settings
from django.core.management import execute_from_command_line, CommandParser
from django.db import DEFAULT_DB_ALIAS
from django.db.migrations.loader import MigrationLoader

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'multi_db.settings')
django.setup()

parser = CommandParser()
parser.add_argument(
'app_label', nargs='?',
help='App label of an application to synchronize the state.',
)
app_label = parser.parse_args().app_label

loader = MigrationLoader(None) # 获取所有migration
db = settings.DB_ROUTING.get(app_label, DEFAULT_DB_ALIAS)
# 遍历app下所有migration
migrations = sorted(name for app, name in loader.disk_migrations if app == app_label)
for migration in migrations:
argv = ["manage.py", "sqlmigrate", "--database", db, app_label, migration]
print("\n-- python " + " ".join(argv))
execute_from_command_line(argv)
print("-- my_sqlmigrate complete")

现在可以一次性转换所有迁移方案了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ python my_sqlmigrate.py app_1

-- python manage.py sqlmigrate --database db_1 app_1 0001_initial
BEGIN;
--
-- Create model Model1
--
CREATE TABLE `app_1_model1` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, `name` varchar(255) NOT NULL);
COMMIT;

-- python manage.py sqlmigrate --database db_1 app_1 0002_childmodel1
BEGIN;
--
-- Create model ChildModel1
--
CREATE TABLE `app_1_childmodel1` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, `name` varchar(255) NOT NULL, `parent1_id` integer NOT NULL, `parent2_id` integer NOT NULL);
CREATE INDEX `app_1_childmodel1_parent1_id_8dfbb0b1` ON `app_1_childmodel1` (`parent1_id`);
CREATE INDEX `app_1_childmodel1_parent2_id_db0fdb2e` ON `app_1_childmodel1` (`parent2_id`);
COMMIT;
-- my_sqlmigrate complete

总结

文章的实用性可能谈不上有多高,只能说聊以自慰。虽然我在第一篇文章的开头说对多数据库的支持……坑无处不在,但在深入了解之后,我还是被Django完善的架构给折服了:不愧是最优秀的Python Web框架,我还是too young, too simple。


Django多数据库历险记(番外)
https://www.yooo.ltd/2020/05/02/Django多数据库历险记(三)/
作者
OrangeWolf
发布于
2020年5月2日
许可协议