様々な基本になるサンプルを記録しています。

不定期更新です。

記事のサイドに使用している商品の紹介も掲載しているので、良ければご覧ください。

【Oracle】トリガーでログを記録する為のテーブルとトリガーのプロシージャーの作成

ダミーのようなテーブルをあらかじめ作成して
テーブルにデータが追加、更新、削除が行われたとき、
そこに変更ログを記録するようなことができると、
更新以前の状況の更新過程が分かるようになるので便利かもしれない。

そんなダミーテーブルの作成と、更新を記録するトリガーを作成する為の
プロシージャーを載せた。
ただ、このプロシージャーは、対象にするテーブルに主キーを設定しておかないと、作られないようにしてある。
適当にログテーブルを作って、人的にテーブルスペースを圧迫してほしくないので、
主キーのあるテーブルのみ有効ということにしておいた。

まずは、トリガーの自動作成プロシージャー。

CREATE OR REPLACE PROCEDURE CREATE_LOG_TRIGGER (
    STR_TABLE   IN VARCHAR2,
    OUT_STS     OUT VARCHAR2,
    OUT_MSG     OUT VARCHAR2
) AS
    CURSOR REC_CUR IS SELECT
        TABLE_NAME
                      FROM
        USER_TABLES
                      WHERE
        TABLE_NAME   = STR_TABLE;
    TYPE REC_TYPE IS
        TABLE OF REC_CUR%ROWTYPE;
    CURSOR REC_CONS IS SELECT
        COLUMN_NAME
                       FROM
        USER_CONS_COLUMNS
                       WHERE
        TABLE_NAME   = STR_TABLE AND
        CONSTRAINT_NAME IN (
            SELECT
                CONSTRAINT_NAME
            FROM
                USER_CONSTRAINTS
            WHERE
                TABLE_NAME        = STR_TABLE AND
                CONSTRAINT_TYPE   = 'P'
        );
    TYPE REC_TYPE2 IS
        TABLE OF REC_CONS%ROWTYPE;
    CURSOR REC_COLUMNS IS SELECT
        COLUMN_NAME
                          FROM
        USER_TAB_COLUMNS
                          WHERE
        TABLE_NAME   = STR_TABLE;
    TYPE REC_TYPE3 IS
        TABLE OF REC_COLUMNS%ROWTYPE;
    B_WRET_REC     REC_TYPE;
    A_WRET_REC     REC_TYPE2;
    C_WRET_REC     REC_TYPE3;
    W_COUNT        NUMBER(10);
    TRG_NAME       VARCHAR2(100);
    LOG_TBL_NAME   VARCHAR2(100);
    STRSQL         CLOB;
    W_CRLF         VARCHAR2(4) := CHR(13)
                             || CHR(10);
