【问题标题】:Deferrable check constraint in PostgreSQLPostgreSQL 中的可延迟检查约束
【发布时间】:2013-04-25 18:01:51
【问题描述】:

我有如下功能检查强制参与:

CREATE FUNCTION member_in_has_address()
RETURNS BOOLEAN AS $$
BEGIN
RETURN EXISTS (SELECT *
       FROM address a, member_details b
       WHERE b.member_id = a.member_id);
END;
$$  LANGUAGE plpgsql;

然后从 CHECK 约束中调用

ALTER TABLE member_details
 ADD CONSTRAINT member_in_has_address_check
  CHECK (member_in_has_address());

要在标准 SQL 中创建可延迟约束,可以:

ALTER TABLE member_details
 ADD CONSTRAINT member_in_has_address_check
  INITIALLY DEFERRED
  CHECK (member_in_has_address()); 

如何在 PostgreSQL 中做同样的事情?

【问题讨论】:

  • 任何个成员有地址时,您当前的member_in_has_address() 将返回true。它不会检查特定成员是否有地址。
  • 谢谢伊戈尔,但我的主要问题是如何推迟这种约束,直到孩子 (address) 更新。插入如下:插入父级member_details,然后插入具有前键的addressmember_details 强制参与 address
  • 在下面查看我的答案。答案 - 创建一个延迟外键。
  • 是的,你说得对,我刚刚加入了这两个表。

标签: sql postgresql deferred deferrable-constraint


【解决方案1】:

2 种经过测试的方法。

1.改变最初延迟的约束。

begin;

alter TABLE t1 alter CONSTRAINT t1_fkey deferrable INITIALLY DEFERRED;

delete from t1;

-- insert into t1 (...)

alter TABLE t1 alter CONSTRAINT t1_fkey not deferrable;

-- commit;
rollback;

2.SET CONSTRAINTS ALL DEFERRED。

begin;

alter TABLE t1 alter CONSTRAINT t1_fkey deferrable initially immediate;
SET CONSTRAINTS t1_fkey DEFERRED
-- SET CONSTRAINTS ALL DEFERRED;  -- or, do this.

delete from t1;

-- insert into t1 (...)

alter TABLE t1 alter CONSTRAINT t1_fkey not deferrable;

-- commit;
rollback;

