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

问题描述

在以下情况下,我似乎无法弄清楚如何解决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,blah TEXT,id_b,FOREIGN KEY (id_a,id_b) REFERENCES AB(id_a,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"),"b2"),"b3");
INSERT INTO C VALUES (1,"c1"),"c2"),"c3");

INSERT INTO AB VALUES (1,1),(1,2),3);
INSERT INTO BC VALUES (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,3) is not in BC.
-- note that this should fail despite 1,2,3 are unique together
INSERT INTO ABC VALUES (1,'should fail');

尝试使其工作于Django的显而易见的第一步是使用代理键。隐藏的“ rowid”列似乎是很自然的选择,但是这些列cannot be used作为sqlite中的外键,其中外键必须映射到声明的列。但是,有一种解决方法,“ INTEGER PRIMARY KEY AUTOINCREMENT”是special alias in 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_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_c)
    FOREIGN KEY (id_a) REFERENCES A(id_a),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,(NULL,"a3");
INSERT INTO B VALUES (NULL,"b3");
INSERT INTO C VALUES (NULL,"c3");

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

INSERT INTO ABC VALUES (NULL,'should pass');
INSERT INTO ABC VALUES (NULL,'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对象是否首先存在。