Estou tendo problemas para fazer um gatilho INSTEAD OF funcionar corretamente e acho que não entendi como usar NEW. Considere o seguinte cenário simplificado:
CREATE TABLE Product (
product_id SERIAL PRIMARY KEY,
product_name VARCHAR
);
CREATE TABLE Purchase (
purchase_id SERIAL PRIMARY KEY,
product_id INT REFERENCES Product,
when_bought DATE
);
CREATE VIEW PurchaseView AS
SELECT purchase_id, product_name, when_bought
FROM Purchase LEFT JOIN Product USING (product_id);
Eu gostaria de poder criar INSTEAD OF
gatilhos para permitir que eu insira diretamente no PurchaseView
, por exemplo:
INSERT INTO Product(product_name) VALUES ('foo');
INSERT INTO PurchaseView(product_name, when_bought) VALUES ('foo', NOW());
O que eu tinha em mente era algo como:
CREATE OR REPLACE FUNCTION insert_purchaseview_func()
RETURNS trigger AS
$BODY$
BEGIN
INSERT INTO Purchase(product_id, when_bought)
SELECT product_id, when_bought
FROM NEW
LEFT JOIN Product USING (product_name)
RETURNING * INTO NEW;
END;
$BODY$
LANGUAGE plpgsql;
CREATE TRIGGER insert_productview_trig
INSTEAD OF INSERT
ON PurchaseView
FOR EACH ROW
EXECUTE PROCEDURE insert_purchaseview_func();
No entanto, a função de gatilho acima apresenta erros ( relation "new" does not exist
) quando executada. Sei que posso escrever consultas que usam explicitamente atributos de NEW
cláusulas WHERE
e SELECT
, mas às vezes seria conveniente poder incluir NEW
em uma junção. Existe uma maneira de fazer isso?
Solução atual (insatisfatória)
O mais próximo que posso chegar do que quero é
CREATE OR REPLACE FUNCTION insert_purchaseview_func()
RETURNS trigger AS
$BODY$
DECLARE
tmp RECORD;
BEGIN
WITH input (product_name, when_bought) as (
values (NEW.product_name, NEW.when_bought)
)
INSERT INTO Purchase(product_id, when_bought)
SELECT product_id, when_bought
FROM input
LEFT JOIN Product USING (product_name)
RETURNING * INTO tmp;
RETURN NEW;
END;
$BODY$
LANGUAGE plpgsql;
Isso é um pouco insatisfatório por vários motivos:
Preciso escrever explicitamente todos os atributos
NEW
em CTE WITH query, o que para visualizações grandes (especialmente aquelas cujos atributos são determinados automaticamente comSELECT *
) fica pesado;O resultado retornado não tem o
SERIAL
tipoproduct_id
atualizado, então você não obtém o resultado esperado para:INSERT INTO PurchaseView(product_name, when_bought) VALUES ('foo', NOW()) RETURNING *;
NEW
é um registro , não uma tabela . Fundamentos:Configuração ligeiramente modificada
product_name
tem que serUNIQUE
, ou a pesquisa nesta coluna poderia encontrar várias linhas, o que levaria a todos os tipos de confusão.1. Solução simples
Para o seu exemplo simples, procurando apenas a coluna única
product_id
, uma subconsulta correlacionada é mais simples e rápida:Sem variáveis adicionais. Sem CTE (só acrescentaria custo e ruído). As colunas de
NEW
são escritas apenas uma vez (seu ponto 1 ).O anexo
RETURNING purchase_id INTO NEW.purchase_id
cuida do seu ponto 2 : agora, a linha retornada inclui o recém-geradopurchase_id
.Caso o produto não seja encontrado (
NEW.product_name
não existe na tabelaproduct
), a compra ainda é inserida eproduct_id
éNULL
. Isso pode ou não ser desejável.2.
Para pular a linha (e possivelmente aumentar um
WARNING
/EXCEPTION
):Isso pega carona
NEW
nas colunas paraSELECT .. FROM product
. Se o produto for encontrado, tudo ocorre normalmente. Se não, nenhuma linha é retornada doSELECT
e nãoINSERT
acontece. A variável especial PL/pgSQLFOUND
só é verdadeira se a última consulta SQL processou pelo menos uma linha.Pode ser
EXCEPTION
em vez deWARNING
gerar um erro e reverter a transação. Mas prefiro declararpurchase.product_id NOT NULL
e inserir incondicionalmente (consulta 1 ou similar), para o mesmo efeito: gera uma exceção seproduct_id
forNULL
. Mais simples, mais barato.3. Para várias pesquisas
Os
LEFT JOIN
s tornam oINSERT
incondicional novamente. Em vez disso, useJOIN
para pular se não for encontrado.FROM (SELECT NEW.*) i
transforma o registroNEW
em uma tabela derivada com uma única linha , que pode ser usada como qualquer tabela noFROM
cláusula - o que você procurava, inicialmente.db<>mexa aqui
Conforme sugerido nos comentários, parece que o mais próximo que posso fazer do que quero é (corrigindo minha abordagem original na pergunta):
Isso pelo menos faz com que a
RETURNING
cláusula funcione corretamente. Parece que os atributos deNEW
devem ser explicitamente declarados. A seguir:resulta em
ERROR: column "product_name" specified in USING clause does not exist in left table
quando o gatilho é disparado.