Impedindo deleções em uma tabela no Postgres

Posted on June 10, 2018

Suponha que alguns dados estão sumindo do seu banco, e aparentemente nada no código da aplicação parece deletar explicitamente esses dados. Será que é alguma operação interna do seu framework ou ORM que está sendo executada? Será que é algum microsserviço que está se comportando mal? Será que é algum CASCADE que não devia acontecer?

Na Onyo tivemos esse exato problema um tempo atrás, em que certos dados estavam sumindo sem razão aparente. Para resolver, precisávamos de uma solução que pudesse não só impedir qualquer DELETE na tabela, mas também identificar a parte da aplicação que estava causando esse comportamento.

Como não sabíamos qual a origem do problema, só poderíamos resolver isso a nível do banco de dados, e para isso a solução mais óbvia pareceu ser usar triggers.

Triggers

Gatilhos ou triggers permitem atrelar certos procedimentos a certos eventos no banco de dados. Por exemplo, suponha que você queira salvar um histórico de todas as alterações de preço na sua tabela de produtos. Uma forma de fazer isso é criar uma tabela de histórico e um trigger que faça INSERT do histórico sempre que ocorrer um UPDATE de produto:

Note que definimos não só o tipo do evento, mas também em que momento o trigger deve ser executado. Entre as possiblidades está executar o procedimento em vez do evento original. Usando o exemplo acima, poderíamos então evitar deleções de produtos dessa forma:

Essa solução parece resolver nosso problema: em vez de deletar o produto, a consulta vai levantar um erro na nossa aplicação e assim poderemos descobrir qual parte do código está fazendo besteira. Ela só tem um problema: quando tentamos executar as definições acima, recebemos o erro:

ERROR:  "product" is a table
DETAIL:  Tables cannot have INSTEAD OF triggers.

Esse erro ocorre porque triggers com INSTEAD OF só são permitidos para views, não para tabelas. O que fazer então?

Solução 1 - trigger BEFORE DELETE + retornar NULL na função

A documentação do PostgreSQL diz que triggers que acontecem antes do evento podem retornar NULL para sinalizar que as operações seguintes, incluindo o próprio evento e outros triggers, devem ser abortadas. Com isso, podemos manter exatamente a mesma função e mudar apenas a definição do trigger:

Solução 2 - usar rules em vez de triggers

O PostgreSQL também oferece o mecanismo de rules, que são parecidas com triggers, mas bem mais poderosas. Enquanto triggers são chamados enquanto a query original já está sendo executada, rules permitem reescrever a query antes de ela ser executada. Isso significa que podemos impedir as deleções simplesmente refazendo a query original para lançar um erro:

Essa solução também funciona, e tem a vantagem de explicitar na definição que o delete está sendo substituído por outra query, em vez de impedir a deleção implicitamente fazendo a função retornar NULL como na opção anterior. No entanto, a mensagem de erro fica bem menos clara quando a origem da deleção vem de um CASCADE, dando um erro de integridade em vez do ‘Nope nope nope’ definido. A escolha entre as duas soluções é subjetiva, e eu pessoalmente prefiro a primeira solução para ter a consistência da mensagem de erro.

Conclusão

No caso da Onyo usamos a segunda opção simplesmente porque eu desconhecia a primeira na época. Mesmo assim, conseguimos aplicar essa solução e resolver o problema. A causa? Uma certa funcionalidade esquecida executava uma requisição de DELETE em nossa API RESTful, fazendo com que o Django Rest Framework prontamente deletasse o recurso no banco.

Eu não recomendo usar triggers ou rules para implementar regras de negócio, pois misturar a lógica entre código e banco pode dificultar bastante a manutenção da aplicação, por motivos que fogem ao escopo desse post. No entanto, essas ferramentas podem ser a última barreira para proteger a integridade dos seus dados e ainda funcionam como uma ótima forma de debug de guerrilha.