トリガプロシージャ

PostgreSQL 8.0.0 文書
前のページ | 巻戻し | 第 35章PL/pgSQL - SQL手続き言語 | 早送り | 次のページ

# 35.10. トリガプロシージャ

PL/pgSQLはトリガプロシージャの定義に使用できます。 トリガプロシージャは、`CREATE FUNCTION`コマンドを使って、`trigger`という戻り値の型を持った引数のない関数として作成されます。 その関数は、たとえ、`CREATE TRIGGER`にて引数をとるものとしていたとしても、引数を持たないものと宣言しなければなりません。 トリガの引数は、後述する通り、`TG_ARGV`経由で渡されます。

PL/pgSQL関数がトリガとして呼び出された場合、いくつかの特殊な変数が自動的に最上位レベルのブロックで作成されます。 それらを以下に示します。

`NEW`

  `RECORD`データ型。 この変数は行レベルのトリガでの`INSERT`/`UPDATE`操作によって更新された、新しいデータベースの行を保持します。 この変数は文レベルのトリガではNULLです。

`OLD`

  `RECORD`データ型。 この変数は、行レベルのトリガでの`UPDATE`/`DELETE`操作によって更新される前のデータベースの行を保持します。 この変数は文レベルのトリガではNULLです。

`TG_NAME`

  `name`データ型。 実際に発行されたトリガの名前を持つ変数。

`TG_WHEN`

  `text`データ型。 トリガの定義に依存した`BEFORE`または`AFTER`という文字列。

`TG_LEVEL`

  `text`データ型。 トリガの定義に依存した`ROW`または`STATEMENT`という文字列。

`TG_OP`

  `text`データ型。 トリガを発行した操作を示す、`INSERT`, `UPDATE`または`DELETE`という文字列。

`TG_RELID`

  `oid`データ型。 このトリガの呼び出し元になるテーブルのオブジェクトID。

`TG_RELNAME`

  `name`データ型。 このトリガの呼び出し元になるテーブルの名前。

`TG_NARGS`

  `integer`型。 `CREATE TRIGGER`文におけるトリガプロシージャに与えられる引数の数。

`TG_ARGV[]`

  `text`型の配列型。 `CREATE TRIGGER`文での引数。 このインデックスは0から始まります。 無効なインデックス(0未満や`tg_nargs`以上)は、NULL値という結果になります。

トリガ関数はNULLまたは、トリガの発行元になったテーブルの構造を正確に持ったレコード/行を返さなければなりません。

`BEFORE`として発行された行レベルトリガがNULLを返す場合には、トリガマネージャに実際の行への操作を取りやめるように通知します(つまり、その後にトリガが発行されず、その`INSERT`/`UPDATE`/`DELETE`はその行に対して実行されません)。 非NULL値を返す場合には、その操作はその行値で処理されます。 元の`NEW`の値と異なる行値を返すことは、挿入、更新される値を変更することに注意してください。 (しかし、`DELETE`の場合は直接影響しません。) 格納すべき行を変更するために、`NEW`の個々の値を直接置き換え、その`NEW`を返すことも、新しいレコード/行を完全に作成して返すことも可能です。

文レベルの`BEFORE`または`AFTER`トリガ、行レベルの`AFTER`トリガの戻り値は常に無視されます。 NULLとしても構いません。 しかし、これらの種類のトリガでも、エラーを発生させることで操作全体を中断させることが可能です。

例35-1にPL/pgSQLのトリガ関数の例を示します。

例 35-1. PL/pgSQLトリガプロシージャ

