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:
- Idempotência: Executar múltiplas vezes produz mesmo resultado
- Atomicidade: Insert ou update em uma única operação atômica
- Performance: Mais rápido que SELECT + INSERT/UPDATE separados
- Simplicidade: Não precisa verificar existência manualmente
- Race conditions: Evita conflitos em ambientes concorrentes
- ON CONFLICT: Usa recurso nativo do PostgreSQL
Como funciona internamente?
Quando o POSTGRESQL INSERTORUPDATE é executado, o sistema:
- Valida config: Verifica credenciais PostgreSQL
- Valida conflictKeys: Campos que identificam conflito (geralmente PK ou UNIQUE)
- Substitui variáveis: Troca {{variavel}} em values
- Monta SQL: Cria INSERT ... ON CONFLICT ... DO UPDATE
- Executa: PostgreSQL tenta inserir, se conflitar atualiza
- Retorna: Devolve registro (inserido ou atualizado) e rowCount
- 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
- Sincronização: "Importar dados de sistema externo"
- Cache de dados: "Atualizar ou criar cache de API"
- Idempotência: "Requisições podem ser reprocessadas"
- Perfil de usuário: "Criar ou atualizar ao fazer login"
- 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