BEGIN
    OUT_STS        := ' ';
    OUT_MSG        := ' ';
    LOG_TBL_NAME   := STR_TABLE || '_LOG';
    TRG_NAME       := STR_TABLE || '_LOG_TRG';
    OPEN REC_COLUMNS;
    FETCH REC_COLUMNS BULK COLLECT INTO C_WRET_REC;
    CLOSE REC_COLUMNS;
    OPEN REC_CUR;
    FETCH REC_CUR BULK COLLECT INTO B_WRET_REC;
    CLOSE REC_CUR;
    OPEN REC_CONS;
    FETCH REC_CONS BULK COLLECT INTO A_WRET_REC LIMIT 100;
    CLOSE REC_CONS;  

    /*主キーがない場合は作らない*/
    IF
        A_WRET_REC.COUNT = 0
    THEN
        OUT_STS   := 'NONE';
        OUT_MSG   := '主キーなし';
        RETURN;
    END IF;
    STRSQL         := 'CREATE OR REPLACE  TRIGGER '
              || TRG_NAME
              || ' '
              || W_CRLF
              || 'AFTER INSERT OR DELETE OR UPDATE ON '
              || STR_TABLE
              || ' '
              || W_CRLF
              || 'FOR EACH ROW'
              || W_CRLF
              || W_CRLF
              || 'DECLARE'
              || W_CRLF
              || W_CRLF
              || 'W_REC '
              || LOG_TBL_NAME
              || '%ROWTYPE; '
              || W_CRLF
              || 'W_REC_OLD '
              || LOG_TBL_NAME
              || '%ROWTYPE; '
              || W_CRLF
              || 'W_INS_FLG VARCHAR2(50); '
              || W_CRLF
              || W_CRLF
              || '--'
              || STR_TABLE
              || '更新ログ出力トリガー:'
              || TRG_NAME
              || W_CRLF
              || '--※このトリガーは自動生成されています。'
              || W_CRLF
              || '--  トリガー名を絶対に変更しないこと!!'
              || W_CRLF
              || W_CRLF

  /*行トリガー作成*/
              || '--行トリガー'
              || W_CRLF;
    STRSQL         := STRSQL
              || 'BEGIN'
              || W_CRLF
              || W_CRLF
              || 'IF (INSERTING) THEN '
              || W_CRLF
              || W_CRLF
              || 'W_INS_FLG := ''INSERT'';'
              || W_CRLF
              || W_CRLF
              || 'END IF; '
              || W_CRLF
              || W_CRLF
              || 'IF (UPDATING) THEN '
              || W_CRLF
              || W_CRLF
              || 'W_INS_FLG := ''UPDATE'';'
              || W_CRLF
              || W_CRLF
              || 'END IF; '
              || W_CRLF
              || W_CRLF
              || 'IF (DELETING) THEN '
              || W_CRLF
              || W_CRLF
              || 'W_INS_FLG := ''DELETE'';'
              || W_CRLF
              || W_CRLF
              || 'END IF; '
              || W_CRLF
              || W_CRLF
              || W_CRLF;
    FOR I IN 1..C_WRET_REC.COUNT LOOP
        STRSQL   := STRSQL
                  || 'W_REC.'
                  || C_WRET_REC(I).COLUMN_NAME
                  || ' := '
                  || ':NEW.'
                  || C_WRET_REC(I).COLUMN_NAME
                  || ';'
                  || W_CRLF;
    END LOOP;
    STRSQL         := STRSQL
              || W_CRLF
              || W_CRLF;
    FOR I IN 1..C_WRET_REC.COUNT LOOP
        STRSQL   := STRSQL
                  || 'W_REC_OLD.'
                  || C_WRET_REC(I).COLUMN_NAME
                  || ' := '
                  || ':OLD.'
                  || C_WRET_REC(I).COLUMN_NAME
                  || ';'
                  || W_CRLF;
    END LOOP;
    STRSQL         := STRSQL
              || W_CRLF
              || W_CRLF
              || 'IF W_INS_FLG = ''DELETE'' THEN '
              || W_CRLF
              || W_CRLF
              || 'W_REC_OLD.LOG_ID    :=TO_CHAR(SYSTIMESTAMP,''yyyymmddhh24missff3'') || ''-'' || DBMS_RANDOM.STRING(''U'',15);'
              || W_CRLF
              || 'W_REC_OLD.INS_FLG   :=W_INS_FLG;'
              || W_CRLF
              || 'W_REC_OLD.INS_DATE  :=TO_CHAR(SYSDATE,''YYYYMMDD'');'
              || W_CRLF
              || 'W_REC_OLD.INS_TIME  :=TO_CHAR(SYSTIMESTAMP,''YYYY/MM/DD HH24;MI:SS.ff3'');'
              || W_CRLF
              || 'INSERT INTO '
              || LOG_TBL_NAME
              || ' VALUES W_REC_OLD;'
              || W_CRLF
              || W_CRLF
              || 'ELSE '
              || W_CRLF
              || W_CRLF
              || 'W_REC.LOG_ID    :=TO_CHAR(SYSTIMESTAMP,''yyyymmddhh24missff3'')  || ''-'' || DBMS_RANDOM.STRING(''U'',15);'
              || W_CRLF
              || 'W_REC.INS_FLG   :=W_INS_FLG;'
              || W_CRLF
              || 'W_REC.INS_DATE  :=TO_CHAR(SYSDATE,''YYYYMMDD'');'
              || W_CRLF
              || 'W_REC.INS_TIME  :=TO_CHAR(SYSTIMESTAMP,''YYYY/MM/DD HH24;MI:SS.ff3'');'
              || W_CRLF
              || 'INSERT INTO '
              || LOG_TBL_NAME
              || ' VALUES W_REC;'
              || W_CRLF
              || W_CRLF;
    STRSQL         := STRSQL
              || 'END IF; '
              || W_CRLF
              || W_CRLF;

  /*トリガー末尾*/
    STRSQL         := STRSQL
              || W_CRLF
              || 'END '
              || TRG_NAME
              || ';';
    EXECUTE IMMEDIATE STRSQL;
    OUT_STS        := 'OK';
