Pular para conteúdo

POSTGRESQL INSERTORUPDATE - Upsert (Insert ou Update)

O que é este Node?

O POSTGRESQL INSERTORUPDATE é o node responsável por inserir registro ou atualizar se já existir (UPSERT) usando ON CONFLICT DO UPDATE do PostgreSQL.

Por que este Node existe?

Frequentemente precisamos garantir que registro exista sem saber se já foi inserido. O POSTGRESQL INSERTORUPDATE existe para:

  1. Idempotência: Executar múltiplas vezes produz mesmo resultado
  2. Atomicidade: Insert ou update em uma única operação atômica
  3. Performance: Mais rápido que SELECT + INSERT/UPDATE separados
  4. Simplicidade: Não precisa verificar existência manualmente
  5. Race conditions: Evita conflitos em ambientes concorrentes
  6. ON CONFLICT: Usa recurso nativo do PostgreSQL

Como funciona internamente?

Quando o POSTGRESQL INSERTORUPDATE é executado, o sistema:

  1. Valida config: Verifica credenciais PostgreSQL
  2. Valida conflictKeys: Campos que identificam conflito (geralmente PK ou UNIQUE)
  3. Substitui variáveis: Troca {{variavel}} em values
  4. Monta SQL: Cria INSERT ... ON CONFLICT ... DO UPDATE
  5. Executa: PostgreSQL tenta inserir, se conflitar atualiza
  6. Retorna: Devolve registro (inserido ou atualizado) e rowCount
  7. Se erro: Lança exceção (constraint violation, etc.)

Código interno (postgresql.executor.ts:179-214):

private async upsertRow(
  pool: Pool,
  table: string,
  fields: string[],
  values: Record<string, any>,
  conflictKeys: string[],
  context: ExecutionContext,
): Promise<any> {
  const replacedValues = this.replaceObjectVariables(values, context.variables);

  const columns = Object.keys(replacedValues).join(', ');
  const placeholders = Object.keys(replacedValues).map((_, i) => `$${i + 1}`).join(', ');
  const valueArray = Object.values(replacedValues);

  const updateColumns = Object.keys(replacedValues)
    .filter(key => !conflictKeys.includes(key))
    .map(key => `${key} = EXCLUDED.${key}`)
    .join(', ');

  const conflictClause = conflictKeys.join(', ');

  const query = `
    INSERT INTO ${table} (${columns})
    VALUES (${placeholders})
    ON CONFLICT (${conflictClause})
    DO UPDATE SET ${updateColumns}
    RETURNING *
  `;

  const result = await pool.query(query, valueArray);

  return {
    upsertedRow: result.rows[0],
    rowCount: result.rowCount,
  };
}

Quando você DEVE usar este Node?

Use POSTGRESQL INSERTORUPDATE quando não souber se registro já existe:

Casos de uso

  1. Sincronização: "Importar dados de sistema externo"
  2. Cache de dados: "Atualizar ou criar cache de API"
  3. Idempotência: "Requisições podem ser reprocessadas"
  4. Perfil de usuário: "Criar ou atualizar ao fazer login"
  5. Contadores: "Incrementar views ou criar se não existe"

Quando NÃO usar POSTGRESQL INSERTORUPDATE

  • Sabe que não existe: Use INSERT (mais simples)
  • Sabe que existe: Use UPDATE (mais direto)
  • Lógica condicional complexa: Use EXECUTEQUERY com CTE
  • Tabela sem UNIQUE/PK: UPSERT requer constraint de unicidade

Parâmetros Detalhados

config (object, obrigatório)

O que é: Configuração de conexão com PostgreSQL.

table (string, obrigatório)

O que é: Nome da tabela para upsert.

values (object, obrigatório)

O que é: Todos os campos e valores (incluindo conflictKeys).

conflictKeys (array, obrigatório)

O que é: Campos que identificam conflito (devem ter constraint UNIQUE ou PRIMARY KEY).

Formato: ["id"] ou ["email"] ou ["user_id", "product_id"] (chave composta)

IMPORTANTE: Campos em conflictKeys devem ter UNIQUE constraint ou ser PRIMARY KEY

Flow completo para testar:

{
  "name": "Teste PostgreSQL - Upsert",
  "nodes": [
    {
      "id": "start_1",
      "type": "start",
      "position": { "x": 100, "y": 100 },
      "data": { "label": "Início" }
    },
    {
      "id": "email_1",
      "type": "email",
      "position": { "x": 300, "y": 100 },
      "data": {
        "label": "Email",
        "parameters": {
          "message": "Seu email:",
          "variable": "email"
        }
      }
    },
    {
      "id": "input_1",
      "type": "input",
      "position": { "x": 500, "y": 100 },
      "data": {
        "label": "Nome",
        "parameters": {
          "message": "Seu nome:",
          "variable": "nome"
        }
      }
    },
    {
      "id": "postgres_1",
      "type": "postgresql",
      "position": { "x": 700, "y": 100 },
      "data": {
        "label": "Upsert Usuário",
        "parameters": {
          "operation": "insertOrUpdate",
          "config": {
            "host": "localhost",
            "port": 5432,
            "database": "app",
            "user": "app_user",
            "password": "secure_password",
            "ssl": false
          },
          "table": "usuarios",
          "values": {
            "email": "{{email}}",
            "nome": "{{nome}}",
            "ultimo_acesso": "CURRENT_TIMESTAMP",
            "ativo": true
          },
          "conflictKeys": ["email"],
          "responseVariable": "resultado"
        }
      }
    },
    {
      "id": "message_1",
      "type": "message",
      "position": { "x": 900, "y": 100 },
      "data": {
        "label": "Resultado",
        "parameters": {
          "message": "Dados salvos!\n\nEmail: {{email}}\nNome: {{nome}}\n\n(Inserido ou atualizado automaticamente)"
        }
      }
    },
    {
      "id": "end_1",
      "type": "end",
      "position": { "x": 1100, "y": 100 },
      "data": { "label": "Fim" }
    }
  ],
  "edges": [
    { "source": "start_1", "target": "email_1" },
    { "source": "email_1", "target": "input_1" },
    { "source": "input_1", "target": "postgres_1" },
    { "source": "postgres_1", "target": "message_1" },
    { "source": "message_1", "target": "end_1" }
  ]
}

Teste: Se email não existe, insere. Se existe, atualiza nome e ultimo_acesso.

responseVariable (string, opcional)

O que é: Nome da variável para armazenar resultado no contexto.

Parâmetros

Campo Tipo Obrigatório Descrição
operation string Sim Deve ser "insertOrUpdate"
config object Sim Configuração de conexão PostgreSQL
table string Sim Nome da tabela
values object Sim Todos os campos (incluindo conflictKeys)
conflictKeys array Sim Campos UNIQUE/PK que definem conflito
responseVariable string Não Variável para armazenar resultado

Exemplo 1: Sincronizar Produto de API Externa

Objetivo: Importar produtos de API, criando novos ou atualizando existentes

JSON para Importar

{
  "name": "PostgreSQL - Sincronizar Produto",
  "nodes": [
    {
      "id": "start_1",
      "type": "start",
      "position": { "x": 100, "y": 100 },
      "data": { "label": "Início" }
    },
    {
      "id": "input_1",
      "type": "input",
      "position": { "x": 300, "y": 100 },
      "data": {
        "label": "SKU",
        "parameters": {
          "message": "SKU do produto:",
          "variable": "sku"
        }
      }
    },
    {
      "id": "input_2",
      "type": "input",
      "position": { "x": 500, "y": 100 },
      "data": {
        "label": "Nome",
        "parameters": {
          "message": "Nome do produto:",
          "variable": "nomeProduto"
        }
      }
    },
    {
      "id": "number_1",
      "type": "number",
      "position": { "x": 700, "y": 100 },
      "data": {
        "label": "Preço",
        "parameters": {
          "message": "Preço (R$):",
          "variable": "preco",
          "decimals": 2,
          "min": 0.01
        }
      }
    },
    {
      "id": "number_2",
      "type": "number",
      "position": { "x": 900, "y": 100 },
      "data": {
        "label": "Estoque",
        "parameters": {
          "message": "Quantidade em estoque:",
          "variable": "estoque",
          "min": 0
        }
      }
    },
    {
      "id": "postgres_1",
      "type": "postgresql",
      "position": { "x": 1100, "y": 100 },
      "data": {
        "label": "Sincronizar",
        "parameters": {
          "operation": "insertOrUpdate",
          "config": {
            "host": "localhost",
            "port": 5432,
            "database": "ecommerce",
            "user": "app_user",
            "password": "secure_password",
            "ssl": false
          },
          "table": "produtos",
          "values": {
            "sku": "{{sku}}",
            "nome": "{{nomeProduto}}",
            "preco": "{{preco}}",
            "estoque": "{{estoque}}",
            "atualizado_em": "CURRENT_TIMESTAMP"
          },
          "conflictKeys": ["sku"],
          "responseVariable": "produto"
        }
      }
    },
    {
      "id": "message_1",
      "type": "message",
      "position": { "x": 1300, "y": 100 },
      "data": {
        "label": "Confirmação",
        "parameters": {
          "message": "Produto sincronizado!\n\nSKU: {{sku}}\nNome: {{nomeProduto}}\nPreço: R$ {{preco}}\nEstoque: {{estoque}}\n\nID: {{produto.upsertedRow.id}}"
        }
      }
    },
    {
      "id": "end_1",
      "type": "end",
      "position": { "x": 1500, "y": 100 },
      "data": { "label": "Fim" }
    }
  ],
  "edges": [
    { "source": "start_1", "target": "input_1" },
    { "source": "input_1", "target": "input_2" },
    { "source": "input_2", "target": "number_1" },
    { "source": "number_1", "target": "number_2" },
    { "source": "number_2", "target": "postgres_1" },
    { "source": "postgres_1", "target": "message_1" },
    { "source": "message_1", "target": "end_1" }
  ]
}

