Einschränkung - Eine boolesche Zeile ist wahr, alle anderen Zeilen falsch

13

Ich habe eine Spalte: standard BOOLEAN NOT NULL

Ich möchte eine Zeile True und alle anderen False erzwingen. Abhängig von dieser Einschränkung gibt es keine FKs oder sonst etwas. Ich weiß, dass ich es mit plpgsql schaffen kann, aber das scheint ein Vorschlaghammer zu sein. Ich würde so etwas wie eine CHECKoder UNIQUEEinschränkung bevorzugen . Je einfacher desto besser.

Eine Zeile muss True sein, sie kann nicht alle False sein (daher müsste die erste eingefügte Zeile True sein).

Die Zeile muss aktualisiert werden, was bedeutet, dass ich warten muss, um die Einschränkungen zu überprüfen, bis die Aktualisierungen abgeschlossen sind, da alle Zeilen zuerst auf False und danach auf eine Zeile True gesetzt werden können.

Es gibt eine FK zwischen products.tax_rate_idund tax_rate.id, die jedoch nichts mit dem Standard- oder Standardsteuersatz zu tun hat, der vom Benutzer ausgewählt werden kann, um das Erstellen neuer Produkte zu vereinfachen.

PostgreSQL 9.5, wenn es darauf ankommt.

Hintergrund

Die Tabelle ist der Steuersatz. Einer der Steuersätze ist der Standard ( standardda der Standard ein Postgres-Befehl ist). Wenn ein neues Produkt hinzugefügt wird, wird der Standardsteuersatz auf das Produkt angewendet. Wenn dies nicht standardder Fall ist, muss die Datenbank entweder eine Vermutung anstellen oder alle Arten von nicht benötigten Überprüfungen durchführen. Die einfache Lösung, dachte ich, bestand darin, sicherzustellen, dass es eine gibt standard.

Mit "Standard" oben meine ich für die Präsentationsschicht (UI). Es gibt eine Benutzeroption zum Ändern des Standardsteuersatzes. Ich muss entweder zusätzliche Überprüfungen hinzufügen, um sicherzustellen, dass die GUI / der Benutzer nicht versucht, die tax_rate_id auf NULL zu setzen, oder dann einfach einen Standardsteuersatz festlegen.

theGtknerd
quelle
Hast du deine Antwort?
Erwin Brandstetter
Ja, ich habe meine Antwort. Vielen Dank für Ihre Eingabe, @ErwinBrandstetter. Ich neige mich vorerst zu einem Abzug. Dies ist ein Open Source-Projekt in meiner Freizeit. Wenn ich es tatsächlich implementiere, werde ich die von mir verwendete Antwort als akzeptiert markieren.
theGtknerd

Antworten:

15

Variante 1

Da Sie nur eine einzelne Spalte mit benötigen standard = true, setzen Sie den Standard in allen anderen Zeilen auf NULL. Dann UNIQUEfunktioniert eine einfache Einschränkung, da NULL-Werte sie nicht verletzen:

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)
);

DEFAULTist eine optionale Erinnerung daran, dass die erste eingegebene Zeile die Standardeinstellung sein sollte. Es erzwingt nichts. Obwohl Sie nicht mehr als eine Zeile standard = truefestlegen können, können Sie dennoch alle Zeilen auf NULL setzen. Es gibt keine saubere Möglichkeit, dies mit nur Einschränkungen in einer einzelnen Tabelle zu verhindern . CHECKEinschränkungen berücksichtigen keine anderen Zeilen (ohne schmutzige Tricks).

Verbunden:

Aktualisieren:

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

So erlauben Sie einen Befehl wie (wobei die Einschränkung nur am Ende der Anweisung erfüllt ist):

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

.. die UNIQUEEinschränkung müsste sein DEFERRABLE. Sehen:

dbfiddle hier

Variante 2

Habe einen zweiten Tisch mit einer einzelnen Zeile wie:

Erstellen Sie dies als Superuser:

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);

