Évitez la violation unique dans la transaction atomique

15

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.
Petr Přikryl
la source

Réponses:

24

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

À compter Postgres 9.4 , UNIQUE, PRIMARY KEYet les EXCLUDEcontraintes sont vérifiées immédiatement après chaque ligne lorsqu'elle est définie NOT DEFERRABLE. Ceci est différent des autres types de NOT DEFERRABLEcontraintes (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:

Il ne suffit pas qu'une contrainte UNIQUE(ou PRIMARY KEYou EXCLUDE) soit DEFERRABLEpour faire fonctionner votre code présenté avec plusieurs instructions .

Et vous ne pouvez pas l' utiliser ALTER TABLE ... ALTER CONSTRAINTà cette fin. 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 sur moi. Utilisez plutôt:

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

Supprimez et ajoutez la contrainte dans une seule instruction afin qu'il n'y ait pas de fenêtre temporelle pour que quiconque se glisse 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 déclaration rendant 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 seule instruction:

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 devez (en plus) différer réellement la contrainte - ou la définir comme INITIALLY DEFERREDSoit 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 liée aux FOREIGN KEYcontraintes. Par documentation:

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

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

Erwin Brandstetter
la source
13

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, de sorte qu'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 différée .

Voir SET CONSTRAINTSet DEFERRABLEcontraintes comme documenté 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 à la ALTER TABLEdéfinition de contraintes sur DEFERRABLE; vous devrez peut-être à la place DROPet à nouveau ADDla contrainte.

Craig Ringer
la source