8

まず、現在、望ましい動作をしていますが、データベースへの変更が必要になったときに維持するのは簡単ではありません。私は、よりシンプルで、より効率的で、保守しやすいものを探しています (これら 3 つのいずれかを行うものは大歓迎です)。更新を実行すると、現在の行のコピーである履歴行が作成され、現在の行の値が更新されます。その結果、更新前の行の履歴が記録されます。

理由: 私たちは多くの連邦規則に準拠する必要があり、すべての完全な監査履歴を保持するためにこの方法を採用しました。また、いつでもデータベースを見て、物事がどのように見えるかを確認できます (将来の要件) . 同様の理由で、履歴の記録方法を変更することはできません...どのソリューションも、現在のトリガーが作成するデータと同じ結果になる必要があります。

Contactテーブルの現在のトリガーは次のようになります:
(簡潔にするために不要なフィールドを取り除いています。フィールドの数は重要ではありません)

更新前 (各行):

DECLARE
     indexnb number;
BEGIN
  :new.date_modified := '31-DEC-9999';
  indexnb := STATE_PKG.newCONTACTRows.count + 1;
  :new.date_start := sysdate;
  :new.version := :old.version + 1;
  state_pkg.newCONTACTRows(indexnb).ID := :old.ID;
  state_pkg.newCONTACTRows(indexnb).PREFIX := :old.PREFIX;
  state_pkg.newCONTACTRows(indexnb).FIRST_NAME := :old.FIRST_NAME;
  state_pkg.newCONTACTRows(indexnb).MIDDLE_NAME := :old.MIDDLE_NAME;
  state_pkg.newCONTACTRows(indexnb).LAST_NAME := :old.LAST_NAME;
  --Audit columns after this
  state_pkg.newCONTACTRows(indexnb).OWNER := :old.OWNER;
  state_pkg.newCONTACTRows(indexnb).LAST_USER := :old.LAST_USER;
  state_pkg.newCONTACTRows(indexnb).DATE_CREATED := :old.DATE_CREATED;
  state_pkg.newCONTACTRows(indexnb).DATE_MODIFIED := sysdate;
  state_pkg.newCONTACTRows(indexnb).VERSION := :old.VERSION;
  state_pkg.newCONTACTRows(indexnb).ENTITY_ID := :old.id;
  state_pkg.newCONTACTRows(indexnb).RECORD_STATUS := :old.RECORD_STATUS;
  state_pkg.newCONTACTRows(indexnb).DATE_START := :old.DATE_START;
END;

更新前 (すべての行に対して 1 回):

BEGIN
  state_pkg.newCONTACTRows := state_pkg.eCONTACTRows;
END;

更新後 (すべての行に対して 1 回):

DECLARE
BEGIN
  for i in 1 .. STATE_PKG.newCONTACTRows.COUNT loop
    INSERT INTO "CONTACT" (
      ID, 
      PREFIX, 
      FIRST_NAME, 
      MIDDLE_NAME, 
      LAST_NAME, 
      OWNER, 
      LAST_USER, 
      DATE_CREATED, 
      DATE_MODIFIED, 
      VERSION, 
      ENTITY_ID, 
      RECORD_STATUS, 
      DATE_START)
    VALUES (
      CONTACT_SEQ.NEXTVAL, 
      state_pkg.newCONTACTRows(i).PREFIX,
      state_pkg.newCONTACTRows(i).FIRST_NAME,
      state_pkg.newCONTACTRows(i).MIDDLE_NAME,
      state_pkg.newCONTACTRows(i).LAST_NAME,
      state_pkg.newCONTACTRows(i).OWNER,
      state_pkg.newCONTACTRows(i).LAST_USER,
      state_pkg.newCONTACTRows(i).DATE_CREATED,
      state_pkg.newCONTACTRows(i).DATE_MODIFIED,
      state_pkg.newCONTACTRows(i).VERSION,
      state_pkg.newCONTACTRows(i).ENTITY_ID,
      state_pkg.newCONTACTRows(i).RECORD_STATUS,
      state_pkg.newCONTACTRows(i).DATE_START
    );
  end loop;
END;

次のように定義されたパッケージ (トリミングされたフル バージョンは、表ごとにこれをコピーしたものにすぎません):

