【问题标题】:Django ManyToMany through with multiple databasesDjango ManyToMany 通过多个数据库
【发布时间】:2017-12-14 13:20:21
【问题描述】:

TLTR: Django 不在 SQL 查询中包含数据库名称,我可以强制它这样做还是有解决方法?

长版:

我有两个 legacy MySQL 数据库(注意:我对数据库布局没有影响),我正在为其创建一个只读 API 在 Django 1.11 和 python 3.6 上使用 DRF

我正在使用此处建议的 SpanningForeignKey 字段解决 MyISAM 数据库的引用完整性限制:https://stackoverflow.com/a/32078727/7933618

我正在尝试通过多对多通过 DB1 上的表将 DB1 中的表连接到 DB2 中的表。这就是 Django 正在创建的查询:

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

这当然会给我一个错误“表 'DB2.throughtable' 不存在”,因为 throughtable 在 DB1 上,我不知道如何强制 Django 在表前加上 DB 名称。查询应该是:

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

app1 的模型db1_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')

app2 的模型db2_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 在查询中包含数据库名称吗?或者有什么解决方法吗?

【问题讨论】:

  • 您是否尝试从mysql 客户端运行这样的查询?成功了吗?
  • 是的,SELECT table_b.id FROM DB2.table_b INNER JOIN DB1.throughtable ON (table_b.id = throughtable.b_id) WHERE throughtable.b_id = 12345 有效。正如我所说,问题在于 Django 没有将数据库名称添加到查询中的表中。
  • Django 的跨数据库关系是个难题。许多关于 Django 错误跟踪器的拉取请求或提案已被拒绝,原因很明确,主要是因为不完整。 (缺少其他后端、迁移、测试、对未来问题的恐惧、核心开发的可持续性和复杂性以进行微小改进)我发现一些 SO 问题过于复杂,无法期待任何进展。由于期望仅与遗留数据库的关系,这个问题非常现实。这是一个很好的起点。我奖励 Art 以表彰他对 PostgreSQL 的贡献。这里还没有完美的答案。

标签: python mysql django django-database


【解决方案1】:

Django 1.6+(包括 1.11)的解决方案适用于 MySQLsqlite 后端,通过选项 ForeignKey.db_constraint=False 和显式 Meta.db_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。

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

Sqlite3:它必须在运行时附加到另一个默认的 sqlite3 数据库(答案 SQLite - How do you join tables from different databases),最好是通过信号 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 一起使用。一个优点是,如果数据库之间的表名是唯一的,则不需要“db_table”。

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


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

【讨论】:

  • 我以前见过这个带引号的技巧...看起来很有趣,但我只将其视为最后的手段。官方 Django 文档是否说明了有关“点受保护”的任何信息?而且,为什么需要设置db_constraint=False?我以为 MySQL 支持跨数据库外键...
  • 我使用db_constraint=False以便可以创建表Table1(Throughtable),尤其是字段fk(b_id),通过迁移自动创建,以便可以使用JOIN。然后该字段最终可以由 SQL 修改,并且还必须通过 managed=False 为 Table2 禁用迁移。我的目标是编写一个至少由两个后端支持的解决方案。我有一个带有一些 if/else 条件代码的示例,它也适用于 MySQL 和 sqlite3 的迁移。我试图解决 django.test.TestCase,但是创建测试数据库然后通过相同的运行交叉 db JOIN 确实是一个问题。
  • 你对最后的手段是对的......在什么之前,在什么都不做之前?还有一个更好的解决方案,“一半”引用'db2"."table_b'(更奇怪)。无论如何,模型依赖于“db_table”不仅取决于后端('`'与`“`)而且还取决于DATABASES中配置的低级数据库名称(对于MySQL),这很糟糕。是运气还是运气不好它不是迁移的一部分吗?删除 db_constraint=False 后,迁移将在 Table1 上中断。
  • SQL 可以很容易地修复:./manage.py sqlmigrate app 0001_initial:最后一行:ALTER TABLE "app_table1" ADD CONSTRAINT "app_table1_rel_id_3d909b_fk_db2"_"app_table2_id" FOREIGN KEY ("rel_id") REFERENCES "db2"."app_table2" ("id");(由于格式不成比例,将所有反引号替换为双引号)。如果 ...db2`_`app... 被 ...db2_app... 替换,那么它可以工作。
  • 引用技巧解决了我的问题。它可能不是所有后端的完美解决方案,但对于我的情况(遗留只读 MySQL 数据库)来说,它已经足够好了。谢谢!注意:我必须稍微修改 DB 路由器,以便在 model._meta.db_table 中使用 DB 前缀而不是 app_label(如果指定)。
【解决方案2】:

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

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

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


UPD: 提供有关我在 PostgreSQL 中设置的模式详细信息(稍后应赏金的要求)。我在 MySQL 文档中找不到像 search_path 这样的东西。

快速介绍

PostgreSQL 有Schemas。它们是 MySQL 数据库的同义词。因此,如果您是 MySQL 用户,请想象一下将“schema”一词替换为“database”一词。请求可以在模式之间连接表,创建外键等...每个用户(角色)都有一个search_path

这个变量 [search_path] 指定了搜索模式的顺序 一个对象(表、数据类型、函数等)被一个简单的对象引用 名称未指定架构

特别注意“没有指定模式”,因为这正是 Django 所做的。

示例:旧数据库

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

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

我们需要做的就是:

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

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

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

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

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

【讨论】:

  • 我发现了一些链接(1)(2)(3),并在一个过时的拉取请求#6148 中找到了一个很好的讨论,其中包含指向较新 PR 的链接。一个共同点是它们只解决了对 PostgreSQL 中不同模式的访问,但没有 JOIN 或被拒绝。
  • 您能演示一下您的 Postgres 解决方案吗? (它是DRY吗?如果添加一个新的普通字段你需要做什么?必须通过SQL手动创建或在两个模型中使用?你可以从Django生成必要的SQL吗?)(赏金?)
  • @hynekcer 更新。至于“如果新字段/新模型”问题 - 只要您只对search_path 中首先列出的架构中的表执行迁移,django 只会进行正常迁移,之后 postgres 会解析 django 尝试引用的内容。我这边没有额外的动作。
【解决方案3】:

Django 确实有能力使用多个数据库。见https://docs.djangoproject.com/en/1.11/topics/db/multi-db/

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

【讨论】:

  • 我知道如何组合多个数据库,并且还能够解决 MyISAM 的引用完整性限制。原始 SQL 可能是一种选择,但我想尽可能避免这种情况。这种多对多关系只是 DRF 模型序列化器和查询集相当复杂的组合的一部分,它会使它们大多无用。
猜你喜欢
  • 2014-02-03
  • 2011-01-27
  • 1970-01-01
  • 2017-01-15
  • 2012-06-12
  • 2021-06-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多