SQLAlchemy中复杂的外键约束

Cam*_*son 9 python sql postgresql database-design sqlalchemy

我有两张桌子,SystemVariablesVariableOptions.SystemVariables应该是不言自明的,并VariableOptions包含所有变量的所有可能选择.

VariableOptions有一个外键,variable_id它指出哪个变量是一个选项.SystemVariables有一个外键,choice_id它指出哪个选项是当前选择的选项.

我已经利用use_alteron choice_idpost_updateon SystemVariables' choice关系绕过了循环关系.但是,我想添加一个额外的数据库约束,以确保它choice_id是有效的(即它指的是一个引用它的选项).

假设sysVar代表SystemVariables表中的一行,我需要的逻辑基本上是:

VariableOptions[sysVar.choice_id].variable_id == sysVar.id
Run Code Online (Sandbox Code Playgroud)

但我不知道如何使用SQL,声明式或任何其他方法构造这种约束.如果有必要,我可以在应用程序级别验证这一点,但如果可能的话,我想在数据库级别进行验证.我正在使用Postgres 9.1.

这可能吗?

Erw*_*ter 11

你可以在没有肮脏技巧的情况下实现.只是延长外键引用所选择的选项包括variable_idchoice_id.

这是一个工作演示.临时表,所以你可以轻松玩它:

CREATE TEMP TABLE systemvariables (
  variable_id integer PRIMARY KEY
, variable    text
, choice_id   integer
);

INSERT INTO systemvariables(variable_id, variable)
VALUES
  (1, 'var1')
, (2, 'var2')
, (3, 'var3');

CREATE TEMP TABLE variableoptions (
  option_id integer PRIMARY KEY
, option text
, variable_id integer REFERENCES systemvariables(variable_id)
                      ON UPDATE CASCADE ON DELETE CASCADE
, UNIQUE (option_id, variable_id) -- needed for the foreign key
);

ALTER TABLE systemvariables
ADD CONSTRAINT systemvariables_choice_id_fk
   FOREIGN KEY (choice_id, variable_id)
   REFERENCES variableoptions(option_id, variable_id);

INSERT INTO variableoptions
VALUES
  (1, 'var1_op1', 1)
, (2, 'var1_op2', 1)
, (3, 'var1_op3', 1)
, (4, 'var2_op1', 2)
, (5, 'var2_op2', 2)
, (6, 'var3_op1', 3);
Run Code Online (Sandbox Code Playgroud)

允许选择关联选项:

UPDATE systemvariables SET choice_id = 2 WHERE variable_id = 1;
UPDATE systemvariables SET choice_id = 5 WHERE variable_id = 2;
UPDATE systemvariables SET choice_id = 6 WHERE variable_id = 3;
Run Code Online (Sandbox Code Playgroud)

但是没有脱节:

UPDATE systemvariables SET choice_id = 7 WHERE variable_id = 3;
UPDATE systemvariables SET choice_id = 4 WHERE variable_id = 1;
Run Code Online (Sandbox Code Playgroud)
ERROR:  insert or update on table "systemvariables" violates foreign key constraint "systemvariables_choice_id_fk"
DETAIL: Key (choice_id,variable_id)=(4,1) is not present in table "variableoptions".
Run Code Online (Sandbox Code Playgroud)

Voilá.正是你想要的.


所有键列NOT NULL

我想我在后面的回答中找到了一个更好的解决方案:

在注释中解决@ ypercube的问题,以避免具有未知关联的条目产生所有关键列NOT NULL,包括外键.

循环依赖通常会使这种情况变得不可能.这是经典的鸡蛋问题:两者中的一个必须先在那里产生另一个.但大自然找到了解决方法,Postgres也是如此:可延迟的外键约束.

CREATE TEMP TABLE systemvariables (
  variable_id integer PRIMARY KEY
, variable    text
, choice_id   integer NOT NULL
);

CREATE TEMP TABLE variableoptions (
  option_id   integer PRIMARY KEY
, option      text
, variable_id integer NOT NULL
     REFERENCES systemvariables(variable_id)
     ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED
, UNIQUE (option_id, variable_id) -- needed for the foreign key
);

ALTER TABLE systemvariables
ADD CONSTRAINT systemvariables_choice_id_fk FOREIGN KEY (choice_id, variable_id)
   REFERENCES variableoptions(option_id, variable_id)
   DEFERRABLE INITIALLY DEFERRED; -- no CASCADING here!
Run Code Online (Sandbox Code Playgroud)

必须在同一事务中插入变量和相关选项:

BEGIN;

INSERT INTO systemvariables (variable_id, variable, choice_id)
VALUES
  (1, 'var1', 2)
, (2, 'var2', 5)
, (3, 'var3', 6);

INSERT INTO variableoptions (option_id, option, variable_id)
VALUES
  (1, 'var1_op1', 1)
, (2, 'var1_op2', 1)
, (3, 'var1_op3', 1)
, (4, 'var2_op1', 2)
, (5, 'var2_op2', 2)
, (6, 'var3_op1', 3);

END;
Run Code Online (Sandbox Code Playgroud)

NOT NULL约束不能被推迟,则立即执行.但是外键约束可以,因为我们这样定义它.在交易结束时检查,这避免了鸡蛋问题.

在此编辑的方案中,两个外键都是延迟的.您可以按任意顺序输入变量和选项.

您可能已经注意到第一个外键约束没有CASCADE修饰符.(允许更改variableoptions.variable_id级联回来是没有意义的.

另一方面,第二外键具有CASCADE修饰符并且仍被定义为可延迟的.这带来了一些限制.手册:

NO ACTION即使约束被声明为可延迟,也不能推迟除检查之外的引用操作.

NO ACTION 是默认值.

因此,参照完整性检查INSERT被推迟,但声明级联操作DELETEUPDATE不.PostgreSQL 9.0或9.1中不允许以下内容,因为在每个语句后强制执行约束:

UPDATE option SET var_id = 4 WHERE var_id = 5;
DELETE FROM var WHERE var_id = 5;
Run Code Online (Sandbox Code Playgroud)

细节:

奇怪的是,同样的事情在PostgreSQL 8.4中有效,而文档声称具有相同的行为.看起来像旧版本中的一个错误 - 即使它看起来有益而不是乍一看有害.必须已针对较新版本进行修复.

  • @Erwin:在这种情况下,是否可以将所有ID定义为"NOT NULL"? (2认同)