web-dev-qa-db-fra.com

Évitez la violation unique dans la transaction atomique

Est-il possible de créer une transaction atomique dans PostgreSQL?

Considérez que j'ai une catégorie de table avec ces lignes:

id|name
--|---------
1 |'tablets'
2 |'phones'

Et le nom de la colonne a une contrainte unique.

Si j'essaye:

BEGIN;
update "category" set name = 'phones' where id = 1;
update "category" set name = 'tablets' where id = 2;
COMMIT;

Je suis en train:

ERROR:  duplicate key value violates unique constraint "category_name_key"
DETAIL:  Key (name)=(tablets) already exists.
15
Petr Přikryl

En plus de ce que @ Craig fourni (et en corriger une partie):

Postgres effectifs 9.4 , UNIQUE, PRIMARY KEY et EXCLUDE les contraintes sont vérifiées immédiatement après chaque ligne lorsqu'elles sont définies NOT DEFERRABLE. Ceci est différent des autres types de NOT DEFERRABLE contraintes (actuellement uniquement REFERENCES (clé étrangère)) qui sont vérifiées après chaque instruction . Nous avons travaillé tout cela sous cette question connexe sur SO:

Ce n'est pas suffisant pour un UNIQUE (ou PRIMARY KEY ou EXCLUDE) contrainte d'être DEFERRABLE pour que votre code présenté avec plusieurs instructions fonctionne.

Et vous ne pouvez pas utiliser ALTER TABLE ... ALTER CONSTRAINT dans ce but. Par documentation:

ALTER CONSTRAINT

Ce formulaire modifie les attributs d'une contrainte précédemment créée. Actuellement seules les contraintes de clé étrangère peuvent être modifiées .

Accentuation mienne. Utilisez plutôt:

ALTER TABLE t
   DROP CONSTRAINT category_name_key
 , ADD  CONSTRAINT category_name_key UNIQUE(name) DEFERRABLE;

Supprimez et rajoutez la contrainte dans une seule instruction afin qu'il n'y ait pas de fenêtre temporelle pour quiconque de se faufiler dans les lignes incriminées. Pour les grandes tables, il serait tentant de conserver l'index unique sous-jacent d'une manière ou d'une autre, car il est coûteux de le supprimer et de le recréer. Hélas, cela ne semble pas possible avec des outils standards (si vous avez une solution pour cela, faites-le nous savoir!):

Pour une seule instruction rendre la contrainte reportable suffit:

UPDATE category c
SET    name = c_old.name
FROM   category c_old
WHERE  c.id     IN (1,2)
AND    c_old.id IN (1,2)
AND    c.id <> c_old.id;

Une requête avec CTE est également une instruction single:

WITH x AS (
    UPDATE category SET name = 'phones' WHERE id = 1
    )
UPDATE category SET name = 'tablets' WHERE id = 2;

Cependant , pour votre code avec plusieurs instructions vous (en plus) devez reporter la contrainte - ou la définir comme INITIALLY DEFERRED Soit est généralement plus cher que ce qui précède. Mais il peut ne pas être facile de tout regrouper en une seule instruction.

BEGIN;
SET CONSTRAINTS category_name_key DEFERRED;
UPDATE category SET name = 'phones'  WHERE id = 1;
UPDATE category SET name = 'tablets' WHERE id = 2;
COMMIT;

Soyez conscient d'une limitation en relation avec FOREIGN KEY contraintes, cependant. Par documentation:

Les colonnes référencées doivent être les colonnes d'une contrainte de clé unique ou primaire non différable dans la table référencée.

Vous ne pouvez donc pas avoir les deux en même temps.

24
Erwin Brandstetter

Si je comprends bien, votre problème ici est que la contrainte est vérifiée après chaque instruction, mais vous voulez qu'elle soit vérifiée à la fin de la transaction, donc elle compare l'état avant à l'état après, en ignorant les états intermédiaires.

Si c'est le cas, cela est possible avec une contrainte déférable .

Voir SET CONSTRAINTS et DEFERRABLE contraintes comme indiqué dans CREATE TABLE .

Notez que les contraintes différées ont des coûts - le système doit en conserver une liste à vérifier au moment de la validation, elles ne sont donc pas adaptées aux transactions qui apportent d'énormes ensembles de modifications. Ils sont également plus lents à vérifier.

Je pense donc que vous voulez probablement:

ALTER TABLE mytable ALTER CONSTRAINT category_name_key DEFERRABLE;

Notez qu'il semble y avoir une limitation sur ALTER TABLE définition des contraintes sur DEFERRABLE; vous devrez peut-être à la place DROP et re -ADD la contrainte.

13
Craig Ringer