web-dev-qa-db-ja.com

PostgreSQLはINSTEAD OFトリガーのクエリでNEWを使用します

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)が発生します。 NEWWHERE句で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;

これはいくつかの理由で少し満足できません:

  1. CTE WITHクエリでNEWのすべての属性を明示的に書き込む必要があります。これは、大きなビュー(特に、属性がSELECT *で自動的に決定されるビュー)では扱いにくくなります。

  2. 返された結果ではSERIALタイプproduct_idが更新されていないため、次の場合に期待される結果が得られません。

    INSERT INTO PurchaseView(product_name, when_bought) 
    VALUES ('foo', NOW())
    RETURNING *;
    
3
beldaz

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である必要があります。そうしないと、この列の検索で複数の行が見つかり、あらゆる種類の混乱が生じる可能性があります。

1.シンプルなソリューション

単純な例として、単一の列_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です。これは望ましい場合と望ましくない場合があります。

2。

代わりに行をスキップするには(そして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の場合に例外を発生させます。よりシンプルで安価。

3.複数の検索の場合

_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 ここ

3

コメントで示唆されているように、私がしたいことに最も近いことができるようです(質問の元のアプローチを修正します):

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トリガーが起動されたとき。

3
beldaz