PACKAGE STATE_PKG IS
  TYPE CONTACTArray IS TABLE OF CONTACT%ROWTYPE INDEX BY BINARY_INTEGER; 
  newCONTACTRows CONTACTArray; 
  eCONTACTRows CONTACTArray;
END;

現在の結果

結果の履歴サンプルは次のとおりです。

ID    First Last   Ver  Entity_ID  Date_Start              Date_Modified  
1196  John  Smith  5    0          12/11/2009 10:20:11 PM  12/31/9999 12:00:00 AM
1201  John  Smith  0    1196       12/11/2009 09:35:20 PM  12/11/2009 10:16:49 PM
1203  John  Smith  1    1196       12/11/2009 10:16:49 PM  12/11/2009 10:17:07 PM
1205  John  Smith  2    1196       12/11/2009 10:17:07 PM  12/11/2009 10:17:19 PM
1207  John  Smith  3    1196       12/11/2009 10:17:19 PM  12/11/2009 10:20:00 PM
1209  John  Smith  4    1196       12/11/2009 10:20:00 PM  12/11/2009 10:20:11 PM

各履歴レコードには、現在の行の ID である Entity_ID があり、新しいレコードの Date_Start は、最後の履歴行の Date_Modified と一致します。これにより、次のようなクエリを実行できますWhere Entity_ID = :id Or ID = :id And :myDate < Date_Modified And :myDate >= Date_Start。履歴は で取得できますEntity_ID = :current_id

これを行うためのより良いアプローチはありますか? 概念は単純です。行を更新するときに、古い値を挿入して同じテーブルにコピーし、現在の行を更新します...しかし、実際にそれを行うには、もっと簡単な方法をまだ見つけていません。オラクルのより巧妙で賢明な人が、これに対するより良いアプローチを持っていることを願っています。速度はそれほど重要ではありません。ほとんどの Web アプリケーションと同様に、99% 読み取り 1% 書き込みであり、すべての一括操作は挿入であり、履歴を作成しない更新ではありません。

誰かがこれのメンテナンスを簡素化するためのアイデアを持っているなら、私は非常に感謝しています、ありがとう!

4

7 に答える 7

4

残念ながら、トリガーですべての列名(:OLD.this、:OLD.thatなど)を参照することを回避する方法はありません。ただし、実行できることは、テーブル定義(USER_TAB_COLS内)からトリガーコードを生成するプログラムを作成することです。その後、テーブルが変更されるたびに、トリガーの新しいコピーを生成してコンパイルできます。

その方法については、このAskTomスレッドを参照してください。

于 2010-02-10T17:05:46.227 に答える
4

さて、これは書き直しです。最初に回答したときに見逃していたのは、アプリケーションがその履歴をメイン テーブルに保存していることです。これで、@NickCraver がコードについて非常に謝罪している理由がわかりました。

最初にすべきことは、この設計の加害者を追い詰め、二度と同じことをしないようにすることです。このような履歴の保存はスケーリングせず、通常の (非履歴) クエリをより複雑にし、リレーショナルの整合性を妨害します。明らかに、それが問題にならないシナリオがあり、おそらくあなたのサイトはその 1 つですが、一般的にこれは非常に悪い実装です。

これを行う最善の方法は、Oracle 11g Total Recallです。これは、完全に目に見えない効率的な実装を備えた洗練されたソリューションであり、オラクルの他の有料の追加料金の基準により、非常に手頃な価格です。

しかし、Total Recall が問題外であり、本当にこれを行う必要がある場合は、更新を許可しないでください。既存の CONTACT レコードへの変更は、挿入する必要があります。これを機能させるには、INSTEAD OF トリガーを使用してビューを作成する必要がある場合があります。それはまだ厄介ですが、あなたが今持っているものほど厄介ではありません.


Oracle 11.2.0.4 の時点で、Total Recall は Flashback Archive にブランド変更され、エンタープライズ ライセンスの一部として含まれています (ただし、Advanced Compress オプションを購入しない限り、圧縮されたジャーナル テーブルは除外されます)。

Oracle からのこの大規模な機能により、FDA は履歴を保存する通常の方法になるはずです。FDA は効率的で、パフォーマンスが高く、履歴クエリをサポートする標準構文を備えた Oracle に組み込まれています。悲しいかな、私はまだ何年もの間、パッチコックされたトリガー、破損した主キー、およびひどいパフォーマンスを備えた中途半端な実装を見ることを期待しています。ジャーナリングは、すべてのビジネス オペレーションの 99.99% とはほとんど関係のない低レベルの配管であるにもかかわらず、開発者が喜ぶ気晴らしの 1 つであるように思われるからです。

