ORACLE动态完全数据日志(AUDIT LOG)批处理生成脚本

目标:

1)实现对数据更新CRUD的记录

2)可以对动态调整需要做日志的表/列

3)可对比更新前后的数据差别。

4)记录操作的用户和操作方式等等

效果:

1)在T_AUDIT_LOGS中记录动态指定所有的CRUD操作

2)更新后前后数据对比用HTML <TABLE>呈现,比如:

列表值 VALUE TestVal TestVal-修改后
列表值说明 F_DESC 测试值 测试值-修改后
应用模块 APPLY_TO SOMEWHERE SOMEWHERE-修改后
有效 ISVALID 1 2
排序 F_ORDER 1 2
创建日期 CREATE_DATE 2011/09/12 12:42:46 2011/10/12 12:42:46

;

此部分实现比较麻烦,请按步骤做。

 

1)创建日志数据库:

create table T_AUDIT_LOGS
(
  id          NUMBER(18) not null,
  create_date TIMESTAMP(6) default SYSTIMESTAMP not null,
  member_id   VARCHAR2(30),
  table_name  VARCHAR2(30) not null,
  action      VARCHAR2(10) not null,
  data        CLOB,
  record_id   VARCHAR2(100),
  f_function  VARCHAR2(200),
  access_ip   CHAR(15),
  remark      VARCHAR2(500)
)
;
comment on table T_AUDIT_LOGS
  is '系统CRUD记录';
comment on column T_AUDIT_LOGS.member_id
  is '用户ID';
comment on column T_AUDIT_LOGS.table_name
  is '表名';
comment on column T_AUDIT_LOGS.action
  is '操作';
comment on column T_AUDIT_LOGS.data
  is '新旧数据比较';
comment on column T_AUDIT_LOGS.record_id
  is '关键ID号';
comment on column T_AUDIT_LOGS.f_function
  is '访问模块';
comment on column T_AUDIT_LOGS.access_ip
  is '用户IP';
comment on column T_AUDIT_LOGS.remark
  is '备注1';
create index AUDIT_LOGS_CREATE_DATE_IX on T_AUDIT_LOGS (CREATE_DATE);
create index AUDIT_LOGS_MEMBER_ID_IX on T_AUDIT_LOGS (MEMBER_ID);
create index AUDIT_LOGS_TABLE_RECORD_ID_IX on T_AUDIT_LOGS (TABLE_NAME, RECORD_ID);
alter table T_AUDIT_LOGS
  add constraint AUDIT_LOGS_PK primary key (ID);

/

CREATE SEQUENCE audit_logs_seq;

/


 

2)创建日志设置数据表(里存在的记录将做日志记录')

create table T_AUDITDISABLED
(
  table_name VARCHAR2(50) not null,
  colum_name VARCHAR2(50) not null
)
;
comment on table T_AUDITDISABLED
  is '是否需要记录CRUD, 这里存在的记录将----不---LOG';
comment on column T_AUDITDISABLED.table_name
  is '表名,可以使用通配符: *';
comment on column T_AUDITDISABLED.colum_name
  is '列名,可以使用通配符: *   表名+列名 不可重复!';
alter table T_AUDITDISABLED
  add constraint PK_TABLE_NAME_COLUM_NAME primary key (TABLE_NAME, COLUM_NAME);

3)创建包

CREATE OR REPLACE PACKAGE tsh_audit AS

PROCEDURE insert_log (p_username  IN  VARCHAR2,
                      p_object    IN  VARCHAR2,
                      p_action    IN  VARCHAR2,
                      p_data      IN  CLOB,
                      p_record_id IN  VARCHAR2,
                      p_function  IN  VARCHAR2,
                      p_access_ip IN  VARCHAR2,
                      p_remark    IN  VARCHAR2
                      );

END;

/

CREATE OR REPLACE PACKAGE BODY tsh_audit AS

PROCEDURE insert_log (p_username  IN  VARCHAR2,
                      p_object    IN  VARCHAR2,
                      p_action    IN  VARCHAR2,
                      p_data      IN  CLOB,
                      p_record_id IN  VARCHAR2,
                      p_function  IN  VARCHAR2,
                      p_access_ip IN  VARCHAR2,
                      p_remark    IN  VARCHAR2
                      ) IS
BEGIN
  INSERT INTO t_audit_logs (
    id,
    CREATE_DATE,
    MEMBER_ID,
    TABLE_NAME,
    action,
    data,
    record_id,
    F_function,
    access_ip,
    REMARK
    )
  VALUES (
    audit_logs_seq.NEXTVAL,
    SYSTIMESTAMP,
    p_username,
    p_object,
    p_action,
    p_data,
    p_record_id,
    p_function,
    p_access_ip,
    p_remark
  );
