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



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

(对于了解SQL但不了解sqlite的人来说,下面唯一可能不熟悉的是sqlite的"无ROWID"子句。默认情况下,sqlite会在表上添加一个隐藏的整数自动递增的"ROWID"列。使用"无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";列似乎是一个自然的选择,但这些列不能用作sqlite中的外键,因为在sqlite中,外键必须映射到声明的列。然而,有一种变通方法,";INTEGER PRIMARY KEY AUTOINCREMENT";是sqlite中的一个特殊别名,它将导致命名列引用";rowid";。这就是我们将要尝试的。

StackOverflow上关于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的方法。

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

所以你的模型有点像

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对象是否存在。

最新更新