Vermeiden Sie eindeutige Verstöße bei atomaren Transaktionen

15

Ist es möglich in PostgreSQL eine atomare Transaktion zu erstellen?

Angenommen, ich habe eine Tabellenkategorie mit den folgenden Zeilen:

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

Der Spaltenname hat eine eindeutige Einschränkung.

Wenn ich es versuche:

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

Ich erhalte:

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

Antworten:

24

Neben was @Craig bereitgestellt hat (und einige davon korrigiert):

Effective Postgres 9.4 , UNIQUE, PRIMARY KEYund EXCLUDEwerden Randbedingungen überprüft unmittelbar nach jeder Zeile , wenn definiert NOT DEFERRABLE. Dies unterscheidet sich von anderen Arten von NOT DEFERRABLEEinschränkungen (derzeit nur REFERENCES(Fremdschlüssel)), die nach jeder Anweisung überprüft werden . All dies haben wir unter dieser verwandten Frage zu SO ausgearbeitet:

Es reicht nicht aus, dass eine UNIQUE(oder PRIMARY KEYoder EXCLUDE) Einschränkung darin besteht DEFERRABLE, dass der präsentierte Code mit mehreren Anweisungen funktioniert.

Und Sie können nicht verwenden ALTER TABLE ... ALTER CONSTRAINTfür diesen Zweck. Per Dokumentation:

ALTER CONSTRAINT

Dieses Formular ändert die Attribute einer zuvor erstellten Einschränkung. Derzeit können nur Fremdschlüsseleinschränkungen geändert werden .

Meine kühne Betonung. Verwenden Sie stattdessen:

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

Löschen Sie die Einschränkung und fügen Sie sie in einer einzigen Anweisung wieder hinzu, damit niemand Zeit hat, sich in beleidigende Zeilen zu schleichen. Für große Tabellen wäre es verlockend, den zugrunde liegenden eindeutigen Index irgendwie beizubehalten, da es kostspielig ist, ihn zu löschen und neu zu erstellen. Leider scheint das mit Standardwerkzeugen nicht möglich zu sein (wenn Sie eine Lösung dafür haben, lassen Sie es uns bitte wissen!):

Für eine einzelne Anweisung reicht es aus , die Einschränkung aufschiebbar zu machen:

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;

Eine Abfrage mit CTEs ist auch eine einzelne Anweisung:

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

Allerdings , für Ihren Code mit mehreren Anweisungen Sie (zusätzlich) Notwendigkeit, tatsächlich defer die Einschränkung - oder definiert es als INITIALLY DEFERREDentweder die Regel teurer als die oben ist. Es ist jedoch möglicherweise nicht einfach, alles in eine Aussage zu packen.

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

Beachten Sie jedoch eine Einschränkung im Zusammenhang mit FOREIGN KEYEinschränkungen. Per Dokumentation:

Die referenzierten Spalten müssen die Spalten einer nicht deferrierbaren Eindeutigkeits- oder Primärschlüsseleinschränkung in der referenzierten Tabelle sein.

Sie können also nicht beide gleichzeitig haben.

Erwin Brandstetter
quelle
13

Soweit ich weiß, haben Sie hier das Problem, dass die Einschränkung nach jeder Anweisung überprüft wird. Sie möchten jedoch, dass sie am Ende der Transaktion überprüft wird, sodass der Vorher-Zustand mit dem Nachher-Zustand verglichen wird, wobei die Zwischenzustände ignoriert werden.

Wenn ja, ist dies mit einer aufschiebbaren Einschränkung möglich .

Siehe SET CONSTRAINTSund DEFERRABLEEinschränkungen, wie in dokumentiertCREATE TABLE .

Beachten Sie, dass zurückgestellte Einschränkungen mit Kosten verbunden sind. Das System muss eine Liste der Einschränkungen führen, die zum Zeitpunkt des Festschreibens überprüft werden müssen, damit sie nicht für Transaktionen geeignet sind, bei denen umfangreiche Änderungen vorgenommen werden. Sie sind auch langsamer zu überprüfen.

Ich denke, Sie möchten wahrscheinlich:

ALTER TABLE mytable ALTER CONSTRAINT category_name_key DEFERRABLE;

Beachten Sie, dass es anscheinend eine Einschränkung beim ALTER TABLEFestlegen von Einschränkungen für gibt DEFERRABLE. Sie könnte stattdessen haben DROPund re- ADDdie Einschränkung.

Craig Ringer
quelle