END CREATE_LOG_TRIGGER;

これ単体で実行しても意味がないので、
上記のプロシージャーがコンパイルされた上で、以下を実行する。



以下は、ダミーテーブルと上記のCREATE_LOG_TRIGGERを
実行するプロシージャー。
もちろんユーザーにトリガーとテーブルを作る権限がないと動かない。

CREATE OR REPLACE PROCEDURE LOG_TABLE_CREATE (
    STR_TABLE   IN VARCHAR2,
    OP_FLG      IN NUMBER
       /*0:作成  1:削除*/
   ,
    OUT_STS     OUT VARCHAR2,
    OUT_MSG     OUT VARCHAR2
) AS
    CURSOR REC_CUR IS SELECT
        TABLE_NAME
                      FROM
        USER_TABLES
                      WHERE
        TABLE_NAME NOT LIKE '%_LOG' AND
        TABLE_NAME LIKE
            CASE
                NVL(STR_TABLE,' ')
                WHEN
                    ' '
                THEN
                    TABLE_NAME
                ELSE
                    STR_TABLE
            END;
    TYPE REC_TYPE IS
        TABLE OF REC_CUR%ROWTYPE;

/*プライマリーキーを探す*/
    CURSOR REC_CONS (
        TB_NAME VARCHAR2
    ) IS SELECT
        COLUMN_NAME
         FROM
        USER_CONS_COLUMNS
         WHERE
        TABLE_NAME   = TB_NAME AND
        CONSTRAINT_NAME IN (
            SELECT
                CONSTRAINT_NAME
            FROM
                USER_CONSTRAINTS
            WHERE
                TABLE_NAME        = TB_NAME AND
                CONSTRAINT_TYPE   = 'P'
        );
    TYPE REC_TYPE3 IS
        TABLE OF REC_CONS%ROWTYPE;
    B_WRET_REC       REC_TYPE;
    C_WRET_REC       REC_TYPE3;
    W_COUNT          NUMBER(10);
    LOG_TABLE_NAME   VARCHAR2(100);
    LOG_PKEY_NAME    VARCHAR2(100);
    TRG_NAME         VARCHAR2(100);
