INSTEAD OFトリガーを正しく機能させるのに問題があり、NEWの使い方を誤解しているようです。次の簡略化されたシナリオを考えてみます。
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);
INSTEAD OF
トリガーを作成してPurchaseView
に直接挿入できるようにしたいと思います。例:
INSERT INTO Product(product_name) VALUES ('foo');
INSERT INTO PurchaseView(product_name, when_bought) VALUES ('foo', NOW());
私が考えていたのは、次のようなものでした。
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();
ただし、上記のトリガー関数を実行すると、エラー(relation "new" does not exist
)が発生します。 NEW
とWHERE
句でSELECT
の属性を明示的に使用するクエリを記述できることはわかっていますが、結合にNEW
を含めると便利な場合があります。これを行う方法はありますか?
現在の(不十分な)ソリューション
私が欲しいものに最も近いのは
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;
これはいくつかの理由で少し満足できません:
CTE WITHクエリでNEW
のすべての属性を明示的に書き込む必要があります。これは、大きなビュー(特に、属性がSELECT *
で自動的に決定されるビュー)では扱いにくくなります。
返された結果ではSERIAL
タイプproduct_id
が更新されていないため、次の場合に期待される結果が得られません。
INSERT INTO PurchaseView(product_name, when_bought)
VALUES ('foo', NOW())
RETURNING *;
NEW
はレコードであり、テーブルではありません。基本:
_CREATE TABLE product (
product_id serial PRIMARY KEY,
product_name text UNIQUE NOT NULL -- must be UNIQUE
);
CREATE TABLE purchase (
purchase_id serial PRIMARY KEY,
product_id int REFERENCES product,
when_bought date
);
CREATE VIEW purchaseview AS
SELECT pu.purchase_id, pr.product_name, pu.when_bought
FROM purchase pu
LEFT JOIN product pr USING (product_id);
INSERT INTO product(product_name) VALUES ('foo');
_
_product_name
_はUNIQUE
である必要があります。そうしないと、この列の検索で複数の行が見つかり、あらゆる種類の混乱が生じる可能性があります。
単純な例として、単一の列_product_id
_のみを検索する場合、相関の低いサブクエリが最も単純で高速です。
_CREATE OR REPLACE FUNCTION insert_purchaseview_func()
RETURNS trigger AS
$func$
BEGIN
INSERT INTO purchase(product_id, when_bought)
SELECT (SELECT product_id FROM product WHERE product_name = NEW.product_name), NEW.when_bought
RETURNING purchase_id
INTO NEW.purchase_id; -- generated serial ID for RETURNING - if needed
RETURN NEW;
END
$func$ LANGUAGE plpgsql;
CREATE TRIGGER insert_productview_trig
INSTEAD OF INSERT ON purchaseview
FOR EACH ROW EXECUTE PROCEDURE insert_purchaseview_func();
_
追加の変数はありません。 CTEなし(コストとノイズを追加するだけ)。 NEW
の列はスペルアウト1回のみ(yourpoint 1)。
追加された_RETURNING purchase_id INTO NEW.purchase_id
_は、point 2を処理します:これで、返される行には新しく生成された_purchase_id
_。
製品が見つからない(_NEW.product_name
_がテーブルproduct
に存在しない)場合でも、購入は挿入され、_product_id
_はNULL
です。これは望ましい場合と望ましくない場合があります。
代わりに行をスキップするには(そしてWARNING
/EXCEPTION
を上げる可能性があります):
_CREATE OR REPLACE FUNCTION insert_purchaseview_func()
RETURNS trigger AS
$func$
BEGIN
INSERT INTO purchase AS pu
(product_id, when_bought)
SELECT pr.product_id, NEW.when_bought
FROM product pr
WHERE pr.product_name = NEW.product_name
RETURNING pu.purchase_id
INTO NEW.purchase_id; -- generated serial ID for RETURNING - if needed
IF NOT FOUND THEN -- insert was canceled for missing product
RAISE WARNING 'product_name % not found! Skipping INSERT.', quote_literal(NEW.product_name);
END IF;
RETURN NEW;
END
$func$ LANGUAGE plpgsql;
_
これは、NEW
列を_SELECT .. FROM product
_に便乗させます。製品が見つかった場合、すべてが正常に続行されます。そうでない場合、SELECT
から行は返されず、INSERT
は発生しません。特別なPL/pgSQL変数FOUND
は、最後のSQLクエリが少なくとも1つの行を処理した場合にのみ真になります。
エラーを発生させてトランザクションをロールバックするには、EXCEPTION
ではなくWARNING
にすることができます。しかし、私はむしろ_purchase.product_id NOT NULL
_を宣言して無条件に挿入し(クエリ1または類似)、同じ効果を得ます:_product_id
_がNULL
の場合に例外を発生させます。よりシンプルで安価。
_CREATE OR REPLACE FUNCTION insert_purchaseview_func()
RETURNS trigger AS
$func$
BEGIN
INSERT INTO purchase AS pu
(product_id, when_bought) -- more columns?
SELECT pr.product_id, i.when_bought -- more columns?
FROM (SELECT NEW.*) i -- see below
LEFT JOIN product pr USING (product_name)
-- LEFT JOIN tbl2 t2 USING (t2_name) -- more lookups?
RETURNING pu.purchase_id -- more columns?
INTO NEW.purchase_id; -- more columns?
RETURN NEW;
END
$func$ LANGUAGE plpgsql;
_
_LEFT JOIN
_ sは、INSERT
を再び無条件にします。見つからない場合は、代わりにJOIN
を使用してスキップします。
FROM (SELECT NEW.*) i
は、recordNEW
を単一の派生テーブルに変換しますrow、これはFROM
句の任意のテーブルのように使用できます-最初に探していたもの。
db <> fiddle ここ
コメントで示唆されているように、私がしたいことに最も近いことができるようです(質問の元のアプローチを修正します):
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 purchase_id INTO tmp;
NEW.purchase_id = tmp.purchase_id;
RETURN NEW;
END;
$BODY$
LANGUAGE plpgsql;
これにより、少なくともRETURNING
句が正しく機能します。 NEW
の属性は明示的に宣言する必要があるようです。以下:
-- Using NEW.* in CTE doesn't work
CREATE OR REPLACE FUNCTION insert_purchaseview_func()
RETURNS trigger AS
$BODY$
DECLARE
tmp RECORD;
BEGIN
WITH input as (
values (NEW.*)
)
INSERT INTO Purchase(product_id, when_bought)
SELECT product_id, when_bought
FROM input
LEFT JOIN Product USING (product_name)
RETURNING purchase_id INTO tmp;
NEW.purchase_id = tmp.purchase_id;
RETURN NEW;
END;
$BODY$
LANGUAGE plpgsql;
結果はERROR: column "product_name" specified in USING clause does not exist in left table
トリガーが起動されたとき。