Saída esperada:

Sistema: SKU do produto:
Usuário: PROD-001
Sistema: Nome do produto:
Usuário: Notebook Dell
Sistema: Preço (R$):
Usuário: 3500.00
Sistema: Quantidade em estoque:
Usuário: 15
Sistema: Produto sincronizado!

SKU: PROD-001
Nome: Notebook Dell
Preço: R$ 3500.00
Estoque: 15

ID: 42

Exemplo 2: Registrar Login (Criar ou Atualizar)

Objetivo: Ao fazer login, criar usuário se não existe ou atualizar ultimo_login

JSON para Importar

{
  "name": "PostgreSQL - Login Upsert",
  "nodes": [
    {
      "id": "start_1",
      "type": "start",
      "position": { "x": 100, "y": 100 },
      "data": { "label": "Início" }
    },
    {
      "id": "email_1",
      "type": "email",
      "position": { "x": 300, "y": 100 },
      "data": {
        "label": "Email",
        "parameters": {
          "message": "Email para login:",
          "variable": "email"
        }
      }
    },
    {
      "id": "input_1",
      "type": "input",
      "position": { "x": 500, "y": 100 },
      "data": {
        "label": "Nome",
        "parameters": {
          "message": "Seu nome (se primeiro acesso):",
          "variable": "nome"
        }
      }
    },
    {
      "id": "postgres_1",
      "type": "postgresql",
      "position": { "x": 700, "y": 100 },
      "data": {
        "label": "Registrar Login",
        "parameters": {
          "operation": "insertOrUpdate",
          "config": {
            "host": "localhost",
            "port": 5432,
            "database": "auth",
            "user": "app_user",
            "password": "secure_password",
            "ssl": false
          },
          "table": "usuarios",
          "values": {
            "email": "{{email}}",
            "nome": "{{nome}}",
            "ultimo_login": "CURRENT_TIMESTAMP",
            "total_logins": "COALESCE(total_logins, 0) + 1"
          },
          "conflictKeys": ["email"],
          "responseVariable": "usuario"
        }
      }
    },
    {
      "id": "message_1",
      "type": "message",
      "position": { "x": 900, "y": 100 },
      "data": {
        "label": "Boas-vindas",
        "parameters": {
          "message": "Bem-vindo, {{usuario.upsertedRow.nome}}!\n\nÚltimo login registrado com sucesso."
        }
      }
    },
    {
      "id": "end_1",
      "type": "end",
      "position": { "x": 1100, "y": 100 },
      "data": { "label": "Fim" }
    }
  ],
  "edges": [
    { "source": "start_1", "target": "email_1" },
    { "source": "email_1", "target": "input_1" },
    { "source": "input_1", "target": "postgres_1" },
    { "source": "postgres_1", "target": "message_1" },
    { "source": "message_1", "target": "end_1" }
  ]
}

Saída esperada:

Sistema: Email para login:
Usuário: jose@example.com
Sistema: Seu nome (se primeiro acesso):
Usuário: José Roberto
Sistema: Bem-vindo, José Roberto!

Último login registrado com sucesso.

Resposta do Node

{
  "upsertedRow": {
    "id": 42,
    "email": "jose@example.com",
    "nome": "José Roberto",
    "ultimo_login": "2025-01-15T16:30:00.000Z",
    "total_logins": 1,
    "ativo": true
  },
  "rowCount": 1
}

Boas Práticas

SIM:

  • Use para sincronização de dados externos
  • Garanta que conflictKeys tenham UNIQUE constraint
  • Use para operações idempotentes
  • Ideal para APIs que podem ser chamadas múltiplas vezes
  • Atomicidade evita race conditions
  • Use EXCLUDED.campo para referenciar valores tentados no INSERT

NÃO:

  • Não use sem UNIQUE/PK nos conflictKeys
  • Não use quando sabe que registro não existe (use INSERT)
  • Não use quando sabe que registro existe (use UPDATE)
  • Não ignore constraint em conflictKeys
  • Não use para lógica complexa de merge

Dicas

💡 ON CONFLICT: PostgreSQL exclusivo (MySQL usa ON DUPLICATE KEY UPDATE)

💡 EXCLUDED: Referencia valores que seriam inseridos (use em DO UPDATE SET)

💡 Performance: Upsert é mais rápido que SELECT + INSERT/UPDATE separados

💡 Idempotência: Executar múltiplas vezes produz mesmo resultado (seguro para retry)

💡 Chave composta: conflictKeys pode ter múltiplos campos: ["user_id", "product_id"]

💡 COALESCE: Use para operações como contadores (COALESCE(campo, 0) + 1)

💡 Auditoria: Adicione created_at e updated_at para rastreamento

Próximo Node

POSTGRESQL INSERT - Inserir registros → POSTGRESQL UPDATE - Atualizar registros → POSTGRESQL DELETE - Deletar registros → POSTGRESQL EXECUTEQUERY - Consultas personalizadas