【讨论】:

    【解决方案2】:

    您可以像在其他 RDBMS 中一样在 PostgreSQL 中延迟约束,但对于当前版本 (9.2),您只能延迟 UNIQUE、PRIMARY KEY、EXCLUDE 和 REFERENCES。摘自手册的this page

    DEFERRABLE
    NOT DEFERRABLE

    这控制约束是否可以 延期。将检查不可延迟的约束 在每个命令之后立即。检查约束是 deferable 可以推迟到事务结束(使用 SET CONSTRAINTS 命令)。 NOT DEFERRABLE 是默认值。 目前,只有 UNIQUE、PRIMARY KEY、EXCLUDE 和 REFERENCES(外 key) 约束接受此子句。 NOT NULL 和 CHECK 约束 不可延期。

    INITIALLY IMMEDIATE
    INITIALLY DEFERRED

    如果约束是可延迟的, 此子句指定检查约束的默认时间。如果 约束是 INITIALLY IMMEDIATE,它在每个语句之后检查。 这是默认设置。如果约束是 INITIALLY DEFERRED,它是 仅在交易结束时检查。约束检查时间 可以使用 SET CONSTRAINTS 命令更改。

    如果每个成员都有一个地址,您可以创建一个从 member_detailsaddress 的简单延迟外键,而不是您当前的约束来检查。

    更新:您需要创建 2 个外键。从address(member_id)member_details(member_id) 的一个普通的。另一个 - 从 member_details(member_id) 推迟到 address(member_id)

    使用这两个外键,您将能够:

    1. member_details中创建一个成员。
    2. address 中为步骤 1 中的成员创建一个地址
    3. 提交(没有错误)

    1. member_details中创建一个成员。
    2. 提交(并从延迟的外键中获取错误)。

    【讨论】:

    • 谢谢。所以这意味着我可以在address 中延迟一个外键,然后先插入address,然后再插入member_details
    • 但是还有一个问题,member_details 有自动递增的主键,所以我先插入member_details,然后检索它的主键,然后再插入address
    • @RadovanLuptak 只需创建 2 个外键,一个 member_details(member_id) -> address(member_id),第二个 address(member_id) -> member_details(member_id)
    • 我对此很陌生,你能告诉我有什么好处吗?我只怀疑当我将两个外键都强制为NOT NULL 时,我可以实现双方的强制参与,并且通过使外键引用address address_id UNIQUE 我可以实现一对一的参与。
    • @RadovanLuptak 没错。一对 FK 可用于强制执行 1..many1..many 关系。使用UNIQUE 进一步限制一侧,您可以强制11..many 关系。并且无需引用address_id。两个键都可以在member_id
    【解决方案3】:

    这是我想出来的。

    ALTER TABLE address
    ADD CONSTRAINT address_member_in_has_address
    FOREIGN KEY (member_id) REFERENCES member_details(member_id)
    ON DELETE CASCADE
    DEFERRABLE INITIALLY DEFERRED;
    
    CREATE FUNCTION member_in_has_address() RETURNS trigger AS $BODY$
        BEGIN
        IF NOT EXISTS(SELECT * 
                       FROM member_details
                       WHERE member_id IN (SELECT member_id 
                                            FROM address)) 
        THEN
                RAISE EXCEPTION 'Error: member does not have address';
            END IF;
        RETURN NEW;
        END;
    $BODY$ LANGUAGE plpgsql;
    
    CREATE CONSTRAINT TRIGGER manatory_participation_member_details_ins
     AFTER INSERT ON member_details 
     DEFERRABLE INITIALLY DEFERRED 
     FOR EACH ROW  
     EXECUTE PROCEDURE member_in_has_address();
    
    CREATE CONSTRAINT TRIGGER manatory_participation_member_details_del
     AFTER INSERT ON member_details 
     DEFERRABLE INITIALLY DEFERRED 
     FOR EACH ROW 
     EXECUTE PROCEDURE member_in_has_address();
    

    我在没有触发器的情况下在两个表中使用外键尝试了 Igor 的版本。在这种情况下,此约束不会被延迟。

    ALTER TABLE member_details
    ADD CONSTRAINT member_details_in_has_address
    FOREIGN KEY (address_id) REFERENCES address
    ON UPDATE NO ACTION ON DELETE CASCADE
    DEFERRABLE INITIALLY DEFERRED;
    

    我明白了:错误:“address_id”列中的空值违反了非空约束

    使用此匿名块插入时:

    DO $$ 
    DECLARE 
     mem BIGINT;
    BEGIN
    INSERT INTO member_details (member_first_name, member_last_name, member_dob, member_phone_no, 
    member_email, member_gender, industry_position, account_type, music_interests)
    VALUES ('Rado','Luptak','07/09/80','07540962233','truba@azet.sk','M','DJ','basic','hard core');
    
    SELECT member_id 
     INTO mem
    FROM member_details
    WHERE member_first_name = 'Rado' AND member_last_name = 'Luptak'
    AND member_dob = '07/09/76';
    
    INSERT INTO address (address_id, house_name_no, post_code, street_name, town, country, member_id)
    VALUES (mem, '243', 'E17 3TT','Wood Road','London', 'UK', mem);
    
    UPDATE member_details
     SET  address_id = mem WHERE member_id = mem;
    END
    $$;
    

    使用地址表(Igor 版本)的 address_id 强制参与 member_details 的另一个问题是,这允许我将行插入 member_details 并引用现有地址行,但现有地址行引用不同的 member_details 行。当后面的 member_details 行被删除时,它会级联并删除地址行,地址行可以或不能删除(取决于设置)新插入的 member_details 行。当加入 member_id 和 address_id 时,它也会返回不同的详细信息。因此,它需要另一个约束,所以我一直使用触发器并在插入之前删除它并在插入之后重新创建它,因为触发器没有被延迟。

    【讨论】:

    • 正如我之前所说,函数member_in_has_address() 将在任何成员有地址时返回true。它不会检查特定成员是否有地址。
    【解决方案4】:

    将您的查询包装在事务中,然后在需要至少一个地址时使用延迟外键和延迟约束触发器:

    CREATE CONSTRAINT TRIGGER member_details_address_check_ins
      AFTER INSERT ON member_details
    DEFERRABLE INITIALLY DEFERRED
    FOR EACH ROW
    EXECUTE PROCEDURE member_details_address_check_ins();
    
    ALTER TABLE address
    ADD CONSTRAINT address_member_details_member_id_fkey
    FOREIGN KEY (member_id) REFERENCES member_details(member_id)
    ON UPDATE NO ACTION ON DELETE NO ACTION
    DEFERRABLE INITIALLY DEFERRED;
    
    CREATE CONSTRAINT TRIGGER address_member_details_check_del
      AFTER DELETE ON address
    DEFERRABLE INITIALLY DEFERRED
    FOR EACH ROW
    EXECUTE PROCEDURE address_member_details_check_del();
    
    -- also consider the update cases for the inevitable merge of duplicate members.
    

    在单独的注释中,规范化且美观,但将地址和联系方式(例如电子邮件)放在单独的地址表中偶尔会引入非常丰富多彩的 UI/UX 问题。例如。一位未经培训的秘书更改了公司和她老板在 A 公司的所有联系人的地址,其中一个人转到了 B 公司。是的,当 UI 的行为与 Outlook 不同时,它确实发生过……

    无论如何,我发现将这些东西存储在与联系人相同的表中通常更方便,即 address1、address2、email1、email2 等。它使其他各种事情变得更简单。原因 - 即像您正在调查的那样运行检查。在极少数情况下,您想要存储两个以上这样的信息,实际上根本不值得这么麻烦。

    【讨论】:

    • 谢谢,我完全忘记了插入和删除触发器以强制参与。我已经在 Sybase 中完成了。
    • 顺便说一句,今天有一个相关问题,我最终扩展了我的结论:dba.stackexchange.com/a/41430/1860
    • 不能同意您将 1:n 联系人类型信息(如地址、邮件、电话...)更普遍地与联系人本身“加入”的观点。在不关注这一点的应用程序中,这可能没问题,但只要经常或长时间可靠灵活地管理联系人更相关,使用 1:n 关系和单独的表来处理它可能会更根据我的经验,很容易设计和维护。在这种情况下,Outlook 是否不提供 IMO 最佳实践方法,因为它只有一个非常基本的数据模型,不适合更复杂的联系人管理。
    • @AndreasDietrich:是的,不使用 1-n 关系也困扰着我。但在实践中,它也引入了非常丰富多彩的 UI/UX 问题,导致不正确的数据到处都是。我确实看到一位秘书重命名了一个联系人的组织,因此更改被错误地应用于其他几十个联系人 - 加上一个 dup org 条目。它还引入了工程(和性能)问题,在我看来,这些问题根本不值得在小公司花费任何时间。
    • 所以“较小的公司”与“不再关注 CRM 等以及相当糟糕的程序员;)”对我来说听起来很合理。否则,直接索引应该没有性能处理。您描述的问题似乎源于与此问题本身似乎无关的应用程序错误。糟糕的编程或错误的软件(不是?)不应成为不应用合理模型、良好技术或最佳实践的借口(我不是指“金杯柄”)。经常让事情变得困难的是我们的一次性+廉价社会的哲学
    猜你喜欢
    • 2020-04-19
    • 2010-11-03
    • 2010-11-03
    • 1970-01-01
    • 2012-04-28
    • 2016-11-13
    • 2014-03-05
    • 1970-01-01
    • 2015-04-04
    相关资源
    最近更新 更多