Evitar violação exclusiva na transação atômica

15

É possível criar transações atômicas no PostgreSQL?

Considere que eu tenho uma categoria de tabela com estas linhas:

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

E o nome da coluna tem restrição exclusiva.

Se eu tentar:

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

Estou entendendo:

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

Respostas:

24

Além do que o @Craig forneceu (e corrigiu alguns deles ):

Eficaz Postgres 9.4 , UNIQUE, PRIMARY KEYe EXCLUDEas restrições são verificadas imediatamente após cada linha quando definido NOT DEFERRABLE. Isso é diferente de outros tipos de NOT DEFERRABLErestrições (atualmente apenas REFERENCES(chave estrangeira)) que são verificadas após cada instrução . Nós resolvemos tudo isso sob esta pergunta relacionada no SO:

É não o suficiente para uma UNIQUE(ou PRIMARY KEYou EXCLUDErestrição) para ser DEFERRABLEfazer o seu código apresentado com várias instruções de trabalho.

E você não pode usar ALTER TABLE ... ALTER CONSTRAINTpara esse fim. Por documentação:

ALTER CONSTRAINT

Este formulário altera os atributos de uma restrição criada anteriormente. Atualmente, apenas as restrições de chave estrangeira podem ser alteradas .

Negrito ênfase minha. Use em vez disso:

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

Solte e adicione a restrição de volta em uma única instrução, para que não haja janela de tempo para alguém se infiltrar nas linhas ofensivas. Para tabelas grandes, seria tentador conservar o índice exclusivo subjacente de alguma forma, porque é caro excluí-lo e recriá-lo. Infelizmente, isso não parece possível com ferramentas padrão (se você tiver uma solução para isso, informe-nos!):

Para uma única declaração, tornar a restrição adiada é 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;

Uma consulta com CTEs também é uma única instrução:

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

No entanto , para o seu código com várias instruções, você (adicionalmente) precisa realmente adiar a restrição - ou defini-la como INITIALLY DEFERREDOu normalmente é mais cara que a anterior. Mas pode não ser facilmente viável agrupar tudo em uma única declaração.

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

Esteja ciente de uma limitação em conexão com FOREIGN KEYrestrições, no entanto. Por documentação:

As colunas referenciadas devem ser as colunas de uma restrição de chave primária ou única não diferida na tabela referenciada.

Então você não pode ter os dois ao mesmo tempo.

Erwin Brandstetter
fonte
13

Pelo que entendi, seu problema aqui é que a restrição é verificada após cada instrução, mas você deseja que seja verificada no final da transação, para comparar o estado antes com o estado posterior, ignorando os estados intermediários.

Nesse caso, isso é possível com uma restrição adiada .

Veja SET CONSTRAINTSe DEFERRABLErestrições conforme documentado em CREATE TABLE.

Observe que restrições diferidas têm custos - o sistema precisa manter uma lista delas para verificar no momento da confirmação, para que não sejam boas para transações que fazem grandes conjuntos de alterações. Eles também são mais lentos para verificar.

Então eu acho que você provavelmente quer:

ALTER TABLE mytable ALTER CONSTRAINT category_name_key DEFERRABLE;

Observe que parece haver uma limitação na ALTER TABLEdefinição de restrições para DEFERRABLE; você pode ter que DROPsubstituir ADDa restrição.

Craig Ringer
fonte