Jetzt gibt es immer eine einzelne Zeile, die auf den Standard zeigt (in diesem einfachen Fall wird auch die Standardrate direkt dargestellt). Nur ein Superuser konnte es brechen. Sie können dies auch mit einem Auslöser nicht zulassen BEFORE DELETE.

dbfiddle hier

Verbunden:

Sie können ein hinzufügen VIEW, um dasselbe wie in Variante 1 zu sehen :

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

Verwenden Sie bei Abfragen, bei denen Sie nur den Standardtarif wünschen, (nur) taxrate_standard.taxratedirekt.


Sie haben später hinzugefügt:

Es gibt eine FK zwischen products.tax_rate_idundtax_rate.id

Die Implementierung von Variante 2 durch einen armen Mann würde darin bestehen, nur eine Zeile products(oder eine ähnliche Tabelle) hinzuzufügen, die auf den Standardsteuersatz verweist. Ein Dummy-Produkt, das Sie möglicherweise als "Standardsteuersatz" bezeichnen - sofern Ihr Setup dies zulässt.

Die FK-Einschränkungen erzwingen die referenzielle Integrität. Um dies zu vervollständigen, erzwingen Sie tax_rate_id IS NOT NULLfür die Zeile (falls dies für die Spalte im Allgemeinen nicht der Fall ist). Und verbiete seine Löschung. Beides könnte ausgelöst werden. Kein zusätzlicher Tisch, aber weniger elegant und nicht so zuverlässig.

Erwin Brandstetter
quelle
2
Ich kann den Zwei-Tabellen-Ansatz nur empfehlen. Ich würde auch vorschlagen, dieser Variante eine Beispielabfrage hinzuzufügen, damit das OP sehen kann, wie es CROSS JOINgegen den Standard, LEFT JOINgegen den spezifischen und dann COALESCEzwischen den beiden vorgeht.
jpmc26
2
+1, ich hatte die gleiche Vorstellung von der zusätzlichen Tabelle, aber keine Zeit, eine Antwort richtig zu schreiben. Über die erste Tabelle und die CONSTRAINT standard_only_1_true UNIQUE (standard): Ich nehme an, die Tabelle wird nicht groß sein, also spielt es keine Rolle, aber da die Einschränkung einen Index für die gesamte Tabelle definiert, würde ein teilweise eindeutiger Index nicht WHERE (standard)weniger Speicherplatz verbrauchen?
Ypercubeᵀᴹ
@ ypercubeᵀᴹ: Ja, der Index für die gesamte Tabelle ist größer, das ist ein Nachteil für diese Variante. Aber wie Sie sagten: Es ist offensichtlich ein winziger Tisch, also spielt es kaum eine Rolle. Ich strebte die einfachste Standardlösung mit nur Einschränkungen an. Konzeptioneller Beweiß. Persönlich bin ich mit jpmc26 und bevorzuge stark Variante 2.
Erwin Brandstetter
9

Sie können einen gefilterten Index verwenden

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);
FEHLER: Doppelter Schlüsselwert verletzt eindeutige Einschränkung "only_one_row_with_column_true_uix"
DETAIL: Schlüssel (foo) = (t) existiert bereits.

dbfiddle hier


Aber wie Sie sagten, muss die erste Zeile wahr sein, dann können Sie eine CHECK-Einschränkung verwenden, aber selbst mit einer Funktion können Sie die erste Zeile später löschen.

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);
FEHLER: Neue Zeile für die Beziehung "Test" verletzt die Prüfbedingung "ck_one_true"
DETAIL: Fehlerhafte Zeile enthält (5, t).

select * from test;
id | foo
-: | : -
 1 | t  
 2 | f  
 3 | f  
 4 | f  
delete from test where id = 1;

dbfiddle hier


Sie können das Problem lösen, indem Sie einen BEFORE DELETE-Trigger hinzufügen, um sicherzustellen, dass die erste Zeile (foo ist wahr) niemals gelöscht wird.

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;

FEHLER: Zeile, in der foo wahr ist, kann nicht gelöscht werden.

dbfiddle hier

McNets
quelle