Combine ON DELETE SET NULL e ON DELETE CASCADE atribuindo cada ação a uma chave estrangeira separada. Este guia usa artigos sintéticos para mostrar como a exclusão de uma coleção pode preservar seus artigos, enquanto a exclusão de um artigo remove suas seções.

A tarefa é definir esse comportamento, verificar os dois caminhos de exclusão no Terminal de um Mac e repetir as verificações com node-postgres. Os exemplos usam chaves estrangeiras comuns e tabelas temporárias, portanto cada execução tem suas próprias linhas de exemplo.

Qual relacionamento deve usar cada ação de exclusão?

Uma ação de exclusão pertence à chave estrangeira na tabela que faz a referência. SET NULL limpa a referência quando a linha referenciada desaparece; CASCADE exclui a linha que faz a referência. O PostgreSQL documenta relacionamentos opcionais como um uso adequado de SET NULL e componentes dependentes como um uso adequado de CASCADE. Restrições

Neste exemplo, uma coleção agrupa artigos, mas um artigo pode existir sem uma coleção. Uma seção pertence a um único artigo e não tem função independente. Estas são escolhas explícitas de projeto para o exemplo:

Chave estrangeira Ação escolhida Resultado pretendido
demo_articles.collection_id referencia demo_collections.collection_id ON DELETE SET NULL Remover uma coleção preserva seus artigos sem uma referência à coleção.
demo_sections.article_id referencia demo_articles.article_id ON DELETE CASCADE Remover um artigo remove suas seções.

O comportamento resultante depende de onde a exclusão começa. Excluir uma coleção altera o collection_id de um artigo; não exclui o artigo. Portanto, não há exclusão de artigo à qual a cláusula ON DELETE CASCADE da chave estrangeira da seção possa responder. Excluir o próprio artigo ativa essa cascata. Isso decorre das definições documentadas das duas ações. CREATE TABLE

Não coloque duas chaves estrangeiras conflitantes no mesmo relacionamento para expressar “limpar a referência e excluir a linha”. Decida se a linha que faz a referência deve sobreviver e escolha a ação que implementa essa decisão.

Crie um exemplo completo no seu Mac

Você precisa de um servidor PostgreSQL em execução, de um banco de dados existente ao qual possa se conectar e de psql disponível no Terminal. Os comandos abaixo usam o banco de dados local existente chamado postgres; substitua pelo nome do seu banco de dados existente, se necessário. A instalação e a configuração do servidor não fazem parte deste guia.

Salve o conteúdo a seguir como um arquivo de texto simples chamado setup.sql em uma pasta de trabalho. As três tabelas são temporárias: o PostgreSQL remove tabelas temporárias quando a sessão termina. Todas as tabelas referenciadas neste exemplo também são temporárias, pois o PostgreSQL não permite chaves estrangeiras entre tabelas temporárias e permanentes. CREATE TABLE

CREATE TEMP TABLE demo_collections (
    collection_id integer PRIMARY KEY,
    title text NOT NULL
);

CREATE TEMP TABLE demo_articles (
    article_id integer PRIMARY KEY,
    collection_id integer,
    title text NOT NULL,
    CONSTRAINT demo_articles_collection_fk
        FOREIGN KEY (collection_id)
        REFERENCES demo_collections (collection_id)
        ON DELETE SET NULL
);

CREATE TEMP TABLE demo_sections (
    section_id integer PRIMARY KEY,
    article_id integer NOT NULL,
    heading text NOT NULL,
    CONSTRAINT demo_sections_article_fk
        FOREIGN KEY (article_id)
        REFERENCES demo_articles (article_id)
        ON DELETE CASCADE
);

CREATE INDEX demo_articles_collection_idx
    ON demo_articles (collection_id);
CREATE INDEX demo_sections_article_idx
    ON demo_sections (article_id);

INSERT INTO demo_collections (collection_id, title) VALUES
    (1, 'Layout Notes'),
    (2, 'Color Notes');

INSERT INTO demo_articles (article_id, collection_id, title) VALUES
    (101, 1, 'Grid Spacing'),
    (102, 2, 'Color Pairings');

INSERT INTO demo_sections (section_id, article_id, heading) VALUES
    (1001, 101, 'Overview'),
    (1002, 101, 'Examples'),
    (1003, 102, 'Overview');

Observe que collection_id aceita valores nulos, enquanto article_id na tabela de seções é obrigatório. Uma ação SET NULL ainda precisa satisfazer as demais restrições da tabela. Os índices explícitos cobrem as colunas que fazem as referências; o PostgreSQL não cria esses índices automaticamente quando uma chave estrangeira é declarada. Restrições

Verifique os dois caminhos de exclusão em uma sessão

