Premièrement, nous avons actuellement le comportement souhaité, mais ce n'est pas trivial de maintenir lorsque des modifications apportées à la base de données sont nécessaires. Je cherche quelque chose de plus simple, plus efficace ou plus facile à maintenir (tout ce que l'un de ces 3 serait le plus accueilli). Lorsque nous effectuons une mise à jour, une ligne d'historique est créée, une copie de la ligne forte> courante forte> et les valeurs de la ligne actuelle sont ensuite mises à jour. Le résultat étant que nous avons un enregistrement d'historique de la façon dont la ligne était avant qu'elle a été mise à jour.
raisonnement: nous devons nous conformer à un certain nombre de règles fédérales et que cette route a été établie pour avoir un historique d'audit complet de tout, comme Eh bien, car nous pouvons regarder la base de données à tout moment et voir comment les choses avaient l'air (exigence future). Pour des raisons similaires, je ne peux pas modifier la manière dont l'histoire est enregistrée forte> ... Toute solution doit entraîner les mêmes données que les déclencheurs actuels créent. P> Voici à quoi ressemblent les déclencheurs actuels Le avant la mise à jour (chaque ligne): p> après la mise à jour (une fois pour toutes les lignes): p> Le colis défini comme (La version complète coupée n'est qu'une copie de cette table): p> Voici un historique d'historique résultant: Contact Code> Table:
(champs extrêmement inutiles pour la brièveté, le nombre de champs peu importe) em> p> le résultat actuel h2>
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
7 Réponses :
Malheureusement, il n'y a aucun moyen d'éviter de référencer tous les noms de colonne (: old.C'est: old.that, etc.) en déclencheurs. Cependant, ce que vous pourriez faire est de rédiger un programme sur Générez fort> le code de déclenchement de la définition de la table (dans user_tab_cols). Ensuite, chaque fois que la table est changée, vous pouvez générer et compiler une nouvelle copie des déclencheurs. P>
Voir ce thread pour comment faire cela. P>
@Tony - C'est ce que nous faisons actuellement et synchroniser plusieurs copies de la base de données est un vrai cauchemar ... En espérant que quelqu'un connaisse une alternative, quelque chose comme Sélectionnez * dans TEMP code>, mise à jour uniquement Peu de champs d'audit, font un insert ... Je ne sais pas comment travailler cela, par exemple, l'oracle n'est pas mon champ principal :(
D'accord, c'est une réécriture. Ce que j'ai manqué quand j'ai répondu pour la première fois, c'est que l'application stocke son histoire dans la table principale. Maintenant, je comprends pourquoi @nickcraver est tellement excusé sur le code. P>
Eh bien, la première chose à faire est de chasser les auteurs de ce design et de ne jamais le refaire. Stocker l'historique comme celui-ci ne fait pas échouer, fait des requêtes normales (non historiques) plus compliquées et sabotages Intégrité relationnelle. De toute évidence, il y a des scénarios où rien de ce qui importe, et peut-être que votre site est l'un d'entre eux, mais en général, il s'agit d'une très mauvaise implémentation. P>
La meilleure façon de faire c'est Oracle 11g Rappel total . C'est une solution élégante, avec une implémentation totalement invisible et effoncente, et - par les normes des autres extras frais de l'oracle - assez raisonnablement à prix raisonnable. P>
Mais si le rappel total est hors de question et que vous devez vraiment le faire cela, ne permettez pas de mises à jour em>. Une modification d'un enregistrement de contact existant doit être un insert. Afin de faire ce travail, vous devrez peut-être créer une vue avec un déclencheur. C'est toujours levé mais pas tout aussi comme officier que ce que vous avez maintenant. P>
AS de Oracle 11.2.0.4 Le rappel total a été rebrandé des archives de flashback et est incluse dans le cadre de la licence d'entreprise (bien que de la chasse des tables de journal compressées à moins que nous achètisons l'option de compresse avancée). p>
Cette largesse d'Oracle devrait faire de la FDA la manière normale de stocker l'histoire: c'est efficace, il est performatif, c'est un oracle intégré à la syntaxe standard pour soutenir les requêtes historiques. Hélas Je m'attends à voir des implémentations à moitié cuit avec des déclencheurs sparticulaires, des clés primaires brisées et des performances horribles pendant de nombreuses années. Parce que Journaling semble être l'une de ces distractions que les développeurs adorent, malgré le fait que sa plomberie de bas niveau est largement pertinente pour atteindre 99,99% de toutes les opérations commerciales. p>
Vous dites faire cela avec combinaison juste : nouveau.date_modified: = '31 -Dec-9999 '; : new.date_start: = sysdate; : NEW.VVERSION: =: Old.Version + 1; code> dans un seul déclencheur? Cela peut travailler ... Il y avait une raison pour laquelle nous ne pouvions pas faire des déclencheurs célibataires avant, mais c'est l'une des situations qu'il y a si longtemps, je ne me souviens que de la raison, laissez-moi essayer votre approche et voir ce qui se passe. Merci!
Je pensais que, mais il n'y a-t-il pas de danger de "table mutation" avec un insert à plusieurs rangées? Ou est-ce ok ces jours-ci - je ne me souviens pas.
@APC - j'ai essayé ceci ... Tony est correct, cela produit un ora-04091: le contact de table est mutating, déclencheur / fonction peut ne pas le voir code>, c'est pourquoi nous ne pouvions pas y aller L'itinéraire avant ... a pris le voir pour se souvenir, comme la plupart des choses anciennes.
11g n'est pas hors de question, mais nous sommes sur 10 pour le moment et je ne peux pas mettre à niveau sans préavis juste. Pour la mise à l'échelle, il s'agit d'une question de la moindre complication dans toutes les couches. Nous référons souvent l'historique de Linq-to-oracle et dB.Contacts.Current () Code> vs
dB.Contacts code> facilite l'accès à l'historique de manière générique ... c'est le raisonnement derrière cela. Le partitionnement basé sur l'entité_id (= 0,! = 0) garde la performance bien dans notre cas ... mais le système n'est pas encore énorme. Il est possible qu'une meilleure solution puisse être atteinte en modifiant l'historique de la base de données et en les couches LINQ, mais pas de temps.
@APC - Votre solution originale avec certaines modifications fonctionnées, il n'y a aucune raison pour que nous ne puissions pas utiliser les transactions anonymes à cette fin, je n'avais pas rencontré ce travail auparavant, merci de me faire prendre plus de difficultés sur l'approche plus simple!
Si vous souhaitez développer une solution générique, vous pouvez consulter le package DBMS_SQL. Avec vous, vous pouvez développer un paquet / procédure qui prend un nom de table comme entrée et construit les mises à jour en fonction de cela, en examinant la structure de la table dans le dictionnaire et en construisant les mises à jour à la volée. Ce serait un développement non trivial au-delà, mais beaucoup moins de maintenance à l'avenir, car si une structure de table change, le code le sentirait et s'adapterait. Cette méthode fonctionnerait pour toute table que vous souhaitez l'utiliser. P>
Selon la complexité de votre base de données (nombre de tableaux, taille, profondeur des relations PK / FK, autre logique en déclencheurs), vous voudrez peut-être regarder Oracle Workspace Gestion . Vous établissez un appel d'API pour mettre une table sous gestion de l'espace de travail, ce qui donne lieu à Oracle remplaçant la table avec une vue mise à jour et d'autres objets correspondants qui maintiennent une historique de toutes les versions des lignes. P>
J'ai utilisé cela et il y a des inconvénients, un avantage pour l'audit est que les objets de code sont tous générés par Oracle et leur exactitude est généralement supposée. P>
J'ai regardé l'outil de Workspace Mgmt. C'est vraiment gentil mais on m'a dit que nous avons besoin d'une licence distincte pour cela et ils ne veulent pas payer pour cela.
Je ne suis pas sûr des produits Oracle inférieurs, mais WM fait partie de l'édition d'entreprise.
La seule fois où je pourrais recommander que les enregistrements historiques soient stockés dans la même table que les enregistrements «actuels» se situent lorsque les liens FK sur les enregistrements doivent ou devraient être liés à ceux-ci. Par exemple, une application que j'ai vue a eu des liens FK qui relieraient à l'enregistrement à partir d'un "point à temps", c'est-à-dire que si l'enregistrement a été mis à jour, le FK serait toujours lier à l'historique - c'était un Une partie importante de la conception et de la séparation des enregistrements d'historique en une deuxième table aurait rendu plus lourd. p>
En dehors de cela, je préférerais qu'une obligation d'activité de suivi de tous les changements soit résolue à l'aide d'une table "Histoire" distincte pour chaque table. Bien sûr, cela signifie plus de DDL, mais il simplifie énormément le code d'application et vous bénéficierez également de meilleures performances et évolutivité. P>
En fait, dans notre cas, une table d'histoire distincte complique le code de l'application beaucoup plus ... Nous y accédons via Linq et l'accès à l'historique ou à l'actualité est très rapide / facile (traité dans une poignée d'opérateurs génériques / méthodes d'extension). Il y a très peu de filtrage de code ... mais l'inverse de la récupération d'historique d'une autre source serait formidable dans notre cas spécifique b>. Je suis d'accord avec vous en général, nous avons une utilisation / situation très spécialisée qu'il convient mieux ... mais bien sûr, je suis toujours ouvert aux alternatives si la solution globale b> est plus simple, plus facile, ou plus efficace.
Si quelqu'un a le même cas hautement spécialisé que nous le faisons (LINQ Access faisant une seule historique de table unique beaucoup plus propre / plus facile, c'est ce que j'ai fini par faire pour simplifier ce que nous avons, accueille des améliorations ... c'est juste un script qui fonctionnera chaque fois que la base de données change, la régénération des déclencheurs d'audit, le changement principal étant pragma autonome_transaction; code> placer l'historique générant sur une transaction autonome et ne pas se soucier de la mutation (qui n'a pas d'importance pour la façon dont nous 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;
/
Je comprends vos exigences de l'application SPECIFC pour avoir l'historique et les valeurs actuelles dans la même table, mais cela pourrait peut-être être géré en descendant plus d'une voie habituelle d'avoir une table d'audit séparée, mais la construisant comme une vue pseudo-matérialisée. Pour présenter une vue combinée pour l'application.
Pour moi, cela présente l'avantage d'avoir une vue simple "actuelle" et une vue "audit" distincte mais complètement automatisée (qui présente également la vue actuelle). < / p>
quelque chose comme: p>
@Nick - désolé, j'ai raté le point le premier tour. J'ai réécrit ma réponse pour répondre à votre problème.