Postgresql

約束 - 一個布爾行為真,所有其他行為假

  • September 10, 2021

我有一個專欄:standard BOOLEAN NOT NULL

我想強制一行為真,其他所有為假。取決於此約束,沒有 FK 或其他任何東西。我知道我可以用 plpgsql 完成它,但這似乎是一個大錘。我更喜歡類似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,要麼只設置預設稅率。

變體 1

由於您只需要一個帶有 的列standard = true,因此在所有其他行中將標准設置為 NULL。然後一個普通的UNIQUE約束起作用,因為 NULL 值不違反它:

CREATE TABLE taxrate (
  taxrate int PRIMARY KEY
, standard bool DEFAULT true
, CONSTRAINT standard_true_or_null CHECK (standard) -- that's all
, 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
);

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

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

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

現在總是有一行指向標準(在這個簡單的例子中也直接代表標準費率)。只有超級使用者才能破解它。您也可以使用 trigger 來禁止這種情況BEFORE DELETE

dbfiddle在這裡

有關的:

您可以添加 aVIEW以查看與變體 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該行(如果通常不是該列的情況)。並禁止其刪除。兩者都可以放入觸發器。沒有額外的桌子,但不那麼優雅,也不那麼可靠。

您可以使用過濾索引

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在這裡


但是正如您所說,第一行必須為真,然後您可以使用 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;
編號 | 富
-: | :--
 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在這裡

引用自:https://dba.stackexchange.com/questions/197562