Apagar é uma decisão de arquitetura
O pedido chega sempre do mesmo jeito. Alguém cadastrou errado, ou o cliente saiu, ou a tela ficou poluída, e a pergunta é simples: dá pra colocar um botão de excluir aqui?
Dá. O DELETE tem uma linha. Em ambiente de desenvolvimento a tabela tem três registros e nenhum deles aponta pra lugar nenhum, então funciona na primeira tentativa. O ticket fecha no mesmo dia.
O problema aparece semanas depois, quando alguém clica naquele botão em produção. Lá a linha não está sozinha. Ela tem filhos, e alguns desses filhos têm filhos.
O que "só um botão de excluir" está pedindo
Quando alguém pede exclusão, quase nunca é isso que a pessoa quer. O que ela quer costuma ser uma destas coisas:
- sumir da lista, porque a tela está poluída
- parar de contar, porque o registro está estragando um relatório
- não existir mais em lugar nenhum, porque é o único jeito de cumprir uma obrigação
Os três pedidos usam a mesma palavra e pedem implementações diferentes. O primeiro é um filtro. O segundo é uma flag de status. Só o terceiro é DELETE de verdade.
A maior parte dos incidentes de dado apagado que eu já vi começou nessa confusão. Ninguém decidiu apagar informação valiosa. Alguém pediu para não ver mais, e recebeu remoção definitiva.
O que o banco faz quando você não decide
Vale conhecer o comportamento padrão, porque ele já é uma escolha, mesmo quando você não fez nenhuma.
No PostgreSQL, se você declara uma chave estrangeira e não diz nada sobre exclusão, ela nasce como NO ACTION. Isso significa que o banco recusa a operação se ainda existirem linhas referenciando aquele registro. É o mesmo resultado prático de RESTRICT, com uma diferença que só aparece dentro de transação: NO ACTION pode ser adiado até o fim da transação, RESTRICT não.
ON DELETE CASCADE faz o oposto. Apagou o pai, o banco apaga os filhos automaticamente. E a cadeia continua até encontrar uma chave estrangeira declarada com outra regra, onde ela para.
Existem ainda SET NULL e SET DEFAULT, que não apagam o filho: trocam o valor da coluna que apontava para o pai. São úteis em relações opcionais e costumam ser esquecidas.
Nenhuma dessas opções é a certa por padrão. Todas são respostas para perguntas diferentes.
O custo que ninguém coloca na estimativa
O que faz essa decisão ser cara não é o DELETE. É o que vem depois dele.
O critério que eu uso
Antes de escolher a regra da chave estrangeira, eu faço uma pergunta só: o registro filho significa alguma coisa sem o pai?
Se não significa, CASCADE é a resposta honesta. Item de pedido não existe sem o pedido. Endereço de entrega de uma compra não existe sem a compra. Foto dentro de um álbum que foi apagado não vira nada. Manter esses registros vivos só cria lixo que alguém vai ter que limpar depois.
Se significa, RESTRICT. Nota fiscal emitida, lançamento financeiro, log de auditoria e histórico de atendimento têm valor próprio. Eles descrevem algo que aconteceu, e o fato de o cliente ter saído não desfaz o que aconteceu.
E aí vem a parte que quase ninguém implementa, que é o que transforma RESTRICT de obstáculo em ferramenta: uma forma de ver o que está pendurado antes de confirmar. Um endpoint simples que responde quantos registros dependem daquele id, e de que tipo eles são. A tela mostra o número, a pessoa decide com informação.
Mensagem de erro de banco de dados não serve como interface. update or delete on table violates foreign key constraint é uma informação correta escrita para quem não vai lê-la.
Quando o oposto é a resposta certa
Passei um bom tempo defendendo que quase nada deveria ser apagado de verdade, e essa posição tem um limite claro.
O primeiro limite é legal. A LGPD, no artigo 18, garante ao titular o direito de pedir a eliminação dos dados pessoais a qualquer momento, inclusive daqueles tratados com consentimento. Existem exceções previstas em lei para retenção, mas o padrão é que o pedido seja atendido. Um sistema onde nada é apagado de fato, apenas marcado como inativo, não consegue responder a esse pedido, e você descobre isso no pior momento possível.
O segundo limite é operacional. soft delete espalhado sem disciplina cobra caro. Toda consulta precisa lembrar do filtro, e a que esquecer vai devolver dado que deveria estar invisível. Índice único deixa de funcionar como você espera, porque o registro inativo continua ocupando o valor. Relatório passa a divergir dependendo de quem escreveu a query.
O terceiro é mais simples: nem todo dado merece esse cuidado. Rascunho, item de carrinho abandonado, notificação lida, cache de sessão. Apagar de verdade é mais barato do que carregar.
O fecho
A decisão sobre exclusão não é sobre o botão. É sobre quanto o sistema deve saber a respeito do estrago antes de deixar alguém confirmar.
Se apagar naquela tela pode doer, alguém precisa ver o tamanho do dano antes, e não em forma de mensagem de erro.
Qual é o DELETE do seu sistema que hoje ninguém sabe exatamente o que leva junto?