このトリガの例では、テーブルの行が挿入または更新された時には必ず、現在のユーザ名と時刻がその行に入っていることを確実にします。 そして、従業員名が与えられていることとその給料が正の値であることを確認します。

    CREATE TABLE emp (
        empname text,
        salary integer,
        last_date timestamp,
        last_user text
    );

    CREATE FUNCTION emp_stamp() RETURNS trigger AS $emp_stamp$
        BEGIN
            -- empname とsalary が与えられていることをチェック
            IF NEW.empname IS NULL THEN
                RAISE EXCEPTION 'empname cannot be null';
            END IF;
            IF NEW.salary IS NULL THEN
                RAISE EXCEPTION '% cannot have null salary', NEW.empname;
            END IF;

            -- 支払い時に問題が起こらないように
            IF NEW.salary < 0 THEN
                RAISE EXCEPTION '% cannot have a negative salary', NEW.empname;
            END IF;

            -- 誰がいつ変更したかを記録
            NEW.last_date := 'now';
            NEW.last_user := current_user;
            RETURN NEW;
        END;
    $emp_stamp$ LANGUAGE plpgsql;

    CREATE TRIGGER emp_stamp BEFORE INSERT OR UPDATE ON emp
        FOR EACH ROW EXECUTE PROCEDURE emp_stamp();

テーブルにおける変更のログを取る他の方法は、挿入、更新または削除の各々に対する行を保有する新テーブルを作成することです。 この方法はテーブルにおける変更の監査と考えることができます。

例35-2は PL/pgSQL による監査用トリガプロシージャの一例を示します。

例 35-2. PL/pgSQLによる 監査用のトリガプロシージャ

このトリガの例では、`emp` テーブルにおける行の挿入、更新または削除のどれもが emp_audit テーブルの中へ確実に記録 (すなわち監査) されます。 現在時刻とユーザ名は、行った操作の種類と共に `emp_audit` の行の中に打刻されます。

    CREATE TABLE emp (
        empname           text NOT NULL,
        salary            integer
    );

    CREATE TABLE emp_audit(
        operation         char(1)   NOT NULL,
        stamp             timestamp NOT NULL,
        userid            text      NOT NULL,
        empname           text      NOT NULL,
        salary integer
    );

    CREATE OR REPLACE FUNCTION process_emp_audit() RETURNS TRIGGER AS $emp_audit$
        BEGIN
            --
    		-- empで行った操作を反映する行をemp_auditに作成
    		-- 操作の種類を決定するために、特殊な変数TG_OPを活用
            --
            IF (TG_OP = 'DELETE') THEN
                INSERT INTO emp_audit SELECT 'D', now(), user, OLD.*;
                RETURN OLD;
            ELSIF (TG_OP = 'UPDATE') THEN
                INSERT INTO emp_audit SELECT 'U', now(), user, NEW.*;
                RETURN NEW;
            ELSIF (TG_OP = 'INSERT') THEN
                INSERT INTO emp_audit SELECT 'I', now(), user, NEW.*;
                RETURN NEW;
            END IF;
            RETURN NULL; -- AFTERトリガですので、結果は無視されます
        END;
    $emp_audit$ language plpgsql;

    CREATE TRIGGER emp_audit
    AFTER INSERT OR UPDATE OR DELETE ON emp
        FOR EACH ROW EXECUTE PROCEDURE process_emp_audit();

トリガの使用目的の 1つは、あるテーブルのサマリテーブル (データを抜粋したテーブル) を維持することです。 サマリテーブルは元のテーブルに代わって、ある種の問い合わせに対して使用でき、しばしば実行時間を大幅に縮小します。 通常この手法はデータウェアハウスに使用され、計測または観測データ (ファクトテーブルといいます) を非常に大きくすることができます。 データウェアハウス内のファクトテーブルに対してサマリテーブルを維持するPL/pgSQLのトリガプロシージャの例を例35-3に示します。

例 35-3. サマリテーブルを維持するための PL/pgSQL トリガプロシージャ

