在这种复杂情况下,如何解决 Django 缺少组合键的问题?

Posted

技术标签:

【中文标题】在这种复杂情况下,如何解决 Django 缺少组合键的问题?【英文标题】:How to work around Django's lack of composite keys in this complicated case? 【发布时间】:2021-02-12 18:09:10 【问题描述】:

对于以下情况,我似乎无法弄清楚如何解决 Django 中缺少复合键的问题。我将使用 SQLite3 方言编写我想要的架构。

(对于了解 SQL 但不了解 sqlite 的人来说,下面唯一可能不熟悉的是 sqlite 的“WITHOUT ROWID”子句。默认情况下,sqlite 在表上添加一个隐藏的整数自动递增“rowid”列。使用“WITHOUT ROWID”命令将其关闭。)

PRAGMA foreign_keys = ON;

CREATE TABLE A (
    id_a INT PRIMARY KEY,
    name_a TEXT 
) WITHOUT ROWID;

CREATE TABLE B (
    id_b INT PRIMARY KEY,
    name_b TEXT
) WITHOUT ROWID;

CREATE TABLE C (
    id_c INT PRIMARY KEY,
    name_c TEXT
) WITHOUT ROWID;

CREATE TABLE AB (
    id_a INT,
    id_b INT,
    PRIMARY KEY (id_a, id_b),
    FOREIGN KEY (id_a) REFERENCES A(id_a),
    FOREIGN KEY (id_b) REFERENCES B(id_b)
) WITHOUT ROWID;

CREATE TABLE BC (
    id_b INT,
    id_c INT,
    PRIMARY KEY (id_b, id_c),
    FOREIGN KEY (id_b) REFERENCES B(id_b),
    FOREIGN KEY (id_c) REFERENCES C(id_c)
) WITHOUT ROWID;

CREATE TABLE ABC (
    id_a INT,
    id_b INT,
    id_c INT,
    blah TEXT,
    PRIMARY KEY (id_a, id_b, id_c),
    FOREIGN KEY (id_a, id_b) REFERENCES AB(id_a, id_b),
    FOREIGN KEY (id_b, id_c) REFERENCES BC(id_b, id_c)
) WITHOUT ROWID;

表 AB 和 BC 具有复合键约束,可以使用代理键轻松解决,但表 ABC 具有复杂的键约束,不能直接在 Django 中实现。

这是一些测试数据

INSERT INTO A VALUES (1, "a1"), (2, "a2"), (3, "a3");
INSERT INTO B VALUES (1, "b1"), (2, "b2"), (3, "b3");
INSERT INTO C VALUES (1, "c1"), (2, "c2"), (3, "c3");

INSERT INTO AB VALUES (1,1), (1,2), (2,1), (2, 3);
INSERT INTO BC VALUES (1,3), (2,1), (3,1);

-- This should work because (1,1) is in AB and (1,3) is in BC.
INSERT INTO ABC VALUES (1,1,3,'should pass');

-- This should fail because although (1,2) is in AB, (2,3) is not in BC.
-- note that this should fail despite 1,2,3 are unique together
INSERT INTO ABC VALUES (1,2,3,'should fail');

尝试让它工作 Django 的明显第一步是使用代理键。隐藏的“rowid”列似乎是一个自然的选择,但这些列cannot be used 作为 sqlite 中的外键,其中外键必须映射到声明的列。但是,有一个解决方法,“INTEGER PRIMARY KEY AUTOINCREMENT”是一个special alias in sqlite,这将导致命名列引用“rowid”。所以这就是我们将尝试的。

*** 上关于 Django 和复合键的类似问题提到使用 NOT NULL 和 UNIQUE 约束,因此我们也可以这样做:

PRAGMA foreign_keys = ON;

CREATE TABLE A (
    id_a INTEGER PRIMARY KEY AUTOINCREMENT,
    name_a TEXT 
);

CREATE TABLE B (
    id_b INTEGER PRIMARY KEY AUTOINCREMENT,
    name_b TEXT
);

CREATE TABLE C (
    id_c INTEGER PRIMARY KEY AUTOINCREMENT,
    name_c TEXT
);

CREATE TABLE AB (
    id_ab INTEGER PRIMARY KEY AUTOINCREMENT,
    id_a INT NOT NULL,
    id_b INT NOT NULL,
    UNIQUE (id_a, id_b)
    FOREIGN KEY (id_a) REFERENCES A(id_a),
    FOREIGN KEY (id_b) REFERENCES B(id_b)
);

