约束-一个布尔行为true,所有其他行为false


13

我有一列: standard BOOLEAN NOT NULL

我想强制执行True,其余所有都设置为False。根据此约束,没有FK或其他任何内容。我知道我可以用plpgsql完成它,但这似乎像是一把大锤。我更喜欢a CHECKUNIQUE约束。越简单越好。

一行必须为True,不能全部为False(因此,插入的第一行必须为True)。

该行将需要更新,这意味着我必须等待检查约束,直到更新完成,因为所有行都可能先设置为False,然后再设置为True。

products.tax_rate_id和之间有一个FK tax_rate.id,但与默认税率或标准税率无关,用户可以选择默认税率或标准税率来轻松创建新产品。

PostgreSQL 9.5(如果重要)。

背景

该表是税率。税率之一是默认税率(standard因为默认税率是Postgres命令)。添加新产品时,将对产品应用标准税率。如果没有standard,则数据库必须进行猜测或进行各种不必要的检查。我认为,简单的解决方案是确保存在standard

上面的“默认”是指表示层(UI)。有一个用于更改默认税率的用户选项。我或者需要添加额外的检查以确保GUI /用户不会尝试将tax_rate_id设置为NULL,或者只是设置默认税率。


那你有答案吗?
Erwin Brandstetter

是的,我有我的答案,非常感谢@ErwinBrandstetter的输入。我现在倾向于扳机。这是我自己的一个开源项目。当我实际实现它时,我会标记我接受的答案。
theGtknerd

Answers:


15

变体1

由于您所需的只是带有的单列standard = true,因此在所有其他行中将standard设置为NULL。然后一个普通的UNIQUE约束起作用,因为NULL值不违反它:

CREATE TABLE taxrate (
   taxrate int PRIMARY KEY
 , standard bool DEFAULT true
 , CONSTRAINT standard_true_or_null CHECK (standard) -- yes, that's the whole constraint
 , CONSTRAINT standard_only_1_true UNIQUE (standard)
);

DEFAULT是一个可选的提醒,输入的第一行应成为默认行。它没有强制执行任何操作。虽然您不能将设置多行standard = true,但仍然可以将所有行设置为NULL。没有干净的方法来阻止这种情况,仅在单个表中使用约束即可CHECK约束不考虑其他行(没有肮脏的技巧)。

有关:

更新:

BEGIN;
UPDATE taxrate SET standard = NULL WHERE standard;
UPDATE taxrate SET standard = TRUE WHERE taxrate = 2;
COMMIT;

要允许这样的命令(仅在语句末尾满足约束条件):

WITH kingdead AS (
   UPDATE taxrate
   SET standard = NULL
   WHERE standard
   )
UPDATE taxrate
SET standard = TRUE
WHERE taxrate = 1;

.. UNIQUE约束必须是DEFERRABLE。看到:

dbfiddle 在这里

变体2

第二个表,其中有一行,例如:

以超级用户身份创建它:

CREATE TABLE taxrate (
   taxrate int PRIMARY KEY
);

CREATE TABLE taxrate_standard (
   taxrate int PRIMARY KEY REFERENCES taxrate
);

CREATE UNIQUE INDEX taxrate_standard_singleton ON taxrate_standard ((true));  -- singleton

REVOKE DELETE ON TABLE taxrate_standard FROM public;  -- can't delete

INSERT INTO taxrate (taxrate) VALUES (42);
INSERT INTO taxrate_standard (taxrate) VALUES (42);

现在总是有一行指向标准(在这种简单情况下,也直接代表标准汇率)。只有超级用户才能破解它。您也可以通过触发器禁止这样做BEFORE DELETE

dbfiddle 在这里

有关:

您可以添加一个VIEW以与变体1相同:

CREATE VIEW taxrate_combined AS
SELECT t.*, (ts.taxrate = t.taxrate) AS standard
FROM   taxrate t
LEFT   JOIN taxrate_standard ts USING (taxrate);

在您只需要标准费率的查询中,taxrate_standard.taxrate直接使用(仅)。


您后来添加了:

products.tax_rate_id和之间有一个FKtax_rate.id

一个穷人的实现变型2的是只增加一排products指着标准税率(或任何类似的表); 如果您的设置允许,您可以将其称为“标准税率”。

FK约束强制执行参照完整性。要完成此操作,请强制执行tax_rate_id IS NOT NULL该行(如果通常情况下不是该列)。并禁止其删除。两者都可以触发。没有多余的桌子,但不太优雅,也不可靠。


2
强烈建议使用两张表的方法。我还建议向该变体添加示例查询,以便OP可以了解如何CROSS JOIN针对标准,LEFT JOIN针对特定对象,然后COALESCE针对两者。
jpmc26

2
+1,我对多余的表有相同的想法,但是没有时间正确地写出答案。关于第一个表和CONSTRAINT standard_only_1_true UNIQUE (standard):我想表不会很大,所以没关系,但是由于约束将在整个表上定义一个索引,WHERE (standard)使用空间更少的局部唯一索引会不会呢?
ypercubeᵀᴹ

@ypercubeᵀᴹ:是的,整个表上的索引更大,这是此变体的缺点。但是就像您说的那样:这显然是一张很小的桌子,所以没关系。我的目标是只有约束的最简单的标准解决方案。概念证明。就个人而言,我与jpmc26和强烈赞成变种2
欧文Brandstetter修改

9

您可以使用过滤索引

create table test
(
    id int primary key,
    foo bool
);
CREATE UNIQUE INDEX only_one_row_with_column_true_uix 
    ON test (foo) WHERE (foo);  --> where foo is true
insert into test values (1, false);
insert into test values (2, true);
insert into test values (3, false);
insert into test values (4, false);
insert into test values (5, true);
错误:重复的键值违反了唯一约束“ only_one_row_with_column_true_uix”
详细信息:键(foo)=(t)已存在。

dbfiddle 在这里


但是正如您所说,第一行必须为true,然后您可以使用CHECK约束,但是即使使用函数,您也可以稍后删除第一行。

create function check_one_true(new_foo bool)
returns int as
$$
begin
    return 
    (
        select count(*) + (case new_foo when true then 1 else 0 end)
        from test 
        where foo = true
    );
end
$$
language plpgsql stable;
alter table test 
    add constraint ck_one_true check(check_one_true(foo) = 1); 
insert into test values (1, true);
insert into test values (2, false);
insert into test values (3, false);
insert into test values (4, false);
insert into test values (5, true);
错误:关系“ test”的新行违反了检查约束“ ck_one_true”
详细信息:失败行包含(5,t)。

select * from test;
id | 富
-:| :-
 1 | Ť  
 2 | F  
 3 | F  
 4 | F  
delete from test where id = 1;

dbfiddle 在这里


您可以通过添加BEFORE DELETE触发器来解决此问题,以确保从不删除第一行(foo为true)。

create function dont_delete_foo_true()
returns trigger as
$x$
begin
    if old.foo then
        raise exception 'Can''t delete row where foo is true.';
    end if;
    return old;
end;
$x$ language plpgsql;
create trigger trg_test_delete
before delete on test
for each row 
execute procedure dont_delete_foo_true();
delete from test where id = 1;

错误:无法删除foo为true的行。

dbfiddle 在这里

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.