Django ManyToMany 通过多个数据库

Django ManyToMany through with multiple databases

TLTR: Django 不 在 SQL 查询中包含数据库名称 ,我可以以某种方式强制它执行此操作还是有解决方法吗?

长版:

我有两个 legacy MySQL databases (注意:我对数据库布局没有影响)我'm 在 Django 1.11 和 python 3.6

上使用 DRF 创建只读 API

我正在使用此处建议的 SpanningForeignKey 字段解决 MyISAM 数据库的引用完整性限制:

我正在尝试通过 DB1 上的 table 通过 ManyToMany 将来自 DB1 的 table 连接到来自 DB2 的 table。这是 Django 正在创建的查询:

SELECT "table_b"."id" FROM "table_b" INNER JOIN "throughtable" ON ("table_b"."id" = "throughtable"."b_id") WHERE "throughtable"."b_id" = 12345

这当然会给我一个错误 "Table 'DB2.throughtable' doesn't exist",因为 throughtable 在 DB1 上,我不知道如何强制 Django 在 table 前面加上数据库名称。查询应该是:

SELECT table_b.id FROM DB2.table_b INNER JOIN DB1.throughtable ON (table_b.id = throughtable.b_id) WHERE throughtable.b_id = 12345

app1db1_app/models.py 的模型:(DB1)

class TableA(models.Model):
    id = models.AutoField(primary_key=True)
    # some other fields
    relations = models.ManyToManyField(TableB, through='Throughtable')

class Throughtable(models.Model):
    id = models.AutoField(primary_key=True)
    a_id = models.ForeignKey(TableA, to_field='id')
    b_id = SpanningForeignKey(TableB, db_constraint=False, to_field='id')

app2db2_app/models.py 的模型:(DB2)

class TableB(models.Model):
    id = models.AutoField(primary_key=True)
    # some other fields

数据库路由器:

def db_for_read(self, model, **hints):
    if model._meta.app_label == 'db1_app':
        return 'DB1'

    if model._meta.app_label == 'db2_app':
        return 'DB2'

    return None

我可以强制 Django 在查询中包含数据库名称吗?或者有什么解决方法吗?

Django 确实具有处理多个数据库的能力。参见 https://docs.djangoproject.com/en/1.11/topics/db/multi-db/

您还可以在 Django 中使用原始 SQL 查询。参见 https://docs.djangoproject.com/en/1.11/topics/db/sql/

我有一个与 PostgreSQL 类似的设置。利用 search_path 在 Django 中实现跨模式引用(postgres 中的模式 = mysql 中的数据库)。不幸的是,似乎 MySQL 没有这样的机制。

不过,您可以试试运气 creating views。在引用其他数据库的一个数据库中创建视图,将其用于 select 数据。我认为这是最好的选择,因为无论如何你都希望你的数据是只读的。

但这不是一个完美的解决方案,执行 raw queries 在某些情况下可能更有用。


更新: 提供有关我使用 PostgreSQL 设置的模式详细信息(根据赏金要求)。我在 MySQL 文档中找不到类似 search_path 的内容。

快速介绍

PostgreSQL 有 Schemas. They are synonymous to MySQL databases. So if you are MySQL user, imaginatively replace word "schema" with word "database". Requests can join tables between schemas, create foreign keys, etc... Each user (role) has a search_path:

This variable [search_path] specifies the order in which schemas are searched when an object (table, data type, function, etc.) is referenced by a simple name with no schema specified.

特别注意 "no schema specified",因为这正是 Django 所做的。

示例:遗留数据库

假设我们有 coupe 遗留模式,并且由于不允许修改它们,我们还需要一个新模式来存储 NM 关系。

  • old1 是第一个遗留模式,它有 old1_table (为了方便起见,这也是模型名称)
  • old2 是第二个遗留模式,它有 old2_table
  • django_schema是新的,会存储需要的NM关系

我们需要做的是:

alter role django_user set search_path = django_schema, old1, old2;

