web-dev-qa-db-fra.com

Contrainte - une ligne booléenne est vraie, toutes les autres lignes sont fausses

J'ai une colonne: standard BOOLEAN NOT NULL

Je voudrais appliquer une ligne True et toutes les autres False. Il n'y a pas de FK ou autre chose en fonction de cette contrainte. Je sais que je peux l'accomplir avec plpgsql, mais cela ressemble à un marteau. Je préférerais quelque chose comme une contrainte CHECK ou UNIQUE. Plus c'est simple, mieux c'est.

Une ligne doit être True, elles ne peuvent pas toutes être False (donc la première ligne insérée doit être True).

La ligne devra être mise à jour, ce qui signifie que je dois attendre pour vérifier les contraintes jusqu'à ce que les mises à jour soient terminées, car toutes les lignes peuvent être définies sur False en premier et une ligne sur True après.

Il y a un FK entre products.tax_rate_id et tax_rate.id, mais cela n'a rien à voir avec le taux de taxe par défaut ou standard, sélectionnable par l'utilisateur pour faciliter la création de nouveaux produits.

PostgreSQL 9.5 si cela est important.

Contexte

Le tableau est le taux d'imposition. L'un des taux de taxe est la valeur par défaut (standard car la valeur par défaut est une commande Postgres). Lorsqu'un nouveau produit est ajouté, le taux de taxe standard est appliqué au produit. S'il n'y a pas de standard, la base de données doit faire une supposition ou toutes sortes de vérifications inutiles. La solution simple, je pensais, était de s'assurer qu'il y avait un standard.

Par "par défaut" ci-dessus, je veux dire pour la couche de présentation (UI). Il existe une option utilisateur pour modifier le taux de taxe par défaut. Je dois soit ajouter des vérifications supplémentaires pour m'assurer que l'interface graphique/l'utilisateur n'essaie pas de définir le tax_rate_id sur NULL, soit simplement définir un taux de taxe par défaut.

13
theGtknerd

Variante 1

Puisque tout ce dont vous avez besoin est une seule colonne avec standard = true, définissez la norme sur NULL dans toutes les autres lignes. Alors une contrainte UNIQUE simple fonctionne, puisque les valeurs NULL ne la violent pas:

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 est un rappel facultatif que la première ligne entrée doit devenir la valeur par défaut. Ce n'est pas en vigueur quoi que ce soit. Bien que vous ne puissiez pas définir plusieurs lignes sur standard = true, vous pouvez toujours définir toutes les lignes NULL. Il n'y a aucun moyen propre d'empêcher cela avec seulement contraintes dans une seule table. CHECK les contraintes ne prennent pas en compte les autres lignes (sans astuces sales).

En relation:

Mettre à jour:

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

Pour autoriser une commande comme (où la contrainte n'est satisfaite qu'à la fin de l'instruction):

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

.. la contrainte UNIQUE devrait être DEFERRABLE. Voir:

dbfiddle ici

Variante 2

Avoir un deuxième tableau avec une seule ligne comme:

Créez ceci en tant que superutilisateur:

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

Maintenant, il y a toujours une seule ligne pointant vers la norme (dans ce cas simple représentant également directement le taux standard). Seul un superutilisateur pouvait le casser. Vous pouvez également interdire cela avec un déclencheur BEFORE DELETE.

dbfiddle ici

En relation:

Vous pouvez ajouter un VIEW pour voir la même chose que dans 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);

Dans les requêtes où tout ce que vous voulez est le taux standard, utilisez (uniquement) taxrate_standard.taxrate directement.


Vous avez ajouté plus tard:

Il y a un FK entre products.tax_rate_id et tax_rate.id

Une implémentation du pauvre de la variante 2 consisterait simplement à ajouter une ligne à products (ou tout autre tableau similaire) pointant vers la taxe standard taux; un produit factice que vous pourriez appeler "Taux de taxe standard" - si votre configuration le permet.

Les contraintes FK imposent l'intégrité référentielle. Pour le terminer, appliquez tax_rate_id IS NOT NULL pour la ligne (si ce n'est pas le cas pour la colonne en général). Et interdire sa suppression. Les deux pourraient être mis en déclencheurs. Pas de table supplémentaire, mais moins élégante et moins fiable.

15
Erwin Brandstetter

Vous pouvez utiliser un index filtré

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);
 ERREUR: la valeur de clé en double viole la contrainte unique "only_one_row_with_column_true_uix" 
 DÉTAIL: La clé (foo) = (t) existe déjà. 
 

dbfiddle ici


Mais comme vous l'avez dit, la première ligne doit être vraie, alors vous pouvez utiliser une contrainte CHECK, mais même en utilisant une fonction, vous pouvez supprimer la première ligne plus tard.

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);
 ERREUR: une nouvelle ligne pour la relation "test" viole la contrainte de vérification "ck_one_true" 
 DÉTAIL: La ligne défaillante contient (5, t). 
 
select * from test;
 id | foo 
 -: | : - 
 1 | t 
 2 | f 
 3 | f 
 4 | F  
delete from test where id = 1;

dbfiddle ici


Vous pouvez le résoudre en ajoutant un déclencheur BEFORE DELETE pour vous assurer que la première ligne (foo est vrai) n'est jamais supprimée.

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;

ERREUR: impossible de supprimer la ligne où foo est vrai.

dbfiddle ici

9
McNets