【问题标题】:Complex foreign key constraint in SQLAlchemySQLAlchemy 中的复杂外键约束
【发布时间】:2012-01-13 17:11:51
【问题描述】:

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

VariableOptions 有一个外键 variable_id,它表明它是哪个变量的选项。 SystemVariables 有一个外键 choice_id,它指明了当前选择的选项。

我已经在choice_id 上使用use_alterSystemVariables 上的post_update 解决了循环关系@'choice 关系。但是,我想添加一个额外的数据库约束,以确保 choice_id 有效(即,它指的是一个选项,该选项是指回它)。

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

VariableOptions[sysVar.choice_id].variable_id == sysVar.id

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

这可能吗?

【问题讨论】:

  • 我认为您应该删除[python] 标签并添加[database-design] 一个。
  • 一旦我使用 SQLAlchemy 完成这项工作,我将发布代码作为答案,因此我将 [python] 标记留给其他 SQLAlchemy 用户。我会改掉[declarative]
  • 对于未来的读者:请参阅Erwin's answer 了解 SQL 解决方案,see my answer 了解使用 SQLALchemy 完成的相同操作。

标签: python sql postgresql database-design sqlalchemy


【解决方案1】:

您可以实现这一点,而无需使用肮脏的技巧。只需扩展外键引用所选选项以包括 variable_idchoice_id

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

CREATE TABLE systemvariables (
  variable_id int PRIMARY KEY
, choice_id   int
, variable    text
);
   
INSERT INTO systemvariables(variable_id, variable) VALUES
  (1, 'var1')
, (2, 'var2')
, (3, 'var3')
;

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

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)
;

允许选择相关选项:

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;

但是没有出格:

UPDATE systemvariables SET choice_id = 7 WHERE variable_id = 3;
UPDATE systemvariables SET choice_id = 4 WHERE variable_id = 1;
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".

正是你想要的。

所有键列NOT NULL

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

寻址@ypercube's question in the comments,以避免具有未知关联的条目使所有键列NOT NULL,包括外键。

循环依赖通常会使这成为不可能。这是经典的 chicken-egg 问题:两者中的一个必须先出现才能产生另一个。但是自然找到了解决方法,Postgres 也是如此:可延迟的外键约束

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

CREATE TABLE variableoptions (
  option_id   int PRIMARY KEY
, option      text
, variable_id int NOT NULL REFERENCES systemvariables
     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!

新的变量和相关选项必须插入到同一个事务中:

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;

NOT NULL 约束不能延迟,它会立即强制执行。但是外键约束可以,因为我们是这样定义的。在交易结束时进行检查,避免了鸡蛋问题。

在这个编辑场景中,两个外键都被延迟。您可以按任意顺序输入变量和选项。
如果您使用 CTE 作为 一个语句 在两个表中输入相关条目,您甚至可以使其与普通的不可延迟 FK 约束一起工作详见the linked answer

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

另一方面,第二个外键有一个CASCADE 修饰符,但定义为DEFERRABLE。这有一些限制。 The manual:

NO ACTION 检查以外的参考操作不能被推迟, 即使约束被声明为可延迟的。

NO ACTION 是默认值。

因此,INSERT 上的引用完整性检查被推迟,但 DELETEUPDATE 上声明的级联操作不会。在 PostgreSQL 9.0 或更高版本中不允许执行以下操作,因为在每条语句之后都会强制执行:

UPDATE option SET var_id = 4 WHERE var_id = 5;
DELETE FROM var WHERE var_id = 5;

详情:

【讨论】:

  • 哦,太棒了,那会很好用!现在我只需要在 SQLAlchemy 中查找复合外键就可以了 :) 谢谢!
  • @Erwin:在这种情况下,所有的id都可以定义为NOT NULL吗?
  • @yppercube:很好的问题。您可以轻松地将variableoptions.variable_id 定义为NOT NULL。强制您始终输入变量之前您可以输入相关选项。您可以也将systemvariables.choice_id 定义为NOT NULL,但这需要额外的措施。请参阅我修改后的答案。
  • 不错!确实,可延迟约束是一个(扭曲的)但有效的解决方案。
  • 这里有很多有用的信息 :) 我已经使用 SQLAlchemy 的 ORM 的声明性方法成功地实现了您的 SQL。我稍后会添加代码作为(又一个)这个问题的答案。
【解决方案2】:

编辑: 0.7.4 版本的 SQLAlchemy(在我开始询问这个问题的同一天发布,7/12/'11!),包含一个新的 autoincrement 主键值这也是外键的一部分,ignore_fk。文档也进行了扩展,包含了我最初尝试完成的一个很好的示例。

现在一切都解释清楚了here

如果您想查看我在上述版本之前提出的代码,请查看此答案的修订历史记录。

【讨论】:

    【解决方案3】:

    我真的不喜欢循环引用。通常有一种方法可以避免它们。这是一种方法:

    SystemVariables 
    ---------------
      variable_id 
      PRIMARY KEY (variable_id)
    
    
    VariableOptions 
    ---------------
      option_id 
      variable_id 
      PRIMARY KEY (option_id)
      UNIQUE KEY (variable_id, option_id) 
      FOREIGN KEY (variable_id) 
        REFERENCES SystemVariables(variable_id)
    
    
    CurrentOptions
    --------------
      variable_id 
      option_id 
      PRIMARY KEY (variable_id)
      FOREIGN KEY (variable_id, option_id)
        REFERENCES VariableOptions(variable_id, option_id)
    

    【讨论】:

    • 循环引用并没有真正困扰我,我宁愿将关系很好地包含在两个表中。无论哪种方式,复合外键都是重要的部分,所以你和 Erwin 的答案都是有效的:)
    • 如果您对将某些列(例如systemvariables.choice_id)定义为NULL 感到满意,那么这两种方法几乎相同。
    • 如果从孩子身上删除更容易,并且信息只通过事务写入,从而防止在某些数据丢失的地方写入,null? dba.stackexchange.com/questions/58949/…
    猜你喜欢
    • 2018-03-05
    • 2011-08-30
    • 1970-01-01
    • 2022-08-22
    • 1970-01-01
    • 2018-02-19
    • 2021-12-04
    • 2022-12-21
    • 1970-01-01
    相关资源
    最近更新 更多