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

How to work around Django's lack of composite keys in this complicated case?

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

(对于了解 SQL 但不了解 sqlite 的人来说,下面唯一可能不熟悉的是 sqlite 的“WITHOUT ROWID”子句。默认情况下,sqlite 添加一个隐藏的整数自动递增“rowid”列tables. 使用“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 具有复合键约束,可以使用代理键轻松解决,但 tableABC 具有复杂的键约束,无法直接在 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 as a foreign key in sqlite, where foreign keys must map to declared columns. However, there is a workaround, "INTEGER PRIMARY KEY AUTOINCREMENT" is a special alias in sqlite 将导致命名列引用“rowid”。这就是我们将要尝试的。

Whosebug 上有关 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
);

如上所示table 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 约束,我发现无法在约束检查中引用 tables AB 和 BC。

不过,我不确定 SQL 部分。 我建议您不要使用 A、B 和 C 模型的外键,而是使用 table 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对象是否存在。