就是这样。是的,就这么简单。 Django 没有在任何地方指定的模式名称 ("databases")。 Django 实际上不知道发生了什么,一切都由 PostgreSQL 在幕后管理。由于 django_schema 在列表中排在第一位,因此将在那里创建新的 table。所以下面的代码 ->

class Throughtable(models.Model):
    a_id = models.ForeignKey('old1_table', ...)
    b_id = models.ForeignKey('old2_table', ...)

-> 将导致创建引用 old1_tableold2_table 的 table throughtable 的迁移。

问题:如果你碰巧有几个同名的table,你要么需要重命名它们,要么仍然欺骗 Django 在table 个名字。

Django 1.6+(包括 1.11)存在 MySQLsqlite 后端,通过选项 ForeignKey.db_constraint=False 和显式 Meta.db_table。如果数据库名称和 table 名称被“`”(对于 MySQL)或“””(对于其他数据库)引用,例如 db_table = '"db2"."table2"')。然后它不再被引用并且点不被引用。有效查询由 Django ORM 编译。一个更好的类似解决方案是 db_table = 'db2"."table2'(这不仅允许加入,而且还更接近一个问题跨数据库约束迁移)

db2_name = settings.DATABASES['db2']['NAME']

class Table1(models.Model):
    fk = models.ForeignKey('Table2', on_delete=models.DO_NOTHING, db_constraint=False)

class Table2(models.Model):
    name = models.CharField(max_length=10)
    ....
    class Meta:    
        db_table = '`%s`.`table2`' % db2_name  # for MySQL
        # db_table = '"db2"."table2"'          # for all other backends
        managed = False

查询集:

>>> qs = Table2.objects.all()
>>> str(qs.query)
'SELECT "DB2"."table2"."id" FROM DB2"."table2"'
>>> qs = Table1.objects.filter(fk__name='B')
>>> str(qs.query)
SELECT "app_table1"."id"
    FROM "app_table1"
    INNER JOIN "db2"."app_table2" ON ( "app_table1"."fk_id" = "db2"."app_table2"."id" )
    WHERE "db2"."app_table2"."b" = 'B'

Django 中的所有数据库后端 支持该查询解析,但是后端必须单独讨论其他必要步骤。我试图更笼统地回答,因为我发现了 similar important question.

迁移需要选项 'db_constraint',因为 Django 无法创建引用完整性约束
ADD foreign key table1(fk_id) REFERENCES db2.table2(id),
但它 can be created manually 对于 MySQL.

针对特定后端的一个问题是,是否可以在 运行 时将另一个数据库连接到默认数据库,以及是否支持跨数据库外键。这些型号也是writable。间接连接的数据库应该用作带有 managed=False 的遗留数据库(因为只有一个 table django_migrations 用于迁移跟踪只在直接连接的数据库中创建。这个 table 应该在同一数据库中仅描述 tables。)但是,如果数据库系统支持此类索引,则可以在托管端自动创建外键索引。

Sqlite3:它必须在 运行 时连接到另一个默认的 sqlite3 数据库(回答 SQLite - How do you join tables from different databases), at best by the signal connection_created:

from django.db.backends.signals import connection_created

def signal_handler(sender, connection, **kwargs):
    if connection.alias == 'default' and connection.vendor == 'sqlite':
        cur = connection.cursor()
        cur.execute("attach '%s' as db2" % db2_name)
        # cur.execute("PRAGMA foreign_keys = ON")  # optional

connection_created.connect(signal_handler)

那么它当然不需要数据库路由器,正常的 django...ForeignKey 可以与 db_constraint=False 一起使用。一个优点是,如果 table 名称在数据库之间是唯一的,则 "db_table" 不是必需的。

MySQLforeign keys between different databases中很容易。 SELECT、INSERT、DELETE 等所有命令都支持任何数据库名称,而无需事先附加它们。


这个问题是关于遗留数据库的。然而,我在迁移方面也有一些有趣的结果。