Vincolo: una riga booleana è vera, tutte le altre righe sono false


13

Ho una colonna: standard BOOLEAN NOT NULL

Vorrei imporre una riga True e tutti gli altri False. Non ci sono FK o altro a seconda di questo vincolo. So che posso realizzarlo con plpgsql, ma questo sembra un martello. Io preferirei qualcosa di simile a una CHECKo UNIQUEvincolo. Più semplice è, meglio è.

Una riga deve essere True, non possono essere tutti False (quindi la prima riga inserita dovrebbe essere True).

La riga dovrà essere aggiornata, il che significa che devo aspettare per controllare i vincoli fino a quando non vengono effettuati gli aggiornamenti, poiché tutte le righe possono essere impostate False prima e una riga True in seguito.

C'è un FK tra products.tax_rate_ide tax_rate.id, ma non ha nulla a che fare con l'aliquota fiscale predefinita o standard, che è selezionabile dall'utente per facilitare la creazione di nuovi prodotti.

PostgreSQL 9.5 se è importante.

sfondo

La tabella è l'aliquota fiscale. Una delle aliquote fiscali è l'impostazione predefinita ( standardpoiché l'impostazione predefinita è un comando Postgres). Quando viene aggiunto un nuovo prodotto, al prodotto viene applicata l'aliquota fiscale standard. In caso contrario standard, il database deve eseguire un'ipotesi o tutti i tipi di controlli non necessari. La soluzione semplice, pensavo, era assicurarsi che ci fosse un standard.

Per "impostazione predefinita" sopra, intendo per il livello di presentazione (UI). Esiste un'opzione utente per modificare l'aliquota fiscale predefinita. O ho bisogno di aggiungere ulteriori controlli per garantire che la GUI / utente non tenti di impostare tax_rate_id su NULL, oppure di impostare semplicemente un'aliquota fiscale predefinita.


Quindi hai la tua risposta?
Erwin Brandstetter,

Sì, ho la mia risposta, grazie mille per il tuo contributo, @ErwinBrandstetter. Per il momento mi sto sporgendo verso un grilletto. Questo è un progetto open source sul mio tempo. Quando lo realizzo, segnerò la risposta accettata che uso.
theGtknerd

Risposte:


15

Variante 1

Poiché tutto ciò che serve è una singola colonna con standard = true, impostare lo standard su NULL in tutte le altre righe. Quindi funziona un semplice UNIQUEvincolo, poiché i valori NULL non lo violano:

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è un promemoria facoltativo che la prima riga inserita deve diventare l'impostazione predefinita. Non sta facendo rispettare nulla. Sebbene non sia possibile impostare più di una riga su standard = true, è comunque possibile impostare NULL su tutte le righe. Non esiste un modo pulito per impedirlo con solo vincoli in una singola tabella. CHECKi vincoli non considerano altre righe (senza trucchi sporchi).

Relazionato:

Aggiornare:

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

Per consentire un comando simile (dove il vincolo è soddisfatto solo alla fine dell'istruzione):

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

.. il UNIQUEvincolo dovrebbe essere DEFERRABLE. Vedere:

dbfiddle qui

Variante 2

Avere una seconda tabella con una singola riga come:

Crea questo come superutente:

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

Ora c'è sempre una singola riga che punta allo standard (in questo caso semplice rappresenta anche direttamente la tariffa standard). Solo un superutente potrebbe romperlo. Potresti anche impedirlo con un grilletto BEFORE DELETE.

dbfiddle qui

Relazionato:

È possibile aggiungere a VIEWper vedere lo stesso della variante 1 :

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

Nelle query in cui tutto ciò che desideri è la tariffa standard, utilizza (solo) taxrate_standard.taxratedirettamente.


Successivamente hai aggiunto:

C'è un FK tra products.tax_rate_idetax_rate.id

L'implementazione della variante 2 da parte di un povero sarebbe semplicemente aggiungere una riga products(o una tabella simile) che punta all'aliquota fiscale standard; un prodotto fittizio che potresti chiamare "Aliquota fiscale standard" - se la tua configurazione lo consente.

I vincoli FK impongono l'integrità referenziale. Per completarlo, applica tax_rate_id IS NOT NULLla riga (se non è il caso della colonna in generale). E non consentire la sua cancellazione. Entrambi potrebbero essere attivati. Nessun tavolo extra, ma meno elegante e non altrettanto affidabile.


2
Altamente raccomandare l'approccio a due tavolo. Suggerirei anche di aggiungere una query di esempio a quella variazione in modo che l'OP possa vedere come CROSS JOINcontro lo standard, LEFT JOINlo specifico e quindi COALESCEtra i due.
jpmc26,

2
+1, ho avuto la stessa idea sul tavolo extra, ma non ho tempo di scrivere correttamente una risposta. A proposito della prima tabella e del CONSTRAINT standard_only_1_true UNIQUE (standard): suppongo che la tabella non sarà grande, quindi non importa molto ma poiché il vincolo definirà un indice sull'intera tabella, un indice univoco parziale con WHERE (standard)meno spazio?
ypercubeᵀᴹ

@ ypercubeᵀᴹ: Sì, l'indice sull'intera tabella è più grande, questo è uno svantaggio per questa variante. Ma come hai detto: è ovviamente un tavolino, quindi non importa. Miravo alla soluzione standard più semplice con solo vincoli. Verifica teorica. Personalmente, sono con jpmc26 e preferisco fortemente la variante 2.
Erwin Brandstetter,

9

È possibile utilizzare un indice filtrato

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);
ERRORE: il valore chiave duplicato viola il vincolo univoco "only_one_row_with_column_true_uix"
DETTAGLIO: Key (foo) = (t) esiste già.

dbfiddle qui


Ma come hai detto, la prima riga deve essere vera, quindi è possibile utilizzare un vincolo CHECK, ma anche utilizzando una funzione è possibile eliminare la prima riga in un secondo momento.

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);
ERRORE: nuova riga per la relazione "test" viola il vincolo di controllo "ck_one_true"
DETTAGLIO: la riga non riuscita contiene (5, t).

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

dbfiddle qui


È possibile risolverlo aggiungendo un trigger PRIMA DELETE per assicurarsi che la prima riga (foo sia vera) non venga mai eliminata.

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;

ERRORE: impossibile eliminare la riga dove foo è vero.

dbfiddle qui

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.