CREATE TABLE BC (
    id_bc INTEGER PRIMARY KEY AUTOINCREMENT,
    id_b INT NOT NULL,
    id_c INT NOT NULL,
    UNIQUE (id_b,id_c)
    FOREIGN KEY (id_b) REFERENCES B(id_b),
    FOREIGN KEY (id_c) REFERENCES C(id_c)
);

CREATE TABLE ABC (
    id_abc INTEGER PRIMARY KEY AUTOINCREMENT,
    id_a INT NOT NULL,
    id_b INT NOT NULL,
    id_c INT NOT NULL,
    blah TEXT,
    UNIQUE (id_a, id_b, id_c)
    FOREIGN KEY (id_a) REFERENCES A(id_a),
    FOREIGN KEY (id_b) REFERENCES B(id_b),
    FOREIGN KEY (id_c) REFERENCES C(id_c)
    -- this table is under-constrained compared to the compound foreign key version previously given
);

如表所示,ABC 是欠约束的。下面是相同的测试数据来证明这一点(在插入时使用 NULL 来表示自增列):

INSERT INTO A VALUES (NULL, "a1"), (NULL, "a2"), (NULL, "a3");
INSERT INTO B VALUES (NULL, "b1"), (NULL, "b2"), (NULL, "b3");
INSERT INTO C VALUES (NULL, "c1"), (NULL, "c2"), (NULL, "c3");

INSERT INTO AB VALUES (NULL, 1,1), (NULL, 1,2), (NULL, 2,1), (NULL, 2, 3);
INSERT INTO BC VALUES (NULL, 1,3), (NULL, 2,1), (NULL, 3,1);

INSERT INTO ABC VALUES (NULL,1,1,3,'should pass');
INSERT INTO ABC VALUES (NULL,1,2,3,'should fail'); -- but does not

在插入触发器之前使用的唯一选项是测试否则会漏掉的值吗?使用 Django 约束,我发现无法在约束检查中引用表 AB 和 BC。

【问题讨论】:

【参考方案1】:

不过,我不确定 SQL 部分。 我建议您不要使用 A、B 和 C 模型的外键,而是使用表 AB 和 BC 的外键。

所以你的模型类似于

Class ABC(models.Model):
    ....
    ab = ForeignKey(AB, "insert_other_constraint_here")
    bc = ForeignKey(BC, "insert_other_constraint_here")

但这里的问题是,每次你想创建 ABC 的对象时,你必须先得到 AB 和 BC:

ab = AB.objects.get(a=a,b=b)
bc = BC.objects.get(b=b,b=c)
ABC.objects.create(...,ab=ab,bc=bc)

这样,如果 AB 没有 (a,b) 的组合或 BC 没有 (b,c) 的组合,则会引发错误。

编辑:

但是,这样做会使INSERT INTO ABC VALUES (1,2,3,'should fail'); 不可行,因为您需要 AB 和 BC 而不是 A、B、C 值。如果您仍想使用 A、B、C 的值来创建 ABC:

我猜另一种方法是覆盖 save() 方法。

def save(self, *args, **kwargs):
    ab = AB.objects.filter(a=self.a,b=self.b)
    bc = BC.objects.filter(b=self.b,b=self.c)
    if ab is None or bc is None:
         "Raise error here"
    super(ABC, self).save(*args, **kwargs) 

所以它在创建之前首先检查 AB 和 BC 对象是否存在。

【讨论】:

谢谢,阿尔维安多。我担心你可能是对的,在唯一的手段中的压倒一切的保存。我使用的所有模式都有这样的复杂关系。我不知道没有复合键的 Django 人是如何生活的。我认为这会将我推向 Flash,以便我可以使用 SQLAlchemy。谢谢。周末愉快。 我认为您需要if not ab or not bc 进行逻辑测试。显然使用exists() 方法可以加快测试速度。

以上是关于在这种复杂情况下,如何解决 Django 缺少组合键的问题?的主要内容,如果未能解决你的问题,请参考以下文章

jQuery和Django:在这种情况下如何正确使用jquery

Django model 遇到查询条件组合比较多的情况下怎么写

无法弄清楚这种重复的复杂性

django 在这种情况下如何使用Q和过滤related_name?

芯片设计-如何在缺少 CAD 团队的情况下进行异常日志分析

Django model 遇到查询条件组合比较多的情况下怎么写