BEGIN
    OUT_STS   := ' ';
    OUT_MSG   := ' ';
    OPEN REC_CUR;
    LOOP
        FETCH REC_CUR BULK COLLECT INTO B_WRET_REC LIMIT 100;
        EXIT WHEN
            B_WRET_REC.COUNT = 0;
        FOR I IN 1..B_WRET_REC.COUNT LOOP
            LOG_TABLE_NAME   := B_WRET_REC(I).TABLE_NAME
                              || '_LOG';
            TRG_NAME         := B_WRET_REC(I).TABLE_NAME
                        || '_LOG_TRG';
            LOG_PKEY_NAME    := ' ';
            W_COUNT          := 0;
      /*テーブルが見つからなければ削除処理はしない*/
            SELECT
                COUNT(*)
            INTO W_COUNT
            FROM
                USER_TABLES
            WHERE
                TABLE_NAME   = LOG_TABLE_NAME;
            IF
                W_COUNT > 0
            THEN

             /*トリガーの存在を確認*/
                W_COUNT   := 0;
                SELECT
                    COUNT(TRIGGER_NAME)
                INTO W_COUNT
                FROM
                    USER_TRIGGERS
                WHERE
                    TRIGGER_NAME   = TRG_NAME;

             /*存在しているのであればトリガーを削除する*/
                IF
                    W_COUNT > 0
                THEN

                 /*トリガー削除*/
                    EXECUTE IMMEDIATE 'DROP TRIGGER ' || TRG_NAME;
                END IF;

             /*プライマリーキーがあるか確認*/
                BEGIN
                    SELECT
                        CONSTRAINT_NAME
                    INTO LOG_PKEY_NAME
                    FROM
                        USER_CONSTRAINTS
                    WHERE
                        TABLE_NAME        = LOG_TABLE_NAME AND
                        CONSTRAINT_TYPE   = 'P';
                EXCEPTION
                    WHEN OTHERS THEN
                        LOG_PKEY_NAME   := ' ';
                END;
                IF
                    LOG_PKEY_NAME <> ' '
                THEN

                /*プライマリーキー削除*/
                    EXECUTE IMMEDIATE 'ALTER TABLE '
                                      || LOG_TABLE_NAME
                                      || ' DROP CONSTRAINT '
                                      || LOG_PKEY_NAME;
                END IF;

             /*テーブル削除*/
                EXECUTE IMMEDIATE 'DROP TABLE ' || LOG_TABLE_NAME;
            END IF;
            IF
                OP_FLG = 0
            THEN


          /*元テーブルの主キーがあるかチェック*/
                C_WRET_REC   := REC_TYPE3 ();
                OPEN REC_CONS(B_WRET_REC(I).TABLE_NAME);
                FETCH REC_CONS BULK COLLECT INTO C_WRET_REC;
                CLOSE REC_CONS;      

          /*主キーがなければログテーブルの作成はしない*/
                IF
                    C_WRET_REC.COUNT > 0
                THEN

	          /*テーブル作成*/
                    EXECUTE IMMEDIATE 'CREATE TABLE '
                                      || LOG_TABLE_NAME
                                      || ' AS SELECT * FROM '
                                      || B_WRET_REC(I).TABLE_NAME
                                      || ' WHERE ROWNUM = 0';

	          /*カラム追加*/
                    EXECUTE IMMEDIATE 'ALTER TABLE '
                                      || LOG_TABLE_NAME
                                      || ' ADD ( '
                                      || 'LOG_ID   VARCHAR2(50 byte) NOT NULL,'
                                      || 'INS_FLG  VARCHAR2(50 byte) NOT NULL,'
                                      || 'INS_DATE VARCHAR2(50 byte) NOT NULL,'
                                      || 'INS_TIME VARCHAR2(50 byte) NOT NULL'
                                      || ') ';
	          /*主キー追加                  */
                    EXECUTE IMMEDIATE 'ALTER TABLE '
                                      || LOG_TABLE_NAME
                                      || ' ADD '
                                      || 'CONSTRAINT '
                                      || LOG_TABLE_NAME
                                      || '_PKEY '
                                      || 'PRIMARY KEY (LOG_ID) ';

	          /*トリガー作成*/
                    CREATE_LOG_TRIGGER(B_WRET_REC(I).TABLE_NAME,OUT_STS,OUT_MSG);
                END IF;
            END IF;
        END LOOP;
    END LOOP;
    CLOSE REC_CUR;
END LOG_TABLE_CREATE;


これをSQLDEVELOPERのデバッグサンプルのSQLを引用して実行してみる。
と、その前に作成対象のテーブルにプライマリキーを貼る。
f:id:karinto441:20181009000733p:plain

ちなみに使ったサンプルのテーブルのスクリプトは以下の記事にあるので
使いたければどうぞ。
fubukin.hatenablog.com


さて、では以下を実行してみる。

DECLARE
  STR_TABLE VARCHAR2(200);
  OP_FLG NUMBER;
  OUT_STS VARCHAR2(200);
  OUT_MSG VARCHAR2(200);
BEGIN
  STR_TABLE := 'TABLE1';
  OP_FLG := 0;

  LOG_TABLE_CREATE(
    STR_TABLE => STR_TABLE,
    OP_FLG => OP_FLG,
    OUT_STS => OUT_STS,
    OUT_MSG => OUT_MSG
  );
  /* Legacy output: 
DBMS_OUTPUT.PUT_LINE('OUT_STS = ' || OUT_STS);
*/ 
  :OUT_STS := OUT_STS;
  /* Legacy output: 
DBMS_OUTPUT.PUT_LINE('OUT_MSG = ' || OUT_MSG);
*/ 
  :OUT_MSG := OUT_MSG;