ここに述べるスキーマの一部は Ralph Kimball による The Data Warehouse Toolkit の Grocery Store の例に基づいています。

    --
    -- time dimension と sales fact の主テーブル
    --
    CREATE TABLE time_dimension (
        time_key                    integer NOT NULL,
        day_of_week                 integer NOT NULL,
        day_of_month                integer NOT NULL,
        month                       integer NOT NULL,
        quarter                     integer NOT NULL,
        year                        integer NOT NULL
    );
    CREATE UNIQUE INDEX time_dimension_key ON time_dimension(time_key);

    CREATE TABLE sales_fact (
        time_key                    integer NOT NULL,
        product_key                 integer NOT NULL,
        store_key                   integer NOT NULL,
        amount_sold                 numeric(12,2) NOT NULL,
        units_sold                  integer NOT NULL,
        amount_cost                 numeric(12,2) NOT NULL
    );
    CREATE INDEX sales_fact_time ON sales_fact(time_key);

    --
    -- sales by time のサマリテーブル
    --
    CREATE TABLE sales_summary_bytime (
        time_key                    integer NOT NULL,
        amount_sold                 numeric(15,2) NOT NULL,
        units_sold                  numeric(12) NOT NULL,
        amount_cost                 numeric(15,2) NOT NULL
    );
    CREATE UNIQUE INDEX sales_summary_bytime_key ON sales_summary_bytime(time_key);

    --
    -- 更新、挿入および削除によりサマリテーブルの列を修正する関数とトリガ
    --
    CREATE OR REPLACE FUNCTION maint_sales_summary_bytime() RETURNS TRIGGER AS $maint_sales_summary_bytime$
        DECLARE
            delta_time_key          integer;
            delta_amount_sold       numeric(15,2);
            delta_units_sold        numeric(12);
            delta_amount_cost       numeric(15,2);
        BEGIN

            -- 増加または減少量を算出
            IF (TG_OP = 'DELETE') THEN

                delta_time_key = OLD.time_key;
                delta_amount_sold = -1 * OLD.amount_sold;
                delta_units_sold = -1 * OLD.units_sold;
                delta_amount_cost = -1 * OLD.amount_cost;

            ELSIF (TG_OP = 'UPDATE') THEN

                -- time_key を変更する更新を禁止します
                -- (削除 + 挿入の方法により大部分の変更を行うから
                -- それほど厄介ではありません)。
                IF ( OLD.time_key != NEW.time_key) THEN
                    RAISE EXCEPTION 'Update of time_key : % -> % not allowed', OLD.time_key, NEW.time_key;
                END IF;

                delta_time_key = OLD.time_key;
                delta_amount_sold = NEW.amount_sold - OLD.amount_sold;
                delta_units_sold = NEW.units_sold - OLD.units_sold;
                delta_amount_cost = NEW.amount_cost - OLD.amount_cost;

            ELSIF (TG_OP = 'INSERT') THEN

                delta_time_key = NEW.time_key;
                delta_amount_sold = NEW.amount_sold;
                delta_units_sold = NEW.units_sold;
                delta_amount_cost = NEW.amount_cost;

            END IF;

            -- サマリテーブルの行を新しい値で更新します。
            UPDATE sales_summary_bytime
                SET amount_sold = amount_sold + delta_amount_sold,
                    units_sold = units_sold + delta_units_sold,
                    amount_cost = amount_cost + delta_amount_cost
                WHERE time_key = delta_time_key;

            -- この time_key (すなわち新データ) に対応する行が存在しないことがあります。
            IF (NOT FOUND) THEN
                BEGIN
                    INSERT INTO sales_summary_bytime (
                                time_key,
                                amount_sold,
                                units_sold,
                                amount_cost)
                        VALUES (
                                delta_time_key,
                                delta_amount_sold,
                                delta_units_sold,
                                delta_amount_cost
                               );
                EXCEPTION
                    --
                    -- 新しい time_key に対する 2つのトランザクションが
                    -- データを追加する競合状態を捕捉します。
                    --
                    WHEN UNIQUE_VIOLATION THEN
                        UPDATE sales_summary_bytime
                            SET amount_sold = amount_sold + delta_amount_sold,
                                units_sold = units_sold + delta_units_sold,
                                amount_cost = amount_cost + delta_amount_cost
                            WHERE time_key = delta_time_key;

                END;
            END IF;
            RETURN NULL;

        END;
    $maint_sales_summary_bytime$ LANGUAGE plpgsql;

    CREATE TRIGGER maint_sales_summary_bytime
    AFTER INSERT OR UPDATE OR DELETE ON sales_fact
        FOR EACH ROW EXECUTE PROCEDURE maint_sales_summary_bytime();

前のページ | ホーム | 次のページ
エラーとメッセージ | 上に戻る | Oracle PL/SQLからの移植