Django多数据库实战:Mysql从逻辑隔离到跨库外键问题的解决方案
Django目录文件夹:
一、 背景与目标:为什么需要多数据库?
随着项目规模的扩大,我们经常会遇到这样的需求:将一个大型单体应用(Monolith)按照业务领域进行逻辑拆分。例如,一个“元数据平台”可能包含“元数据中心”、“主数据中心”、“数据质量中心”等多个相对独立的业务模块。
为了实现清晰的职责分离和数据隔离,我们设定了以下架构目标:
- 单一代码库,逻辑隔离:所有功能仍在同一个Django项目中开发,但每个“中心”由一组独立的App构成;
- 数据存储隔离:每个“中心”的数据应存储在各自独立的数据库(在MySQL中)或Schema(在PostgreSQL中)中,便于管理、备份和权限控制;
- 保持ORM便利性:尽管数据被物理隔离,我们仍希望能在Django应用层保持一定程度的关联查询能力,而不是完全退化到原生SQL。
二、 核心挑战:跨数据库的外键约束
在尝试实现这一目标时,我们遇到了一个由数据库本身限制所导致的致命问题:
MySQL(以及大多数关系型数据库)不允许跨物理数据库(Schema)创建外键(FOREIGN KEY)约束。
本身的数据库是meta_puredrf,当我们尝试在 新建的metadata_center 数据库中创建一个引用了 meta_puredrf数据库中 user_user 表的模型时,migrate 命令会失败,并抛出类似以下的错误:
django.db.utils.OperationalError: (1824, "Failed to open the referenced table 'user_user'")
这意味着,我们无法在数据库层面强制维持跨库引用的完整性。
三、 解决方案:数据库路由器 + 逻辑外键
为了解决这个问题,我们采用了一种Django官方推荐的、优雅的组合方案:数据库路由器负责“指路”,逻辑外键负责“解耦”
首先,我们需要让Django知道存在多个数据库。
# settings.pyDATABASES = {'default': { # Django自带应用和用户中心使用的主数据库'ENGINE': 'django.db.backends.mysql','NAME': 'puredrftest',# ...},'metadata_center': { # 为元数据中心创建的专门数据库连接'ENGINE': 'django.db.backends.mysql','NAME': 'metadata_center',# ...},'master_data_center': { # 为未来的主数据中心预留'ENGINE': 'django.db.backends.mysql','NAME': 'master_data_center',# ...}
}
路由器是整个方案的“交通警察”,它告诉Django的ORM,每个模型的读、写、关联和迁移操作应该被路由到哪个数据库。
我们在一个核心应用(如 apps/base_server)下创建一个 db_routers.py 文件。
# 文件路径: apps/base_server/db_routers.pyclass AppBasedRouter:"""一个根据 app_label 将模型路由到不同数据库的路由器。"""# 定义哪些 app 应该被路由到特定的数据库route_app_labels = {'project_management': 'metadata_center','metadata_definations': 'metadata_center','instance_management': 'metadata_center','supplier_management': 'master_data_center', # 示例}def db_for_read(self, model, **hints):"""路由读操作"""return self.route_app_labels.get(model._meta.app_label, 'default')def db_for_write(self, model, **hints):"""路由写操作"""return self.route_app_labels.get(model._meta.app_label, 'default')def allow_relation(self, obj1, obj2, **hints):"""决定是否允许两个对象之间存在关系。为了支持跨库的逻辑外键,我们在这里返回 True。"""return Truedef allow_migrate(self, db, app_label, model_name=None, **hints):"""决定一个 app 的迁移是否应该在某个数据库上运行。"""# 获取该 app 应该在的数据库target_db = self.route_app_labels.get(app_label, 'default')# 只有当目标数据库与当前正在迁移的数据库匹配时,才允许return db == target_db
然后,在 settings.py 中启用这个路由器:
# settings.py
DATABASE_ROUTERS = ['apps.base_server.db_routers.AppBasedRouter']
这是解决跨库外键约束问题的关键。我们需要找到所有跨数据库的 ForeignKey,并告诉Django不要在数据库层面创建物理约束。
示例: 我们的 BaseModel1 引用了 User 模型,而 User 模型在 default 数据库,BaseModel1 的子类(如 MetaType)在 metadata_center 数据库。
# 文件路径: utils/models.pyclass BaseModel1(models.Model):# ...creator = models.ForeignKey(settings.AUTH_USER_MODEL,on_delete=models.SET_NULL,null=True,verbose_name="创建人",db_constraint=False # <-- 关键!)updater = models.ForeignKey(settings.AUTH_USER_MODEL, ..., db_constraint=False)deleter = models.ForeignKey(settings.AUTH_USER_MODEL, ..., db_constraint=False)# ...
db_constraint=False 的作用:
保留逻辑关联: 在您的Django应用内部,instance.creator 仍然是一个有效的外键。您可以执行 instance.creator.username,Django会自动执行一次跨数据库的查询来获取用户信息。
移除物理约束: 在生成数据库迁移时,Django不会为这个字段创建 FOREIGN KEY 约束,从而完美地绕过了MySQL的限制。
由于这是一个重大的架构变更,必须进行一次干净的迁移:
备份所有数据库;
删除所有业务应用 migrations 文件夹下的旧迁移文件;
清空主数据库中的 django_migrations 表;
清空所有业务数据库(如 metadata_center)中的所有表;
运行 python manage.py makemigrations 重新生成所有应用的初始迁移;
运行 python manage.py migrate,Django现在会根据路由器的指示,在正确的数据库中创建所有表;
(可选)编写数据迁移脚本,将备份的数据导回到新的、按Schema划分的表中。
四、 结论
通过多数据库连接、数据库路由器和**db_constraint=False** 这三大法宝的组合,我们成功地构建了一个既能实现数据物理隔离,又能保持应用层逻辑关联的健壮架构。