Salve o próximo bloco como verify.sql junto de setup.sql. É um script completo de psql: \ir inclui o arquivo de configuração usando um caminho relativo ao script de verificação. Execute os dois arquivos por meio deste script para manter as tabelas temporárias na mesma sessão. A opção -f executa um arquivo, e ON_ERROR_STOP interrompe o processamento do script após um erro. psql

BEGIN;
\ir setup.sql

DELETE FROM demo_collections
WHERE collection_id = 1
RETURNING collection_id;

SELECT a.article_id,
       a.title,
       a.collection_id,
       c.title AS collection_title,
       count(s.section_id) AS section_count
FROM demo_articles AS a
LEFT JOIN demo_collections AS c
    ON c.collection_id = a.collection_id
LEFT JOIN demo_sections AS s
    ON s.article_id = a.article_id
GROUP BY a.article_id, a.title, a.collection_id, c.title
ORDER BY a.article_id;

DELETE FROM demo_articles
WHERE article_id = 101
RETURNING article_id;

SELECT section_id, article_id, heading
FROM demo_sections
ORDER BY section_id;

ROLLBACK;

No Terminal, acesse a pasta que contém os arquivos e execute:

psql -X -v ON_ERROR_STOP=1 -d postgres -f verify.sql

Os resultados esperados da primeira consulta são derivados das linhas de exemplo:

Artigo ID da coleção após a exclusão da coleção Título da coleção Número de seções
101 NULL NULL 2
102 2 Color Notes 1

O artigo 101 sobrevive com suas duas seções. Após a segunda exclusão, a consulta de seções deve retornar apenas a seção 1003, pertencente ao artigo 102. Esse par de verificações distingue a preservação de um artigo da exclusão de um artigo e das linhas dependentes dele.

A consulta de verificação usa LEFT JOIN intencionalmente. O PostgreSQL mantém uma linha sem correspondência da tabela à esquerda e fornece valores nulos para os valores ausentes da tabela à direita. Um INNER JOIN com as coleções omitiria o artigo 101 depois que sua referência se tornasse nula, fazendo com que um artigo preservado desaparecesse dos resultados dessa consulta. Expressões de tabela

As cláusulas RETURNING informam as linhas excluídas das tabelas de destino. Elas não fornecem um inventário combinado de todas as seções removidas pela cascata; a consulta separada de seções verifica esse resultado. DELETE

O ROLLBACK final cancela as alterações da demonstração. O PostgreSQL também trata uma instrução isolada como uma transação, portanto uma transação explícita é útil aqui para agrupar a configuração, a exclusão, a inspeção e a limpeza. Transações

Execute as mesmas verificações com node-postgres

Para a versão em JavaScript, mantenha setup.sql inalterado. Use o Node.js 24 neste exemplo e instale o pacote documentado pg na pasta de trabalho: node-postgres

npm install pg

Salve o programa completo a seguir como verify.mjs junto de setup.sql. Ele lê os mesmos dados sintéticos de exemplo, verifica se a exclusão da coleção preserva os dois artigos e as três seções e, em seguida, verifica se a exclusão de um artigo remove apenas as seções correspondentes.

O programa usa um único cliente conectado durante toda a transação, conforme exigido pelo node-postgres. Os valores das consultas são passados pela interface documentada de matriz de parâmetros usando $1. Transações, Consultas

import pg from 'pg';
import { readFile } from 'node:fs/promises';
import assert from 'node:assert/strict';

const { Client } = pg;
const client = new Client();
const setup = await readFile(
  new URL('./setup.sql', import.meta.url),
  'utf8'
);

await client.connect();
try {
  await client.query('BEGIN');
  await client.query(setup);

  const removed = await client.query(
    'DELETE FROM demo_collections WHERE collection_id = $1 RETURNING collection_id',
    [1]
  );
  assert.deepEqual(removed.rows, [{ collection_id: 1 }]);

  const articles = await client.query(
    'SELECT article_id, collection_id FROM demo_articles ORDER BY article_id'
  );
  assert.deepEqual(articles.rows, [
    { article_id: 101, collection_id: null },
    { article_id: 102, collection_id: 2 }
  ]);

  const before = await client.query(
    'SELECT section_id FROM demo_sections ORDER BY section_id'
  );
  assert.deepEqual(
    before.rows.map(row => row.section_id),
    [1001, 1002, 1003]
  );

  await client.query(
    'DELETE FROM demo_articles WHERE article_id = $1',
    [101]
  );
  const after = await client.query(
    'SELECT section_id FROM demo_sections ORDER BY section_id'
  );
  assert.deepEqual(after.rows, [{ section_id: 1003 }]);

  console.log('Expected SET NULL and CASCADE outcomes verified.');
  await client.query('ROLLBACK');
} catch (error) {
  try {
    await client.query('ROLLBACK');
  } catch (rollbackError) {
    console.error('Rollback failed:', rollbackError.message);
  }
  throw error;
} finally {
  await client.end();
}