END;

END;

 /

 

4) 一些ORACLE 函数

4.1)判断是否需要做日志(根据表名和列名,和表T_AUDITDISABLED设置相关)

create or replace function audit_required(p_tablename varchar2, p_columnname varchar2) return boolean is
  Result boolean;
  num number;
begin
  select count(1) into num from t_auditdisabled where
         (table_name = p_tablename and colum_name = p_columnname)
         or (table_name = '*' and colum_name = p_columnname)
         or (table_name = p_tablename and colum_name = '*')
         ;
  if num > 0 then
    Result := false;
  else
    Result := true;
  end if;
  return(Result);
end audit_required;

 

4.2)更新前后数据比较,直接输出是<table>元素

create or replace function Get_Difference(p_tablename varchar2, p_columnname varchar2,p_oldval varchar2, p_newval varchar2) return varchar2 is
  Result varchar2(32767);
begin
  if audit_required( p_tablename, p_columnname ) -- 此表列的是否需要AUDIT?
  and ( (p_oldval is null and p_newval is not null)
  or (p_oldval is not null and p_newval is null)
  or (p_oldval != p_newval) )
  then
    Result := '<tr>'||chr(10)
              ||chr(9)||'<td class="AC">'||GET_COMMENTS(p_tablename,p_columnname)||'</td>'||chr(10)
              ||chr(9)||'<td class="AD">'||p_columnname||'</td>'||chr(10)
              ||chr(9)||'<td class="AO">'||p_oldval||'</td>'||chr(10)
              ||chr(9)||'<td class="AN">'||p_newval||'</td>'||chr(10)
              ||'</tr>'||chr(10);
  else
    Result := '';
  end if;
 
  return(Result);
end Get_Difference;

 

4.3)取得表中说明文字(COMMENTS),4.2)和生成的触发器大量使用此函数。COMMENTS的格式是:列名解释|其他注释。

注意:“列名解释”和“其他注释”之间用“|”号分隔。比如:UserID的Comments是:用户ID|此是用户登录的唯一ID
PS:不会没使用过列注释(comment)吧?上面创建的表都有COMMENT的,养成写表/列COMMENT的习惯,可以使得归档容易。本脚本对COMMENT的格式是有要求了,上面已经说了。

 

create or replace function GET_COMMENTS(p_tablename varchar2, p_columnname varchar2) return varchar2 is
  Result varchar2(32767); 
begin
  select comments INTO Result
    from all_col_comments t2
    where t2.table_name = p_tablename
    and t2.column_name = p_columnname
    AND ROWNUM = 1 ORDER BY ROWNUM ASC;
  Result := split(Result,1,'|');  -- 取COMMENT中的第一个元素,以“|”分隔
  return(Result);
end GET_COMMENTS;

 

4.4)ORACLE的SPIT函数,很好用!在此大量使用。

比如:split('32|3434.3|aaa', 1, '|') 返回 32,

注意:第一个为字串,第2个参数以1为开始,第三个参数是分隔符号

create or replace function split(input_list varchar2, ret_this_one number, delimiter varchar2)
return varchar2
is
  v_list varchar2(32767) := delimiter || input_list;
  start_position number;
  end_position number;
begin
  start_position := instr(v_list, delimiter, 1, ret_this_one);
  if start_position > 0 then
    end_position := instr( v_list, delimiter, 1, ret_this_one + 1);
    if end_position = 0 then
      end_position := length(v_list) + 1;
    end if;
    return(substr(v_list, start_position + 1, end_position - start_position - 1));
  else
    return NULL;
  end if;
end split;

 

4.5)判断字段类型。此处没有使用DUMP函数,因为数据表中有可能是空表。此处只用于生成器中。

create or replace function is_date(p_tablename varchar2,p_columnname varchar2) return boolean is
  Result boolean :=false;
  v_typ varchar2(50);
begin
  SELECT DATA_TYPE INTO v_typ
         FROM all_tab_columns
         WHERE TABLE_NAME = p_tablename
         AND COLUMN_NAME = p_columnname
         AND ROWNUM = 1;
/*
  v_typ := SPLIT(p_dump_string,1,' '); 
  if (v_typ='Typ=13'
        OR v_typ='Typ=12'
        OR v_typ='Typ=180'
        OR v_typ='Typ=181'
        OR v_typ='Typ=231'
        OR v_typ='Typ=183'
      ) THEN
*/   
  if (v_typ='DATE'
        OR v_typ='TIMESTAMP'
      ) THEN
    Result := true;
  ELSE
    Result := false;
  END IF;
  return(Result);
end is_date;

 

4.6)设置默认值。在生成的Trigger中使用。