--rollback; 
END;

左が更新したらログを出力するテーブル、右がログを出力するテーブル
f:id:karinto441:20181009001311p:plain


そして、自動作成されたトリガーは以下になる。

CREATE OR REPLACE TRIGGER TABLE1_LOG_TRG AFTER
    INSERT OR DELETE OR UPDATE ON TABLE1
    FOR EACH ROW
DECLARE
    W_REC       TABLE1_LOG%ROWTYPE;
    W_REC_OLD   TABLE1_LOG%ROWTYPE;
    W_INS_FLG   VARCHAR2(50); 

--TABLE1更新ログ出力トリガー:TABLE1_LOG_TRG
--※このトリガーは自動生成されています。
--  トリガー名を絶対に変更しないこと!!

--行トリガー
BEGIN

    IF
        ( INSERTING )
    THEN
        W_INS_FLG := 'INSERT';
    END IF;
    IF
        ( UPDATING )
    THEN
        W_INS_FLG := 'UPDATE';
    END IF;
    IF
        ( DELETING )
    THEN
        W_INS_FLG := 'DELETE';
    END IF;
    
    W_REC.A1 :=:NEW.A1;
    W_REC.A2 :=:NEW.A2;
    W_REC.A3 :=:NEW.A3;
    W_REC.A4 :=:NEW.A4;
    W_REC.A5 :=:NEW.A5;
    W_REC.A6 :=:NEW.A6;
    W_REC.A7 :=:NEW.A7;
    W_REC.A8 :=:NEW.A8;
    W_REC.A9 :=:NEW.A9;
    W_REC.A10 :=:NEW.A10;
    W_REC.A11 :=:NEW.A11;
    W_REC.A12 :=:NEW.A12;
    W_REC_OLD.A1 :=:OLD.A1;
    W_REC_OLD.A2 :=:OLD.A2;
    W_REC_OLD.A3 :=:OLD.A3;
    W_REC_OLD.A4 :=:OLD.A4;
    W_REC_OLD.A5 :=:OLD.A5;
    W_REC_OLD.A6 :=:OLD.A6;
    W_REC_OLD.A7 :=:OLD.A7;
    W_REC_OLD.A8 :=:OLD.A8;
    W_REC_OLD.A9 :=:OLD.A9;
    W_REC_OLD.A10 :=:OLD.A10;
    W_REC_OLD.A11 :=:OLD.A11;
    W_REC_OLD.A12 :=:OLD.A12;
    
    IF
        W_INS_FLG = 'DELETE'
    THEN
        W_REC_OLD.LOG_ID := TO_CHAR(SYSTIMESTAMP,'yyyymmddhh24missff3')
                            || '-'
                            || DBMS_RANDOM.STRING('U',15);

        W_REC_OLD.INS_FLG := W_INS_FLG;
        W_REC_OLD.INS_DATE := TO_CHAR(SYSDATE,'YYYYMMDD');
        W_REC_OLD.INS_TIME := TO_CHAR(SYSTIMESTAMP,'YYYY/MM/DD HH24;MI:SS.ff3');
        INSERT INTO TABLE1_LOG VALUES W_REC_OLD;

    ELSE
        W_REC.LOG_ID := TO_CHAR(SYSTIMESTAMP,'yyyymmddhh24missff3')
                        || '-'
                        || DBMS_RANDOM.STRING('U',15);

        W_REC.INS_FLG := W_INS_FLG;
        W_REC.INS_DATE := TO_CHAR(SYSDATE,'YYYYMMDD');
        W_REC.INS_TIME := TO_CHAR(SYSTIMESTAMP,'YYYY/MM/DD HH24;MI:SS.ff3');
        INSERT INTO TABLE1_LOG VALUES W_REC;

    END IF;

END TABLE1_LOG_TRG;

自動生成は作ってみると後々、手動で作る手間も時間も省けるから楽だね。