Oracle - Triggers to create a history row on update

后端 未结 7 543
天命终不由人
天命终不由人 2021-02-04 16:28

First, we currently have the behavior that\'s desired, but it\'s not trivial to maintain when any changes to the database are needed. I\'m looking for anything simpler, more ef

相关标签:
7条回答
  • 2021-02-04 16:34

    In case someone has the same highly specialized case we do (Linq access making single table history much cleaner/easier, this is what I ended up doing to simplify what we have, welcome any improvements....this is just a script that will run whenever the database changes, regenerating the audit triggers, the main change being PRAGMA AUTONOMOUS_TRANSACTION; placing the history generating on an autonomous transaction and not caring about mutation (which doesn't matter for how we audit):

    Declare
      cur_trig varchar(4000);
      has_ver number;
    Begin
        For seq in (Select table_name, sequence_name 
                  From user_tables ut, user_sequences us
                  Where sequence_name = replace(table_name, '_','') || '_SEQ'
                    And table_name Not Like '%$%'
                    And Exists (Select 1
                                From User_Tab_Columns utc
                                Where Column_Name = 'ID' And ut.table_name = utc.table_name)
                    And Exists (Select 1
                                From User_Tab_Columns utc
                                Where Column_Name = 'DATE_START' And ut.table_name = utc.table_name)
                    And Exists (Select 1
                                From User_Tab_Columns utc
                                Where Column_Name = 'DATE_MODIFIED' And ut.table_name = utc.table_name))
        Loop
         --ID Insert Triggers (Autonumber for oracle!)
         cur_trig := 'CREATE OR REPLACE TRIGGER ' || seq.table_name || 'CR' || chr(10)
                  || 'BEFORE INSERT ON ' || seq.table_name || chr(10)
                  || 'FOR EACH ROW' || chr(10)
                  || 'BEGIN' || chr(10)
                  || '  SELECT ' || seq.sequence_name || '.NEXTVAL INTO :new.ID FROM DUAL;' || chr(10)
                  || '  IF(:NEW.ENTITY_ID = 0) THEN' || chr(10)
                  || '    SELECT sysdate, sysdate, ''31-DEC-9999'' INTO :NEW.DATE_CREATED, :NEW.DATE_START, :NEW.DATE_MODIFIED FROM DUAL;' || chr(10)
                  || '  END IF;' || chr(10)
                  || 'END;' || chr(10);
    
         Execute Immediate cur_trig;
    
         --History on update Triggers
         cur_trig := 'CREATE OR REPLACE TRIGGER ' || seq.table_name || '_HIST' || chr(10)
                  || '  BEFORE UPDATE ON ' || seq.table_name || ' FOR EACH ROW' || chr(10)
                  || 'DECLARE' || chr(10)
                  || '  PRAGMA AUTONOMOUS_TRANSACTION;' || chr(10)
                  || 'BEGIN' || chr(10)
                  || '  INSERT INTO ' || seq.table_name || ' (' || chr(10)
                  || '   DATE_MODIFIED ' || chr(10)
                  || '   ,ENTITY_ID ' || chr(10);
    
           For col in (Select column_name
                     From user_tab_columns ut
                     Where table_name = seq.table_name
                       And column_name NOT In ('ID','DATE_MODIFIED','ENTITY_ID')
                     Order By column_name)
         Loop
           cur_trig := cur_trig || '   ,' || col.column_name || chr(10);
         End Loop;
    
         cur_trig := cur_trig || ') VALUES ( --ID is Automatic via another trigger' || chr(10)
                              || '   SYSDATE --DateModified Set' || chr(10)
                              || '   ,:old.ID --EntityID Set' || chr(10);
    
         has_ver := 0;
           For col in (Select column_name
                     From user_tab_columns ut
                     Where table_name = seq.table_name
                       And column_name NOT In ('ID','DATE_MODIFIED','ENTITY_ID')
                     Order By column_name)
         Loop
           cur_trig := cur_trig || '   ,:old.' || col.column_name || chr(10);
           If Upper(col.column_name) = 'VERSION' Then 
             has_ver := 1; 
           End If;
         End Loop;
    
         cur_trig := cur_trig || ');' || chr(10)
                              || ':new.DATE_MODIFIED := ''31-DEC-9999'';' || chr(10)
                              || ':new.DATE_START := SYSDATE;' || chr(10);
         If has_ver = 1 Then
           cur_trig := cur_trig || ':new.version := :old.version + 1;' || chr(10);
         End If;
         cur_trig := cur_trig || 'COMMIT;' || chr(10)
                              || 'END;' || chr(10);
    
         Execute Immediate cur_trig;
        End Loop;
    End;
    /
    

    If you can improve, feel free...I've only written a handful of PL/SQL scripts, the need doesn't arise often...probably a lot left to be desired there.

    Answer credit to APC for getting me to look at this a bit harder. I don't recommend this history layout unless it its the rest of your model/application/stack extremely well. For this application, we constantly show a mix of history and current, and filtering is far simpler than combining when it comes to a Linq-to-SQL style access. Thanks for all the answers guys, all good suggestions...and when I have more time and am not crunched by a release schedule, this is something I'll revisit to see if it can be improved further.

    0 讨论(0)
  • 2021-02-04 16:36

    If you want to develop a generic solution, you might want to take a look at DBMS_SQL package. With it you could develop a package/procedure that takes a table name as input and builds the updates based on that, by examining the table structure in the dictionary and building the updates on the fly. It would be non trivial up-front development, but a lot less maintenance in the future, since if a table structure changes, the code would sense that and adapt. This method would work for any table that you care to use it with.

    0 讨论(0)
  • 2021-02-04 16:36

    Depending on the complexity of your database (number of tables, size, depth of PK/FK relationships, other logic in triggers), you might want to look at Oracle Workspace Management. You make an API call to put a table under workspace management which results in Oracle replacing the table with an updatable view and other corresponding objects that maintain a history of all versions of the rows.

    I've used this and while there are disadvantages, one advantage for auditing is that the code objects are all generated by Oracle and their correctness is generally assumed.

    0 讨论(0)
  • 2021-02-04 16:37

    Okay, this is a rewrite. What I missed when I first responded is that the application is storing its History in the main table. Now I understand why @NickCraver is so apologetic about the code.

    Well the first thing to do is to hunt down the perpetrators of this design and make sure they never do it again. Storing history like this doesn't scale, makes normal (non-historical) queries more complicated and sabotages relational integrity. Obviously there are scenarios where none of that matters, and perhaps your site is one of them, but in general this is a very bad implementation.

    The best way of doing this is Oracle 11g Total Recall. It's an elegant solution, with a completely invisible and effcient implementation, and - by the standards of Oracle's other chargeable extras - quite reasonably priced.

    But if Total Recall is out of the question and you really must do it this, don't allow updates. A change to an existing CONTACT record should be an insert. In order to make this work you may need to build a view with an INSTEAD OF trigger. It's still yucky but not quite as yucky as what you have now.


    As of Oracle 11.2.0.4 Total Recall has been rebranded Flashback Archive and is included as part of the Enterprise License (although shorn of the compressed journal tables unless we purchase the Advanced Compress option).

    This largesse from Oracle ought to make FDA the normal way of storing history: it's efficient, it's performative, it's an Oracle built-in with standard syntax to support historical queries. Alas I expect to see half-cooked implementations with spatchcocked triggers, broken primary keys and horrible performance for many years yet. Because journalling seems to be one of those distractions which developers delight in, despite the fact that it's low-level plumbing which is largely irrelevant to 99.99% of all business operations.

    0 讨论(0)
  • 2021-02-04 16:46

    I understand your specifc application requirements to have the history and current values in the same table, but perhaps this could be handled by going down the more usual route of having a separate audit table but building it up as a pseudo-materialized view to present a combined view for the application.

    For me, this has the advantage of having a simple "current" view and a separate but completely automated "audit" view (which in this case also has the current view).

    Something like:

    create sequence seq_contact start with 1000 increment by 1 nocache nocycle;
    
    create table contact (
        contact_id integer,
        first_name varchar2(120 char),
        last_name varchar2(120 char),
        last_update_date date
        );
    
    alter table contact add constraint pk_contact primary key (contact_id);
    
    create table a$contact (
        version_id integer,
        contact_id integer,
        first_name varchar2(120 char),
        last_name varchar2(120 char),
        last_update_date date
        );
    
    alter table a$contact add constraint pk_a$contact primary key
            (contact_id, version_id);
    
    create or replace trigger trg_contact
    before insert or delete or update on contact 
    for each row
    declare
    
        v_row contact%rowtype;
        v_audit a$contact%rowtype;
    
    begin
    
        select seq_contact.nextval into v_audit.version_id from dual;
    
        if not deleting then
    
            :new.last_update_date := sysdate;
    
        end if;
    
        if inserting or updating then
    
            v_audit.contact_id := :new.contact_id;
            v_audit.first_name := :new.first_name;
            v_audit.last_name := :new.last_name;
            v_audit.last_update_date := :new.last_update_date;
    
        elsif deleting then
    
            v_audit.contact_id := :old.contact_id;
            v_audit.first_name := :old.first_name;
            v_audit.last_name := :old.last_name;
            v_audit.last_update_date := sysdate;
    
        end if;
    
        insert into a$contact values v_audit;
    
    end trg_contact;
    /
    
    insert into contact (contact_id, first_name, last_name) values
        (1,'Nick','Pierpoint');
    
    insert into contact (contact_id, first_name, last_name) values
        (2, 'John', 'Coltrane');
    
    insert into contact (contact_id, first_name, last_name) values
        (3, 'Sonny', 'Rollins');
    
    insert into contact (contact_id, first_name, last_name) values
        (4, 'Kenny', 'Wheeler');
    
    update contact set last_name = 'Cage' where contact_id = 1;
    
    delete from contact where contact_id = 1;
    
    update contact set first_name = 'Zowie' where contact_id in  (2,3);
    
    select * from a$contact order by contact_id, version_id;
    
    VERSION_ID  CONTACT_ID  FIRST_NAME  LAST_NAME  LAST_UPDATE_DATE
    1000        1           Nick        Pierpoint  11/02/2010 14:53:49
    1004        1           Nick        Cage       11/02/2010 14:54:00
    1005        1           Nick        Cage       11/02/2010 14:54:06
    1001        2           John        Coltrane   11/02/2010 14:53:50
    1006        2           Zowie       Coltrane   11/02/2010 14:54:42
    1002        3           Sonny       Rollins    11/02/2010 14:53:51
    1007        3           Zowie       Rollins    11/02/2010 14:54:42
    1003        4           Kenny       Wheeler    11/02/2010 14:53:53
    
    0 讨论(0)
  • 2021-02-04 16:47

    The only time I might recommend that historical records be stored in the same table as the "current" records is when FK links to the records must or might need to link to them. For example, one application I've seen had some FK links that would link to the record as of a "point in time", that is, if the record was updated, the FK would still link to the historical record - this was an important part of the design and separating history records into a second table would have made it more unwieldy.

    Apart from that, I'd prefer that a business requirement for tracking all changes should be solved using a separate "history" table for each table. Sure, it means more DDL, but it simplifies the application code enormously and you'll also benefit from better performance and scalability.

    0 讨论(0)
提交回复
热议问题