カナダのInsum SolutionsがAPEX Instant Tipsというタイトルで、毎週金曜日にちょっとした小技を紹介しています。ちょうど最近"Instantly" add full table audit capability to your applicationsとして表の変更履歴を保存する方法を紹介していました。
SQL Developerを使用してジャーナル表(つまり履歴表)のDDLを生成しています。残念なことに、ライブデモでデータ・ディクショナリのインポートが成功しなかったため、ビデオを視聴しても実際の操作がわかりにくくなっています。
以下より、このビデオで触れられている、表の履歴を保存する3の方法を紹介してみます。ひとつはこのビデオの本題であるSQL Developerを使った方法、もうひとつはクイックSQLを使った方法、最後にこのビデオの視聴者からのコメントにあった、Flashback Data Archiveを使った方法です。
以下の準備作業が既に行われていることを前提とします。ワークスペース名や表EMP以外のジャーナル表を作成したい場合は、それぞれ作業内容を読み替えてください。
- Always FreeのAutonomous Transaction Processingのインスタンスがある
- APEXのワークスペースとしてAPEXDEVが作成されている
- サンプル・データセットのEMP/DEPTがインストールされている
SQL Developerを使ってジャーナル表を作成する
接続名を選択し、次へ進みます。
ジャーナル表を作成したい表を含むスキーマを選択します。今回の例では表EMPを含んでいるのはスキーマAPEXDEVなので、APEXDEVにチェックを入れています。次へ進みます。
ジャーナル表の生成対象となる表を選択します。今回の例ではEMPです。次へ進みます。
終了をクリックし、表EMPの定義をデータ・モデラーへインポートします。
インポートのログを保存するかどうか確認されます。今回は不要なので、閉じるをクリックします。
表EMPの定義がインポートされました。
続けて、たった今インポートした表EMPの定義をエクスポートします。エクスポートする際に、ジャーナル表とそれを操作するトリガーのDDLを生成します。
ファイル・メニューより、Data Modeler、エクスポート、DDLファイルを呼び出します。
ダイアログが開くので、生成をクリックします。
表のDDLスクリプトを含めるのタブを開き、対象となる表(この例ではAPEXDEV.EMP)にチェックを入れます。OKをクリックします。
ここでアクティブなスクリプト・セットとしてjournal tablesが選択されています。この指定により、ジャーナル表とトリガーのDDLが生成されます。
DDLが生成され、画面に表示されます。表名の末尾に_JNが付加された表がジャーナル表、_JNtrgが付加されたトリガーがジャーナル表へ表EMPの変更を書き込むトリガーになります。
生成されたDDLスクリプトをファイルに保存するか、必要な部分をコピー&ペーストして保存します。
以下が生成されたDDLの例です。
この中でCREATE TABLE APEXDEV.EMP_JNのDDLとCREATE OR REPLACE TRIGGER APEXDEV.EMP_JNtrgがジャーナル表に関係します。
-- 生成者 Oracle SQL Developer Data Modeler 21.2.0.165.1515 | |
-- 時刻: 2021-08-30 11:00:48 JST | |
-- サイト: Oracle Database 12cR2 | |
-- タイプ: Oracle Database 12cR2 | |
CREATE TABLESPACE data | |
-- WARNING: Tablespace has no data files defined | |
LOGGING ONLINE | |
EXTENT MANAGEMENT LOCAL AUTOALLOCATE | |
FLASHBACK ON; | |
CREATE USER apexdev IDENTIFIED BY ACCOUNT UNLOCK ; | |
-- predefined type, no DDL - MDSYS.SDO_GEOMETRY | |
-- predefined type, no DDL - XMLTYPE | |
CREATE TABLE apexdev.emp ( | |
empno NUMBER(4) NOT NULL, | |
ename VARCHAR2(50 BYTE), | |
job VARCHAR2(50 BYTE), | |
mgr NUMBER(4), | |
hiredate DATE, | |
sal NUMBER(7, 2), | |
comm NUMBER(7, 2), | |
deptno NUMBER(2) | |
) | |
PCTFREE 10 PCTUSED 40 INITRANS 10 TABLESPACE data LOGGING | |
STORAGE ( INITIAL 65536 NEXT 1048576 PCTINCREASE 0 MINEXTENTS 1 MAXEXTENTS 2147483645 FREELISTS 1 FREELIST GROUPS 1 BUFFER_POOL DEFAULT ) | |
NO INMEMORY; | |
CREATE INDEX apexdev.emp_1 ON | |
apexdev.emp ( | |
mgr | |
ASC ) | |
TABLESPACE data PCTFREE 10 INITRANS 20 | |
STORAGE ( INITIAL 65536 NEXT 1048576 PCTINCREASE 0 MINEXTENTS 1 MAXEXTENTS 2147483645 FREELISTS 1 FREELIST GROUPS 1 BUFFER_POOL | |
DEFAULT ) | |
LOGGING; | |
CREATE INDEX apexdev.emp_2 ON | |
apexdev.emp ( | |
deptno | |
ASC ) | |
TABLESPACE data PCTFREE 10 INITRANS 20 | |
STORAGE ( INITIAL 65536 NEXT 1048576 PCTINCREASE 0 MINEXTENTS 1 MAXEXTENTS 2147483645 FREELISTS 1 FREELIST GROUPS 1 BUFFER_POOL | |
DEFAULT ) | |
LOGGING; | |
CREATE UNIQUE INDEX apexdev.emp_pk ON | |
apexdev.emp ( | |
empno | |
ASC ) | |
TABLESPACE data PCTFREE 10 INITRANS 20 | |
STORAGE ( INITIAL 65536 NEXT 1048576 PCTINCREASE 0 MINEXTENTS 1 MAXEXTENTS 2147483645 FREELISTS 1 FREELIST GROUPS 1 BUFFER_POOL | |
DEFAULT ) | |
LOGGING; | |
ALTER TABLE apexdev.emp | |
ADD CONSTRAINT emp_pk PRIMARY KEY ( empno ) | |
USING INDEX apexdev.emp_pk; | |
CREATE TABLE APEXDEV.EMP_JN | |
(JN_OPERATION CHAR(3) NOT NULL | |
,JN_ORACLE_USER VARCHAR2(30) NOT NULL | |
,JN_DATETIME DATE NOT NULL | |
,JN_NOTES VARCHAR2(240) | |
,JN_APPLN VARCHAR2(35) | |
,JN_SESSION NUMBER(38) | |
,EMPNO NUMBER (4) NOT NULL | |
,ENAME VARCHAR2 (50 BYTE) | |
,JOB VARCHAR2 (50 BYTE) | |
,MGR NUMBER (4) | |
,HIREDATE DATE | |
,SAL NUMBER (7,2) | |
,COMM NUMBER (7,2) | |
,DEPTNO NUMBER (2) | |
); | |
CREATE OR REPLACE TRIGGER APEXDEV.EMP_JNtrg | |
AFTER | |
INSERT OR | |
UPDATE OR | |
DELETE ON APEXDEV.EMP for each row | |
Declare | |
rec APEXDEV.EMP_JN%ROWTYPE; | |
blank APEXDEV.EMP_JN%ROWTYPE; | |
BEGIN | |
rec := blank; | |
IF INSERTING OR UPDATING THEN | |
rec.EMPNO := :NEW.EMPNO; | |
rec.ENAME := :NEW.ENAME; | |
rec.JOB := :NEW.JOB; | |
rec.MGR := :NEW.MGR; | |
rec.HIREDATE := :NEW.HIREDATE; | |
rec.SAL := :NEW.SAL; | |
rec.COMM := :NEW.COMM; | |
rec.DEPTNO := :NEW.DEPTNO; | |
rec.JN_DATETIME := SYSDATE; | |
rec.JN_ORACLE_USER := SYS_CONTEXT ('USERENV', 'SESSION_USER'); | |
rec.JN_APPLN := SYS_CONTEXT ('USERENV', 'MODULE'); | |
rec.JN_SESSION := SYS_CONTEXT ('USERENV', 'SESSIONID'); | |
IF INSERTING THEN | |
rec.JN_OPERATION := 'INS'; | |
ELSIF UPDATING THEN | |
rec.JN_OPERATION := 'UPD'; | |
END IF; | |
ELSIF DELETING THEN | |
rec.EMPNO := :OLD.EMPNO; | |
rec.ENAME := :OLD.ENAME; | |
rec.JOB := :OLD.JOB; | |
rec.MGR := :OLD.MGR; | |
rec.HIREDATE := :OLD.HIREDATE; | |
rec.SAL := :OLD.SAL; | |
rec.COMM := :OLD.COMM; | |
rec.DEPTNO := :OLD.DEPTNO; | |
rec.JN_DATETIME := SYSDATE; | |
rec.JN_ORACLE_USER := SYS_CONTEXT ('USERENV', 'SESSION_USER'); | |
rec.JN_APPLN := SYS_CONTEXT ('USERENV', 'MODULE'); | |
rec.JN_SESSION := SYS_CONTEXT ('USERENV', 'SESSIONID'); | |
rec.JN_OPERATION := 'DEL'; | |
END IF; | |
INSERT into APEXDEV.EMP_JN VALUES rec; | |
END; | |
/ALTER TABLE apexdev.emp | |
ADD CONSTRAINT emp_dept_fk FOREIGN KEY ( deptno ) | |
REFERENCES apexdev.dept ( deptno ) | |
NOT DEFERRABLE; | |
ALTER TABLE apexdev.emp | |
ADD CONSTRAINT emp_mgr_fk FOREIGN KEY ( mgr ) | |
REFERENCES apexdev.emp ( empno ) | |
NOT DEFERRABLE; | |
CREATE SEQUENCE apexdev.emp_seq START WITH 1 NOCACHE ORDER; | |
CREATE OR REPLACE TRIGGER apexdev.emp_trg1 BEFORE | |
INSERT ON apexdev.emp | |
FOR EACH ROW | |
WHEN ( new.empno IS NULL ) | |
BEGIN | |
:new.empno := apexdev.emp_seq.nextval; | |
END; | |
/ | |
-- Oracle SQL Developer Data Modelerサマリー・レポート: | |
-- | |
-- CREATE TABLE 1 | |
-- CREATE INDEX 3 | |
-- ALTER TABLE 3 | |
-- CREATE VIEW 0 | |
-- ALTER VIEW 0 | |
-- CREATE PACKAGE 0 | |
-- CREATE PACKAGE BODY 0 | |
-- CREATE PROCEDURE 0 | |
-- CREATE FUNCTION 0 | |
-- CREATE TRIGGER 1 | |
-- ALTER TRIGGER 0 | |
-- CREATE COLLECTION TYPE 0 | |
-- CREATE STRUCTURED TYPE 0 | |
-- CREATE STRUCTURED TYPE BODY 0 | |
-- CREATE CLUSTER 0 | |
-- CREATE CONTEXT 0 | |
-- CREATE DATABASE 0 | |
-- CREATE DIMENSION 0 | |
-- CREATE DIRECTORY 0 | |
-- CREATE DISK GROUP 0 | |
-- CREATE ROLE 0 | |
-- CREATE ROLLBACK SEGMENT 0 | |
-- CREATE SEQUENCE 1 | |
-- CREATE MATERIALIZED VIEW 0 | |
-- CREATE MATERIALIZED VIEW LOG 0 | |
-- CREATE SYNONYM 0 | |
-- CREATE TABLESPACE 1 | |
-- CREATE USER 1 | |
-- | |
-- DROP TABLESPACE 0 | |
-- DROP DATABASE 0 | |
-- | |
-- REDACTION POLICY 0 | |
-- | |
-- ORDS DROP SCHEMA 0 | |
-- ORDS ENABLE SCHEMA 0 | |
-- ORDS ENABLE OBJECT 0 | |
-- | |
-- ERRORS 0 | |
-- WARNINGS 1 |
データベース・アクションのSQLまたはSQLワークショップのSQLコマンドから、CREATE TABLE文およびCREATE TRIGGER文を実行すると、ジャーナル表およびジャーナル表への変更の書き込みが行われるようになります。
CREATE TABLE文の実行です。
CREATE OR REPLACE TRIGGER文の実行です。
クイックSQLを使って履歴表を作成する
7.1.8 History Table in Quick SQL Desupported
Quick SQL no longer generates a history table nor a trigger to log table data changes.
# genpk: false
# semantics: default
emp /history
empno num
ename vc50
job vc50
mgr num
hiredate date
sal num
comm num
deptno num
-- create tables | |
create table emp ( | |
empno number, | |
ename varchar2(50), | |
job varchar2(50), | |
mgr number, | |
hiredate date, | |
sal number, | |
comm number, | |
deptno number | |
) | |
; | |
-- history tracking | |
create sequence history_seq; | |
create table history ( | |
id number primary key, | |
table_name varchar2(128), | |
column_name varchar2(128), | |
action varchar2(1) check (action in ('I','U','D')), | |
action_date date, | |
action_by varchar2(255), | |
data_type varchar2(255), | |
pk1 number, | |
tab_row_version integer, | |
old_vc varchar2(4000), | |
new_vc varchar2(4000), | |
old_number number, | |
new_number number, | |
old_date date, | |
new_date date, | |
old_ts timestamp, | |
new_ts timestamp, | |
old_tswtz timestamp with time zone, | |
new_tswtz timestamp with time zone, | |
old_tswltz timestamp with local time zone, | |
new_tswltz timestamp with local time zone, | |
old_clob clob, | |
new_clob clob, | |
old_blob blob, | |
new_blob blob | |
) | |
/ | |
create index history_idx1 on history (pk1); | |
create index history_idx2 on history (table_name, column_name); | |
create or replace view history_v as | |
select id, | |
table_name, | |
column_name, | |
decode(action,'U','Update','D','Delete') action, | |
action_date, | |
action_by, | |
pk1 table_primary_key, | |
tab_row_version table_row_version, | |
decode(data_type, | |
'NUMBER',old_number||' > '||new_number, | |
'VARCHAR2',substr(old_vc,1,50)||' > '||substr(new_vc,1,50), | |
'DATE',to_char(old_date,'YYYYMMDD HH24:MI:SS')||' > '||to_char(new_date,'YYYYMMDD HH24:MI:SS'), | |
'TIMESTAMP',to_char(old_ts,'YYYYMMDD HH24:MI:SS')||' > '||to_char(new_ts,'YYYYMMDD HH24:MI:SS'), | |
'TIMESTAMP WITH TIMEZONE',to_char(old_tswtz,'YYYYMMDD HH24:MI:SS')||' > '||to_char(new_tswtz,'YYYYMMDD HH24:MI:SS'), | |
'TIMESTAMP WITH LOCAL TIMEZONE',to_char(old_tswltz,'YYYYMMDD HH24:MI:SS')||' > '||to_char(new_tswltz,'YYYYMMDD HH24:MI:SS'), | |
'BLOB','length '||sys.dbms_lob.getlength(old_blob)||' > '||' length '||sys.dbms_lob.getlength(new_blob), | |
'CLOB',sys.dbms_lob.substr(old_vc,50,1)||' > '||sys.dbms_lob.substr(new_vc,50,1) | |
) change | |
from history | |
/ | |
create or replace trigger emp_aud | |
after update or delete on emp | |
for each row | |
declare | |
t varchar2(128) := 'EMP'; | |
u varchar2(128) := nvl(sys_context('APEX$SESSION','APP_USER'),user); | |
begin | |
if updating then | |
if (:old.empno is null and :new.empno is not null) or | |
(:old.empno is not null and :new.empno is null) or | |
:old.empno != :new.empno then | |
insert into history ( | |
id, table_name, column_name, pk1, tab_row_version, action, action_date, action_by, data_type, old_number, new_number | |
) values ( | |
history_seq.nextval, t, 'EMPNO', :old.id, null, 'U', sysdate, u, 'NUMBER', :old.empno, :new.empno); | |
end if; | |
if (:old.ename is null and :new.ename is not null) or | |
(:old.ename is not null and :new.ename is null) or | |
:old.ename != :new.ename then | |
insert into history ( | |
id, table_name, column_name, pk1, tab_row_version, action, action_date, action_by, data_type, old_vc, new_vc | |
) values ( | |
history_seq.nextval, t, 'ENAME', :old.id, null, 'U', sysdate, u, 'VARCHAR2', :old.ename, :new.ename); | |
end if; | |
if (:old.job is null and :new.job is not null) or | |
(:old.job is not null and :new.job is null) or | |
:old.job != :new.job then | |
insert into history ( | |
id, table_name, column_name, pk1, tab_row_version, action, action_date, action_by, data_type, old_vc, new_vc | |
) values ( | |
history_seq.nextval, t, 'JOB', :old.id, null, 'U', sysdate, u, 'VARCHAR2', :old.job, :new.job); | |
end if; | |
if (:old.mgr is null and :new.mgr is not null) or | |
(:old.mgr is not null and :new.mgr is null) or | |
:old.mgr != :new.mgr then | |
insert into history ( | |
id, table_name, column_name, pk1, tab_row_version, action, action_date, action_by, data_type, old_number, new_number | |
) values ( | |
history_seq.nextval, t, 'MGR', :old.id, null, 'U', sysdate, u, 'NUMBER', :old.mgr, :new.mgr); | |
end if; | |
if (:old.hiredate is null and :new.hiredate is not null) or | |
(:old.hiredate is not null and :new.hiredate is null) or | |
:old.hiredate != :new.hiredate then | |
insert into history ( | |
id, table_name, column_name, pk1, tab_row_version, action, action_date, action_by, data_type, old_date, new_date | |
) values ( | |
history_seq.nextval, t, 'HIREDATE', :old.id, null, 'U', sysdate, u, 'DATE', :old.hiredate, :new.hiredate); | |
end if; | |
if (:old.sal is null and :new.sal is not null) or | |
(:old.sal is not null and :new.sal is null) or | |
:old.sal != :new.sal then | |
insert into history ( | |
id, table_name, column_name, pk1, tab_row_version, action, action_date, action_by, data_type, old_number, new_number | |
) values ( | |
history_seq.nextval, t, 'SAL', :old.id, null, 'U', sysdate, u, 'NUMBER', :old.sal, :new.sal); | |
end if; | |
if (:old.comm is null and :new.comm is not null) or | |
(:old.comm is not null and :new.comm is null) or | |
:old.comm != :new.comm then | |
insert into history ( | |
id, table_name, column_name, pk1, tab_row_version, action, action_date, action_by, data_type, old_number, new_number | |
) values ( | |
history_seq.nextval, t, 'COMM', :old.id, null, 'U', sysdate, u, 'NUMBER', :old.comm, :new.comm); | |
end if; | |
if (:old.deptno is null and :new.deptno is not null) or | |
(:old.deptno is not null and :new.deptno is null) or | |
:old.deptno != :new.deptno then | |
insert into history ( | |
id, table_name, column_name, pk1, tab_row_version, action, action_date, action_by, data_type, old_number, new_number | |
) values ( | |
history_seq.nextval, t, 'DEPTNO', :old.id, null, 'U', sysdate, u, 'NUMBER', :old.deptno, :new.deptno); | |
end if; | |
elsif deleting then | |
insert into history ( | |
id, table_name, column_name, pk1, tab_row_version, action, action_date, action_by, data_type, old_number, new_number | |
) values ( | |
history_seq.nextval, t, 'EMPNO', :old.id, null, 'D', sysdate, u, 'NUMBER', :old.empno, :new.empno); | |
insert into history ( | |
id, table_name, column_name, pk1, tab_row_version, action, action_date, action_by, data_type, old_vc, new_vc | |
) values ( | |
history_seq.nextval, t, 'ENAME', :old.id, null, 'D', sysdate, u, 'VARCHAR2', :old.ename, :new.ename); | |
insert into history ( | |
id, table_name, column_name, pk1, tab_row_version, action, action_date, action_by, data_type, old_vc, new_vc | |
) values ( | |
history_seq.nextval, t, 'JOB', :old.id, null, 'D', sysdate, u, 'VARCHAR2', :old.job, :new.job); | |
insert into history ( | |
id, table_name, column_name, pk1, tab_row_version, action, action_date, action_by, data_type, old_number, new_number | |
) values ( | |
history_seq.nextval, t, 'MGR', :old.id, null, 'D', sysdate, u, 'NUMBER', :old.mgr, :new.mgr); | |
insert into history ( | |
id, table_name, column_name, pk1, tab_row_version, action, action_date, action_by, data_type, old_date, new_date | |
) values ( | |
history_seq.nextval, t, 'HIREDATE', :old.id, null, 'D', sysdate, u, 'DATE', :old.hiredate, :new.hiredate); | |
insert into history ( | |
id, table_name, column_name, pk1, tab_row_version, action, action_date, action_by, data_type, old_number, new_number | |
) values ( | |
history_seq.nextval, t, 'SAL', :old.id, null, 'D', sysdate, u, 'NUMBER', :old.sal, :new.sal); | |
insert into history ( | |
id, table_name, column_name, pk1, tab_row_version, action, action_date, action_by, data_type, old_number, new_number | |
) values ( | |
history_seq.nextval, t, 'COMM', :old.id, null, 'D', sysdate, u, 'NUMBER', :old.comm, :new.comm); | |
insert into history ( | |
id, table_name, column_name, pk1, tab_row_version, action, action_date, action_by, data_type, old_number, new_number | |
) values ( | |
history_seq.nextval, t, 'DEPTNO', :old.id, null, 'D', sysdate, u, 'NUMBER', :old.deptno, :new.deptno); | |
end if; | |
end emp_aud; | |
/ |
Flashback Data Archiveを構成する
Flashback Data Archive ... can it handle high volume of transactions?
https://www.youtube.com/watch?v=FpRAc-FEWbE
以下はSYSで作業しています。
create flashback archive default longterm
tablespace users quota 1g retention 1 day;
alter table apexdev.emp flashback archive longterm;
begin
dbms_flashback_archive.set_context_level(
level => 'ALL'
);
end;
/