create or replace function SETVALUE(p_val varchar2, p_default varchar2) return varchar2 is
  Result varchar2(32767);
begin
  if (p_val is null) then
    Result := p_default;
  elsif ('' = p_val) then
      Result := p_default;
  else
      Result := p_val;
  end if; 
  return(Result);
end SETVALUE;

 

5)这是生成日志触发器的关键程序

先看b)点!!!

上面准备就绪后,在SQL*PLUS中:@D:\PROJECTS\audit_trigger.sql username

其中username是表的拥有者,生成SQL脚本文件为:d:\projects\all_audit.sql,得到all_audit.sql,应该知道做什么了吧?--运行-生成TRIGGER!

注意:

a)生成脚本all_audit.sql会在非限制表中(在表T_AUDITDISABLED定义)创建新列audit_info VARCHAR2(1000)

此列是'日志用系统字段,CRUD时必须更新此字段,提供给TRIGGER更新信息

audit_info的数据格式为:MEMBER_ID|CONTROLLER.METHOD|ACCESS_IP|REMARK,即是:

用户ID|调用方法(或URL)|登录IP|备注,注意其中以“|”分隔

b)在使用audit_trigger.sql 之前,必须做:用于日志的表T_AUDIT_LOGS和T_AUDITDISABLED不能也做日志记录(会死循环),可以这样在T_AUDIT_LOGS插入两条记录:

insert into T_AUDITDISABLED (table_name, colum_name) values ('T_AUDIT_LOGS', '*');
insert into T_AUDITDISABLED (table_name, colum_name) values ('T_AUDITDISABLED', '*');
-- 所有表的'AUDIT_INFO'字段也不需要日志记录
insert into T_AUDITDISABLED (table_name, colum_name) values ('*', 'AUDIT_INFO');

c)同上例,对于不需要审计的表/列可以如法炮制(可在生成触发器后动态调整)。


SET SERVEROUTPUT ON SIZE 1000000
SET VERIFY OFF
SET TRIMSPOOL ON
SPOOL ON
Spool d:\projects\all_audit.sql
DECLARE
  v_owner  VARCHAR2(100) := UPPER('&1');
  v_table  VARCHAR2(100);
  v_id  VARCHAR2(100);
  v_dump VARCHAR2(200);

  CURSOR c_tables IS
    SELECT table_name
    FROM   user_tables
    WHERE table_name not in
    (SELECT table_name FROM T_AUDITDISABLED
     WHERE COLUM_NAME = '*');  --对于一些表不生成日志用TRIGGER!, 特别是T_AUDIT_LOGS和T_AUDITDISABLED

  CURSOR c_columns IS
    SELECT column_name
    FROM   all_tab_columns
    WHERE  owner      = v_owner
    AND    table_name = v_table;
