web-dev-qa-db-ja.com

制約-1つのブール行はtrue、他のすべての行はfalse

次の列があります:standard BOOLEAN NOT NULL

1つの行をTrueに、その他の行をすべてFalseに強制したいと思います。この制約に応じて、FKやその他のものはありません。私はplpgsqlでそれを達成できることを知っていますが、これは大槌のようです。 CHECKまたはUNIQUE制約のようなものを好みます。シンプルなほど良い。

1つの行はTrueでなければなりませんが、すべてFalseにすることはできません(したがって、最初に挿入された行はTrueである必要があります)。

行を更新する必要があります。つまり、すべての行が最初にFalseに設定され、その後に1つの行がTrueに設定される可能性があるため、更新が完了するまで制約を確認するのを待つ必要があります。

products.tax_rate_idtax_rate.idの間にはFKがありますが、デフォルトまたは標準の税率とは何の関係もありません。これは、ユーザーが新しい製品を簡単に作成できるように選択できます。

重要な場合はPostgreSQL 9.5。

バックグラウンド

表は税率です。税率の1つがデフォルトです(デフォルトはPostgresコマンドであるため、standard)。新しい商品が追加されると、標準の税率が商品に適用されます。 standardがない場合、データベースは推測またはあらゆる種類の不要なチェックを実行する必要があります。単純な解決策は、standardがあることを確認することだと私は思った。

上記の「デフォルト」とは、プレゼンテーション層(UI)を意味します。デフォルトの税率を変更するためのユーザーオプションがあります。 GUI /ユーザーがtax_rate_idをNULLに設定しないようにするために追加のチェックを追加するか、デフォルトの税率を設定する必要があります。

13
theGtknerd

バリアント1

必要なのはstandard = trueを含む単一の列だけなので、他のすべての行では標準をNULLに設定します。次に、NULL値はそれに違反しないため、単純なUNIQUE制約が機能します。

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は、入力した最初の行がデフォルトになることを示すオプションのリマインダーです。 enforcing何もしていません。複数の行をstandard = trueに設定することはできませんが、すべての行をNULLに設定することはできます。単一のテーブルにconstraintsのみを使用してこれを防ぐための明確な方法はありません。 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

次のような単一行を持つ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を追加して、variant 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_idtax_rate.idの間にはFKがあります

バリアント2の貧乏人の実装は、標準税を指すproducts(または同様のテーブル)に行を追加するだけです。割合; 「標準税率」と呼ぶかもしれないダミーの製品-セットアップで許可されている場合。

FK制約は、参照整合性を適用します。それを完了するには、行にtax_rate_id IS NOT NULLを適用します(それが一般的に列に当てはまらない場合)。そして、その削除を禁止します。どちらもトリガーに入れることができます。余分なテーブルはありませんが、エレガントさが低く、信頼性が低くなります。

15

フィルターされたインデックス を使用できます

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 | foo 
-:| :-
 1 | t 
 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 ここ

9
McNets