約束 - 一個布爾行為真,所有其他行為假
我有一個專欄:
standard BOOLEAN NOT NULL
我想強制一行為真,其他所有為假。取決於此約束,沒有 FK 或其他任何東西。我知道我可以用 plpgsql 完成它,但這似乎是一個大錘。我更喜歡類似
CHECK
或UNIQUE
約束的東西。越簡單越好。一行必須為 True,它們不能全部為 False(因此插入的第一行必須為 True)。
該行將需要更新,這意味著我必須等待檢查約束,直到更新完成,因為所有行可能首先設置為 False,然後設置一行 True。
products.tax_rate_id
和之間有一個 FKtax_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在這裡
有關的:
您可以添加 a
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
該行(如果通常不是該列的情況)。並禁止其刪除。兩者都可以放入觸發器。沒有額外的桌子,但不那麼優雅,也不那麼可靠。
您可以使用過濾索引
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在這裡