BEGIN
  FOR cur_table IN c_tables LOOP
    v_table := cur_table.table_name;
    -- Add/modify columns
   DBMS_OUTPUT.NEW_LINE;
   DBMS_OUTPUT.NEW_LINE;
   DBMS_OUTPUT.PUT_LINE('----------------------------------------');
   DBMS_OUTPUT.PUT_LINE('-----表:' || v_table || '增加AUDIT_INFO -----');
   DBMS_OUTPUT.PUT_LINE('----------------------------------------');
   DBMS_OUTPUT.NEW_LINE;
   --DBMS_OUTPUT.PUT_LINE('alter table '||v_table||' drop column audit_info;'); --注意这两行的修改。
   DBMS_OUTPUT.PUT_LINE('alter table '||v_table||' add audit_info VARCHAR2(1000);');
   DBMS_OUTPUT.PUT_LINE('comment on column '||v_table||'.audit_info');
   DBMS_OUTPUT.PUT_LINE('is ''日志用系统字段,CRUD时必须更新此字段,格式:MEMBER_ID|CONTROLLER.METHOD|ACCESS_IP|REMARK'';');
   DBMS_OUTPUT.NEW_LINE;
   DBMS_OUTPUT.NEW_LINE;
   DBMS_OUTPUT.PUT_LINE('----------------------------------------');
   DBMS_OUTPUT.PUT_LINE('-----表:' || v_table || '的审计触发器 -----');
   DBMS_OUTPUT.PUT_LINE('----------------------------------------');
   DBMS_OUTPUT.NEW_LINE;
 
   DBMS_OUTPUT.PUT_LINE('CREATE OR REPLACE TRIGGER ' || v_table || '_audit_trg');
   DBMS_OUTPUT.PUT_LINE('AFTER INSERT OR UPDATE OR DELETE ON ' || v_table);
   DBMS_OUTPUT.PUT_LINE('FOR EACH ROW');
   DBMS_OUTPUT.PUT_LINE('DECLARE');
   DBMS_OUTPUT.PUT_LINE(' PRAGMA AUTONOMOUS_TRANSACTION;');
   DBMS_OUTPUT.PUT_LINE(' v_action VARCHAR2(30) := ''NONE'';');
   DBMS_OUTPUT.PUT_LINE(' v_log VARCHAR2(32767) :=''<table class="AT">''||chr(10);');
   DBMS_OUTPUT.PUT_LINE('BEGIN');
   DBMS_OUTPUT.PUT_LINE(' IF INSERTING THEN');
   DBMS_OUTPUT.PUT_LINE('  v_action := ''增加'';');
   DBMS_OUTPUT.PUT_LINE(' ELSIF UPDATING THEN');
   DBMS_OUTPUT.PUT_LINE('  v_action := ''更新'';');
   DBMS_OUTPUT.PUT_LINE(' ELSIF DELETING THEN');
   DBMS_OUTPUT.PUT_LINE('  v_action := ''删除'';');
   DBMS_OUTPUT.PUT_LINE(' END IF;');
   FOR cur_rec IN c_columns LOOP
     IF c_columns%ROWCOUNT = 1 THEN
       v_id := cur_rec.column_name;
     END IF;
     --execute immediate 'SELECT DUMP('||cur_rec.column_name||') FROM '||v_table||' WHERE ROWNUM=1'
     --INTO v_dump;
     DBMS_OUTPUT.PUT_LINE(' v_log := v_log||Get_Difference('''||v_table||''',');
     DBMS_OUTPUT.PUT_LINE('  '''||cur_rec.column_name||''',');
     if (is_date(v_table,cur_rec.column_name)) then
      DBMS_OUTPUT.PUT_LINE('  to_char(:OLD.'||cur_rec.column_name||',''YYYY/MM/DD HH24:MI:SS''),');
      DBMS_OUTPUT.PUT_LINE('  to_char(:NEW.'||cur_rec.column_name||',''YYYY/MM/DD HH24:MI:SS''));');
     else
      DBMS_OUTPUT.PUT_LINE('  to_char(:OLD.'||cur_rec.column_name||'),');
      DBMS_OUTPUT.PUT_LINE('  to_char(:NEW.'||cur_rec.column_name||'));');
    end if;
   END LOOP;  
   DBMS_OUTPUT.PUT_LINE(' v_log := v_log||''</table>'';');
   DBMS_OUTPUT.PUT_LINE(' tsh_audit.insert_log (');
   DBMS_OUTPUT.PUT_LINE('  SETVALUE(split(:NEW.AUDIT_INFO,1,''|''),''SYSTEM''), --默认是SYSTEM;
   DBMS_OUTPUT.PUT_LINE('  ''' || v_table || ''',');
   DBMS_OUTPUT.PUT_LINE('  v_action,');
   DBMS_OUTPUT.PUT_LINE('  v_log,');
   DBMS_OUTPUT.PUT_LINE('  :NEW.'||v_id||',');
   DBMS_OUTPUT.PUT_LINE('  split(:NEW.AUDIT_INFO,2,''|''),');  --对本表中AUDIT_INFO的一些处理
   DBMS_OUTPUT.PUT_LINE('  split(:NEW.AUDIT_INFO,3,''|''),');
   DBMS_OUTPUT.PUT_LINE('  split(:NEW.AUDIT_INFO,4,''|'')');
 
   DBMS_OUTPUT.NEW_LINE;
   DBMS_OUTPUT.PUT_LINE(' );');
  
   DBMS_OUTPUT.PUT_LINE(' COMMIT;');
   DBMS_OUTPUT.PUT_LINE('END;');
   DBMS_OUTPUT.PUT_LINE('/');
   --DBMS_OUTPUT.PUT_LINE('SHOW ERRORS');
  END LOOP;  
END;
/
SPOOL OFF

 

有人会问,为什么生成的TIRGGER对每个字段都生成类似以下的代码,一个循环不是搞定了吗?:

 v_log := v_log||Get_Difference('T_MEMBER',
  'MEMBER_ID',
  to_char(:OLD.MEMBER_ID),
  to_char(:NEW.MEMBER_ID));

这是因为,在ORACLE中没有办法动态调用“:OLD.字段名”和“:NEW.字段名”,这个是确凿的,所以不能用循环简单解决此问题。

如果可以动态调用,我也不会写这生成器了。

有时,用笨方法,是最好最直接的方法。

转载自:http://greedwind.blog.163.com/blog/static/278310652011812113812782/

posted @ 2012-09-15 11:18  月亮的影子  阅读(1206)  评论(0编辑  收藏  举报