Execute-o no mesmo banco de dados local existente:

PGDATABASE=postgres node verify.mjs

new Client() lê as variáveis de ambiente de conexão do PostgreSQL. Preserve as configurações de conexão que seu servidor local já exige; alterar apenas o nome do banco de dados não configura outro servidor. Conexão

Uma asserção com falha entra no caminho de reversão e gera um erro. A mensagem de sucesso aparece somente depois que todas as verificações de resultado são aprovadas. Este programa também reverte a transação em caso de sucesso, mantendo a demonstração repetível.

A sintaxe de JavaScript foi verificada durante a preparação. Não havia um executável do PostgreSQL disponível no ambiente de pesquisa, então os exemplos de banco de dados foram comparados com a documentação em vez de executados; as tabelas acima descrevem os resultados esperados.

O que muda com chaves estrangeiras compostas?

O exemplo principal usa chaves estrangeiras de coluna única. Uma chave estrangeira composta introduz outra decisão: a exclusão deve limpar todas as colunas que fazem referência ou apenas a coluna opcional? O PostgreSQL 15 introduziu listas de colunas para ações de chave estrangeira ON DELETE SET; as versões anteriores afetavam todas as colunas da chave estrangeira. Notas de lançamento do PostgreSQL 15

Por exemplo, uma referência composta a um artigo pode conter um identificador de edição obrigatório e um identificador opcional de artigo relacionado. No PostgreSQL 15 ou posterior, ON DELETE SET NULL (related_article_id) pode preservar o identificador da edição e limpar a referência opcional ao artigo. Sem essa lista de colunas, SET NULL afeta ambas as colunas. Essa sintaxe é uma extensão do PostgreSQL e está disponível para ON DELETE, não para ON UPDATE. CREATE TABLE

A atribuição parcial de valores nulos também exige uma regra de correspondência compatível. A opção padrão MATCH SIMPLE permite uma chave estrangeira composta que contenha um valor nulo; MATCH FULL rejeita uma combinação de valores nulos e não nulos nas colunas que fazem a referência. Portanto, manter um identificador de edição obrigatório enquanto se atribui valor nulo apenas ao identificador do artigo relacionado é incompatível com MATCH FULL. Restrições

No PostgreSQL 14, use um esquema com uma referência opcional separada de coluna única, como no exemplo principal, quando isso representar corretamente o relacionamento. Se a relação composta precisar ser mantida, limpe explicitamente sua referência opcional antes de excluir a linha referenciada, usando uma única transação. Essa é uma sequência gerenciada pela aplicação, em vez da sintaxe de lista de colunas não compatível.

Há outro limite documentado no PostgreSQL 18: chaves estrangeiras temporais que usam PERIOD não oferecem suporte a CASCADE nem a SET NULL. As chaves estrangeiras comuns demonstradas aqui são a opção compatível para essas ações de exclusão quando a cobertura de duração não faz parte do relacionamento. CREATE TABLE

Como diagnosticar um resultado inesperado?

Use o caminho de exclusão como sequência de diagnóstico:

  1. Confirme a tabela que faz a referência. Inspecione \d demo_articles e \d demo_sections na sessão que contém as tabelas. Verifique as ações reais das chaves estrangeiras em vez de deduzi-las pelos nomes das colunas. O PostgreSQL documenta \d como uma forma de inspecionar os detalhes da tabela e os nomes das restrições. Modificação de tabelas
  2. Verifique a nulabilidade e as demais restrições. Se a exclusão da coleção falhar, confirme se a coluna que será limpa aceita valores nulos e se as demais restrições aceitam a linha resultante. SET NULL não as ignora. Restrições
  3. Inspecione todas as chaves estrangeiras relevantes. Outra relação pode ter uma ação diferente. Se nenhuma ação for especificada, o padrão é NO ACTION, o que pode impedir a exclusão quando ainda houver referências. CREATE TABLE
  4. Consulte a tabela-base antes de interpretar uma junção. Se o artigo ainda existir nela, inspecione as junções e os filtros que podem omitir sua nova referência nula. Expressões de tabela
  5. Mantenha as instruções da transação no mesmo cliente. Em uma aplicação que usa um pool, obtenha um único cliente para toda a transação. O node-postgres recomenda explicitamente não usar pool.query() para instruções de transação. Transações

Para este modelo de artigo, o teste de aceitação é concreto: remover a coleção 1 mantém o artigo 101 e suas seções intactos, enquanto remover o artigo 101 deixa apenas a seção do artigo 102. Mantenha essas duas verificações juntas ao adaptar o esquema para deixar visíveis os comportamentos pretendidos de preservação e exclusão.

Fontes