于 2010-02-10T17:14:15.567 に答える
4

誰かが私たちと同じ高度に専門化されたケースを持っている場合(Linqアクセスにより、単一のテーブル履歴がよりクリーン/簡単になります。これは、私たちが持っているものを単純化するために最終的に行ったことです。改善を歓迎します....これは、データベースが変更されるたびに実行され、監査トリガーが再生成されます。主な変更はPRAGMA AUTONOMOUS_TRANSACTION;、履歴生成を自律型トランザクションに配置し、ミューテーションを気にしないことです (監査方法には関係ありません)。

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;
/

改善できる場合は、お気軽に...私はほんの一握りのPL/SQLスクリプトしか作成していません。必要になることはあまりありません...おそらく、そこに望まれるものがたくさん残っています。

これをもう少し詳しく調べさせてくれたAPCの功績を認めてください。モデル/アプリケーション/スタックの残りの部分が非常にうまく機能しない限り、この履歴レイアウトはお勧めしません。このアプリケーションでは、常に履歴と現在の組み合わせを表示します。Linq から SQL へのスタイルのアクセスに関して言えば、フィルタリングは組み合わせよりもはるかに簡単です。すべての回答、すべての良い提案に感謝します...そして、もっと時間があり、リリーススケジュールに追われていないときは、これを再訪して、さらに改善できるかどうかを確認します.

于 2010-02-11T01:20:25.680 に答える
2

履歴と現在の値を同じテーブルに保持するための特定のアプリケーション要件を理解していますが、おそらくこれは、別の監査テーブルを持つより一般的なルートをたどることで処理できますが、それを疑似実体化されたビューとして構築して、アプリケーションの組み合わせビュー。

私にとって、これには、単純な「現在の」ビューと、完全に自動化された独立した「監査」ビュー (この場合は現在のビューもあります) があるという利点があります。

何かのようなもの:

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
于 2010-02-11T15:03:40.963 に答える
1

データベースの複雑さ (テーブルの数、サイズ、PK/FK 関係の深さ、トリガーのその他のロジック) に応じて、Oracle Workspace Managementを確認することをお勧めします。テーブルをワークスペース管理下に置く API 呼び出しを行うと、Oracle はテーブルを更新可能なビューと、行のすべてのバージョンの履歴を保持するその他の対応するオブジェクトに置き換えます。

私はこれを使用しましたが、欠点もありますが、監査の利点の 1 つは、コード オブジェクトがすべて Oracle によって生成され、その正確性が一般的に想定されていることです。

于 2010-02-10T18:15:25.867 に答える
1

一般的なソリューションを開発する場合は、DBMS_SQL パッケージを検討することをお勧めします。それを使用すると、テーブル名を入力として取り、それに基づいて更新を構築するパッケージ/プロシージャを開発できます。ディクショナリのテーブル構造を調べて、その場で更新を構築します。これは重要な先行開発ですが、テーブル構造が変更された場合、コードがそれを感知して適応するため、将来のメンテナンスは大幅に少なくなります。この方法は、使用したいすべてのテーブルで機能します。

于 2010-02-10T17:33:29.303 に答える
0

履歴レコードを「現在の」レコードと同じテーブルに格納することをお勧めするのは、レコードへの FK リンクがそれらにリンクする必要がある場合、またはリンクする必要がある場合のみです。たとえば、私が見たあるアプリケーションには、「ある時点」のレコードにリンクする FK リンクがいくつかありました。つまり、レコードが更新された場合、FK は引き続き履歴レコードにリンクします。設計の重要な部分であり、履歴レコードを 2 番目のテーブルに分離すると、扱いにくくなります。

それとは別に、すべての変更を追跡するためのビジネス要件は、テーブルごとに個別の「履歴」テーブルを使用して解決することをお勧めします。確かに、これは DDL が増えることを意味しますが、アプリケーション コードが大幅に簡素化され、パフォーマンスとスケーラビリティが向上するというメリットも得られます。

于 2010-02-11T00:48:48.270 に答える