Evite violaciones únicas en transacciones atómicas

15

¿Es posible crear una transacción atómica en PostgreSQL?

Considere que tengo una categoría de tabla con estas filas:

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

Y el nombre de la columna tiene una restricción única.

Si lo intento:

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

Me estoy poniendo:

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

Respuestas:

24

Además de lo que proporcionó @Craig (y corrigiendo algunos de ellos):

Postgres efectiva 9.4 , UNIQUE, PRIMARY KEYy EXCLUDElas restricciones sean comprobadas inmediatamente después de cada fila cuando se define NOT DEFERRABLE. Esto es diferente de otros tipos de NOT DEFERRABLErestricciones (actualmente solo REFERENCES(clave externa)) que se verifican después de cada declaración . Trabajamos todo esto bajo esta pregunta relacionada sobre SO:

Es no suficiente para una UNIQUE(o PRIMARY KEYo EXCLUDE) la restricción sea DEFERRABLEpara hacer que su código presentado con varias instrucciones de trabajo.

Y usted puede no utilizar ALTER TABLE ... ALTER CONSTRAINTpara este propósito. Por documentación:

ALTER CONSTRAINT

Este formulario altera los atributos de una restricción que se creó previamente. Actualmente solo se pueden alterar las restricciones de clave externa .

El énfasis audaz es mío. Use en su lugar:

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

Descarte y agregue la restricción de nuevo en una sola declaración para que no haya una ventana de tiempo para que nadie pueda colarse en las filas ofensivas. Para tablas grandes sería tentador conservar el índice único subyacente de alguna manera, porque es costoso eliminarlo y recrearlo. Por desgracia, eso no parece posible con las herramientas estándar (si tiene una solución para eso, ¡háganoslo saber!):

Para una sola declaración, hacer que la restricción sea diferible es suficiente:

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;

Una consulta con CTE también es una declaración única :

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

Sin embargo , para su código con múltiples declaraciones, usted (adicionalmente) necesita diferir la restricción, o definirla como INITIALLY DEFERREDO bien, generalmente es más costoso que el anterior. Pero puede no ser fácilmente factible agrupar todo en una sola declaración.

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

Sin embargo, tenga en cuenta una limitación en relación con las FOREIGN KEYrestricciones. Por documentación:

Las columnas a las que se hace referencia deben ser las columnas de una restricción de clave primaria o única no diferible en la tabla de referencia.

Entonces no puedes tener ambos al mismo tiempo.

Erwin Brandstetter
fuente
13

Según tengo entendido, su problema aquí es que la restricción se verifica después de cada declaración, pero desea que se verifique al final de la transacción, por lo que compara el estado anterior con el estado posterior, ignorando los estados intermedios.

Si es así, eso es posible con una restricción diferible .

Ver SET CONSTRAINTSy DEFERRABLErestricciones como se documenta en CREATE TABLE.

Tenga en cuenta que las restricciones diferidas tienen costos: el sistema tiene que mantener una lista de ellas para verificar en el momento de la confirmación, por lo que no son buenas para las transacciones que realizan grandes conjuntos de cambios. También son más lentos para verificar.

Así que creo que probablemente quieras:

ALTER TABLE mytable ALTER CONSTRAINT category_name_key DEFERRABLE;

Tenga en cuenta que parece haber una limitación para ALTER TABLEestablecer restricciones en DEFERRABLE; es posible que tenga que hacerlo DROPy volver a ADDrestringir.

Craig Ringer
fuente