Laden...

Versionierung / Audit Trails mit Entity Framework und Oracle ODP.NET

Erstellt von MrSparkle vor 9 Jahren Letzter Beitrag vor 9 Jahren 7.392 Views
MrSparkle Themenstarter:in
5.657 Beiträge seit 2006
vor 9 Jahren
Versionierung / Audit Trails mit Entity Framework und Oracle ODP.NET

verwendetes Datenbanksystem: Oracle / ODP.NET

Hallo allerseits,

ich bin mit der Aufgabe betraut, eine bestehende WebForms-Anwendung auf MVC mit EF umzustellen.

Das zugrundeliegende Datenmodell sieht so aus:


public abstract class AuditTrailEntity
{
  [Key]
  public int ID { get; set; }
  public int EntityID { get; set; }
  public DateTime ValidFrom { get; set; }
  public DateTime ValidTo { get; set; }
}

Es soll bei jeder Bearbeitung einer Entität also ein vollständiger Datensatz als aktuelle Version gespeichert werden. Dieser hat dann immer eine Gültigkeit von ValidFrom bis ValidTo, und es gibt für jede EntityID nur einen aktuell gültigen Datensatz.

Eine Entität mit einer Navigationseigenschaft sieht dann so aus:


public class Employee : AuditTrailEntity
{
  public int SupervisorID { get; set; }
  public virtual Employee Supervisor { get; set; }
}

Das kann aber nicht funktionieren, da für den Entity Framework die Beziehung nicht eindeutig ist. SupervisorID bezieht sich hier auf EntityID, und diese gilt ja für alle Versionen, nicht nur für die gerade aktuelle.

Ich habe damit also folgende Probleme:* Beim Abfragen der Datensätze muß ich alle Navigationseigenschaften manuell laden, indem ich nur den jeweils gültigen Datensatz auslese.

  • Beim Einfügen von neuen Datensätzen muß ich die Sequenz der Oracle-DB für die EntityID der aktuellen Entität abfragen.

Es gibt dafür meines Erachtens folgende Lösungen: * Entweder verwende ich Code-First, und muß das Auslesen der aktuellen Datensätze und das Erzeugen von neuen Datensätzen manuell durchführen, d.h. ich habe viel Arbeit und zusätzlichen SQL-Code in der Anwendung um beim Hinzufügen die richtige Sequenz abzufragen

  • Oder ich verwende für jede Entität zwei Tabellen, eine für die aktuellen Versionen und eine für die protokollierten Versionen. Damit würde ich aber das Datenmodell verändern, so daß andere Anwendungen nicht mehr damit arbeiten könnten
  • Oder ich verwende Database-First und benutze Stored Procedures für das Auslesen, Hinzufügen und Aktualisieren der Datensätze. Leider gibt es die Möglichkeit, Entitäten auf Prozeduren zu mappen, erst mit EF6, was wiederum von ODP.NET nicht unterstützt wird
  • Oder ich verwende Database-First mit dotConnect von DevArt, das funktioniert auch unter EF6

Gibt es evtl. eine weitere Option, die ich bisher übersehen habe?

Christian

Weeks of programming can save you hours of planning

Gelöschter Account
vor 9 Jahren

Ich habe vom Entity Framework keine Ahnug, leider lässt du auch offen ob die du die Basisklasse verändern darfst. Falls ja würde ich die ID Klasse auf Guid umstellen und das ID Property in der Basiklasse auf virtuell oder abstrakt setzen. int ist nie ein zuverlässiger Lieferant wenn es um Eindeutigkeit geht und es ist erlaubt hier Guid zu verwenden, das habe ich in Ralph Westphal's Buch zu Ado.Net anno 2002 zuallererst gelernt)

16.806 Beiträge seit 2008
vor 9 Jahren

Das sieht für mich aus wie eine Änderungsverlauf einer einzelnen Entität.

Vor genau dem Problem stande ich mit dem EF vor 1,5 Jahren.
Ich hatte ettliche Laufzeitprobleme bzw. Probleme mit der Performance, die unfassbar in den Keller ging. Der damalige Grund für mich auf NoSQL zu wechseln.
Das will ich Dir jetzt nicht unbedingt nahelegen, sondern Dich schon mal warnen 😉

Ich habe das am Ende so umgesetzt, dass ich für jede Entität 2 Tabellen habe.
EntityTable sind die aktuellen Daten
EntityTableHistory sind die Änderungsverläufe

Die Entität hat keine Eigenschaft, wie lange sie gültig ist. In der aktuellen Collection ist das ja auch nicht nötig.
In der History ist jede Entity aber gewrappt.

public class EntityHistory
{
public Guid ID {get;set;}
public Guid ValidFrom {get;set;}
public Guid ValidTo {get;set;}
public Entity Original {get;set;}
}

Wenn Du dies nicht machen kannst (Umstände, Vorgaben) dann sehe ich die gleichen Problematiken wie Du + meine.

742 Beiträge seit 2005
vor 9 Jahren

Das könnte vll. auch ein gutes Szenario für CQRS + EventStore sein. Der EventStore kann ja auch durchaus mit EF implementiert sein.

MrSparkle Themenstarter:in
5.657 Beiträge seit 2006
vor 9 Jahren

Hallo allerseits,

danke für die Ideen. Oracle als DB ist vorgegeben, daher kommt NoSql nicht in Frage.

Die Idee mit der EntityHistory hatte ich auch schon:

für jede Entität zwei Tabellen, eine für die aktuellen Versionen und eine für die protokollierten Versionen

Hat aber den Nachteil, daß ich das Model komplett ändern müßte, um es im EF abzubilden. Und ich müßte für jede Entität (es gibt sehr viele!), eine EntityHistory anlegen. Das gleiche trifft für den EventStore zu.

Eigentlich suche ich nach einer Möglichkeit, wo ich das vorgegebene Model mit möglichst wenig Änderungen im EF verwenden kann.

Wenn das nicht möglich ist, würde ich die Versionierung über Stored Procedures steuern, und diese dann auf die Entitäten mappen. Das geht aber meines Wissens nach nur mit dotConnect von DevArt und EF6.

Christian

Weeks of programming can save you hours of planning

MrSparkle Themenstarter:in
5.657 Beiträge seit 2006
vor 9 Jahren

Hi Abt,

wie würde denn in deinem Beispiel die Entity-Klasse aussehen, die du in der EntityHistory-Klasse wrappst? So wie ich es probiert habe, wird in der EntityHistory-Tabelle immer nur eine Fremdschlüssel-Spalte auf die Entity-Tabelle erzeugt:


CREATE TABLE DB."EmployeeHistory" (
       ID                         RAW(16)      NOT NULL,
       "ValidFrom"                RAW(16)      NOT NULL,
       "ValidTo"                  RAW(16)      NOT NULL,
       "Original_EmployeeID"      NUMBER(10, 0),
       PRIMARY KEY (ID),
       CONSTRAINT "FK_EmployeeHistory_Employee_Original_EmployeeID" FOREIGN KEY ("Original_EmployeeID")
       REFERENCES DB."Employee" (EmployeeID)
)

Christian

Weeks of programming can save you hours of planning

16.806 Beiträge seit 2008
vor 9 Jahren

Schwer zu vergleichen.
Ich kopier halt dank NoSQL das komplette Objekt in die Historie ohne eine Referenz benötigen zu müssen 😉

Aber so in der Art würde das schon auf MSSQL aussehen.
Ich würde evtl noch eine dritte Tabelle nehmen, das heisst EntityTable und EntityArchive ist genau die Entität, und über EntityHistory kannst Du dann die ValidFrom, ValidTo und die OriginalID nehmen. (Außer ich versteh es falsch).
Je mehr Relationen Du mit sichern musst, desto komplizierter wird das eben bei relationalen DBs.

public class User : Entity
{
}

public class UserHistory : Entity
{
   public User Original {get;set;}
}

Würde das sehr einfach bei NoSQL aussehen.

MrSparkle Themenstarter:in
5.657 Beiträge seit 2006
vor 9 Jahren

Aber ich brauch für die Versionierung eine Kopie, also alle Felder aus der Employee-Tabelle auch in der EmployeeHistory-Tabelle. Hast du eine Idee, wie man das mit einer Nicht-NoSql-Datenbank lösen kann?

// Edit: Wozu ist die dritte Tabelle gut?

Weeks of programming can save you hours of planning

16.806 Beiträge seit 2008
vor 9 Jahren

Willst Du die komplette Entität, oder nur gewisse Eigenschaften sichern?
Wie gesagt; je mehr Relationen Du (dauerhaft) über eine Historie sichern musst, desto aufwändiger ist es mit relationalen DBs.

Ich bin Fan davon, dass ich in meinen Tabellen mit aktiven Bewegungsdaten keine Historien hinterlege; dann ist ein Aufräumen einfacher (so macht es zB auch Amazon mit Bestellungen bzw. Facebook mit persönlichen Nachrichten).
Daher schiebe ich die Duplikate einer vollständigen Entität in eine Archiv-Tabelle.

MrSparkle Themenstarter:in
5.657 Beiträge seit 2006
vor 9 Jahren

Willst Du die komplette Entität, oder nur gewisse Eigenschaften sichern?

Die gesamte Entität mit allen Eigenschaften.

Ich bin Fan davon, dass ich in meinen Tabellen mit aktiven Bewegungsdaten keine Historien hinterlege; dann ist ein Aufräumen einfacher.
Daher schiebe ich die Duplikate einer vollständigen Entität in eine Archiv-Tabelle.

Letzteres versuche ich gerade nachzustellen. Ich hab eine Employee-Tabelle mit den aktuellen Entitäten und eine EmployeeHistory-Tabelle mit den archivierten Entitäten. Aber mir gelingt es nicht, das mit dem EF abzubilden.

Die einzige Möglichkeit, die ich sehe, ist wohl, alle Eigenschaften der Entität in der EntityHistory-Entität nochmal zu implementieren, wie in diesem Beispiel. Also ohne Wrapper wie in deinem Beispiel.

Das würde ich gerne irgendwie vereinfachen, um so wenig Code wie möglich schreiben und vor allem warten zu müssen.

Weeks of programming can save you hours of planning

16.806 Beiträge seit 2008
vor 9 Jahren

In meinem Vorschlag musst Du jede Relation duplizieren - das kann sehr viel werden 😉
Genau das war für mich der Grund RDBMS Tschüss zu sagen. Ich habs irgendwann nämlich aufgegeben .

Ob ich eine Audit-Logik im Schema einer Entität haben will. Ich weiß nicht...

MrSparkle Themenstarter:in
5.657 Beiträge seit 2006
vor 9 Jahren

Aber was wäre denn eine geeignete Lösung für ein RDBMS? Ich kann halt keine NoSql-Db verwenden.

Weeks of programming can save you hours of planning

16.806 Beiträge seit 2008
vor 9 Jahren

Wie war das denn beim EF und AsNoTracking() ?
Kopiert das dann evtl. die Relationen komplett mit, oder nur die spezifische Entität? Evtl. auch in Kombination mit Include().

MrSparkle Themenstarter:in
5.657 Beiträge seit 2006
vor 9 Jahren

Ich kann dir gerade nicht mehr folgen. Es geht mir ja erstmal nicht um das Tracken der Änderungen, sondern um ein geeignetes Datenmodell.

Weeks of programming can save you hours of planning

16.806 Beiträge seit 2008
vor 9 Jahren

Jop 😃
Wenn das funktioniert, dann macht es vieles einfacher - auch in Hinblick des Schemas. Das deaktivierte Tracking würde ein Neuanlegen statt Aktualisieren hervorrufen.

  • Du könntest zB die Entität Serialisieren und ein Bson/Json speichern - kommt drauf an, was Du damit machen willst.
  • Du könntest in eine zweite DB das Audit speichern, was den Charme hätte, das die Relationen ebenfalls komplett dupliziert werden und es auf die eigentliche, aktive DB keinerlei Auswirkungen hätte - weder Schema noch Performance.
MrSparkle Themenstarter:in
5.657 Beiträge seit 2006
vor 9 Jahren

Das sind zwei sehr gute Hinweise. Ich werde mir das mal durch den Kopf gehen lassen.
Danke schonmal für deine Hilfe!

Weeks of programming can save you hours of planning

MrSparkle Themenstarter:in
5.657 Beiträge seit 2006
vor 9 Jahren

Ich hab nochmal in Ruhe darüber nachgedacht und ein paar Sachen ausprobiert, und bin zu dem Schluß gekommen, daß es sich hier um einen Anwendungsfall handelt, bei dem der Entity Framework mehr Arbeit verursacht, als er einem abnimmt.

Das eigentliche Problem sind dabei immer die Fremdschlüssel. Während beispielsweise SuperVisorID in den aktuellen Datensätzen immer auf den eindeutigen Schlüssel EmployeeID verweist, gibt es in der History keine eindeutige Zuordnung mehr. Da ändert es auch nichts, wenn ich eine andere Datenbank verwende oder die Datensätze serialisiere und als Blob speichere.

Damit es funktioniert, müßte ich für jede Entität eine zuätzliche History-Entität anlegen, und alle Eigenschaften bis auf die Navigationseigenschaften kopieren. Wrappen oder Vererben hilft da leider nicht:


public class Employee
{
  public int ID { get; set; }
  public int SupervisorID { get; set; }
  public virtual Employee Supervisor { get; set; }
  // ...other properties and navigation properties...
}

public class EmployeeHistory
{
  [Key] 
  public int VersionID { get; set; }
  public int EmployeeID { get; set; }
  public int SupervisorID { get; set; }
  // ...other properties without navigation properties...
}

Das ist viel Arbeit und schlecht zu warten, und es bleibt das Problem, daß man bei jeder Bearbeitung einen Employee-Datensatz in einen EmployeeHistory-Datensatz umwandeln muß um die Daten zu kopieren. Weiterhin hat man beim Anzeigen der History dann das Problem, daß man nicht ohne weiteres auf die verknüften Datensätze zugreifen kann.

Wenn jemand noch eine Idee hat, wie man das Ganze vereinfachen könnte, wäre ich sehr dankbar.

Christian

Weeks of programming can save you hours of planning

16.806 Beiträge seit 2008
vor 9 Jahren

Wenn Du nur die ID der Relationen speicherst, wird das nicht verfälscht?

Wenn sich Supervisor ändert, dann stimmt doch auch die Historie nicht (mehr). Oder ist das in diesem Falle egal?

T
314 Beiträge seit 2013
vor 9 Jahren

Ich habe zuletzt die Versionierung/Historie innerhalb der gleichen Tabelle umgesetzt, da die Anzahl der Gesamteinträger eher vernachlässigbar ist und somit die Überlegung von "Archiv"-Tabellen verworfen wurde.

Dazu hat jede Tabelle die eben diese Versionierung benötigt ein weiteres Feld OriginId. Bei Änderungen am Datensatz wird über einen Trigger in der Datenbank eine Kopie des alten Datensatzen erstellt (mit gesetzter Referenz).

Zusätzlich kann es eben noch die Information des Änderungszeitpunktes geben, sodass ein genauer Ablauf erstellt werden kann.

EF-seitig hatte ich dadurch kaum Mehraufwand. Außer eben beim Auswerten dieser Informationen, dieser entsteht aber sowieso.

MrSparkle Themenstarter:in
5.657 Beiträge seit 2006
vor 9 Jahren

@Abt

Für die History gibt es 3 Anwendungsfälle:1.Versionen anschauen mit historischen Relationen. Dazu bräuchte man einen eindeutigen Verweis auf eine bestimmte Version, oder man verwendet einfach das Datum. 1.Versionen anschauen mit aktuellen Relationen. Dazu wird einfach auf die Employee-Tabelle verwiesen, wo die aktuell gültigen Werte drin stehen. 1.Wiederherstellen einer bestimmten Version. Hier wird nur der aktuelle Datensatz wiederhergestellt, nicht die verknüpften Datensätze.

Alle drei Anwendungsfälle funktionieren, wenn man nur die ID der Original-Entität speichert.

@t0ms3n

Wie sieht denn dein Model aus? Oder genauer gefragt, wie hast du die Relationen abgebildet?

Weeks of programming can save you hours of planning

T
314 Beiträge seit 2013
vor 9 Jahren

Die Relation sind genauso abgebildet. Je nach Anforderung muss natürlich beim Aufbereiten der Historie entschieden werden, ob nach der im Datensatz gefundenen Historienidee aufbereitet wird oder ob von verknüpften Datensätzen die zum jeweiligen Zeitpunkt gültigen Daten ausgelesen werden.


public class Employee
 {
   public int ID { get; set; }
   public int? OriginId { get; set; }
   public DateTime Modified{ get; set; }
   public int SupervisorID { get; set; }
   public virtual Employee Supervisor { get; set; }
   // ...other properties and navigation properties...
 }

Ändert sich direkt am Mitarbeiter z.B. der Supervisor, wird dies einfach gespeichert und ein neuer Datensatz mit den alten Daten erzeugt.

Möchte ich die alten Daten auswerten, so sind ja alle Daten vorhanden. Einzig wie gesagt bei den Relationen musst Du entscheiden ob du auf den aktuellen Datensatz der Relation gehst oder über das Änderungsdatum und OriginId auf den zu dem Zeitpunkt gültigen Datensatz zugreifst.

Bei n:m gibt es das gleiche Verfahren innerhalb der Mappingtabelle.

W
955 Beiträge seit 2010
vor 9 Jahren

Hallo,

sollen eigentlich die Historiendaten auch in der App dargestellt werden (das man einen früheren Zeitpunkt angibt und die App die damalige Situation darstellt) oder wird das nur wegkopiert für späteres Auswerten mit anderen Tools?

MrSparkle Themenstarter:in
5.657 Beiträge seit 2006
vor 9 Jahren

Hi t0ms3n,

ja, so funktioniert es. Vielen Dank für den entscheidenden Hinweis!

Hi witte,

sollte in der Anwendung auch dargestellt werden können, siehe die Anwendungsfälle in meinem letzter Beitrag. Warum ist das wichtig?

Christian

Weeks of programming can save you hours of planning

MrSparkle Themenstarter:in
5.657 Beiträge seit 2006
vor 9 Jahren

Hallo nochmal,

ich hab das ganze jetzt seit einer Weile im Einsatz, aber es gibt noch eine Sache, die mir noch nicht gefällt. Um mal bei dem Mitarbeiterbeispiel zu bleiben:


public class Employee
{
    public int Id { get; set; }
    public int OriginalId { get; set; }
    public AuditState AuditState { get; set; } // Enum values: Create/Updated/Deleted

    public int SupervisorId { get; set; }
    public virtual Employee Supervisor { get; set; }

    [InverseProperty("Supervisor")]
    public virtual ICollection<Employee> Subordinates{ get; set; }
}

Im Gegensatz zu den bisherigen Beispielen hat ein Angestellter nicht nur einen Vorgesetzen, sondern auch Untergebene. Wenn ich jetzt den Vorgesetzten abfrage, bekomme ich den korrekten, derzeit gültigen Datensatz angezeigt:


var supervisor = someEmployee.Supervisor;

Wenn ich aber die Untergebenen des Mitarbeiteres abfrage, dann muß ich folgende Einschränkungen hinzufügen, um nur die derzeitig gültigen herauszufiltern:


var subordinates = supervisor.Subordinates.Where(m => m.OriginalId == 0 && m.AuditState != AuditState.Deleted).ToList();

Für die Filterung habe ich mir eine eigene Erweiterungsmethode geschrieben, aber besonders elegant finde ich das nicht. Denn wenn man einmal vergißt, eine Auflistung zu filtern, bekommt man plötzlich alle Versionen aller Datensätze zurück.

Gibt es dafür noch eine etwas elegantere Lösung?

Christian

Weeks of programming can save you hours of planning

W
955 Beiträge seit 2010
vor 9 Jahren

Hallo,

kannst Du nicht ein Proxy-Objekt drübersetzen welches den Employee nach außen darstellt. Also die Kollektionen entsprechend filtert. Man könnte noch weitergehen: wenn ein bestimmtes Datum o.ä. dem Proxy bekannt gemacht wird könnte er die Situation zu diesem Zeitpunkt darstellen.

MrSparkle Themenstarter:in
5.657 Beiträge seit 2006
vor 9 Jahren

Hi witte,

meinst du ein EF-Proxy oder eine eigene Klasse. Wie würde das dann aussehen?

Christian

Weeks of programming can save you hours of planning

W
955 Beiträge seit 2010
vor 9 Jahren

Ich dachte einfach an das Proxymuster allgemein.



    public class CurrentEmployee
    {

        private Employee _model;


        public CurrentEmployee(Employee model)
        {
            _model = model;  
        }

        public int Id 
        { 
            get { return _model.Id; } 
            set { _model.Id = value; }
        }

        public int OriginalId 
        { 
            get { return _model.OriginalId; } 
            set { _model.OriginalId = value; }
        }

        ...

        public ICollection<Employee> Subordinates
        { 
            get { return _model.Subordinates.Where(m => m.OriginalId == 0 && m.AuditState != AuditState.Deleted).Select(p => new CurrentEmployee(p)).ToList(); }
            set { _model.Subordinates = value; }
        }
    }


    public class PastEmployee
    {

        private Employee _model;


        public PastEmployee(Employee model)
        {
            _model = model;  
        }

        public int Id 
        { 
            get { return _model.Id; } 
            set { _model.Id = value; }
        }

        public int OriginalId 
        { 
            get { return _model.OriginalId; } 
            set { _model.OriginalId = value; }
        }

        ...

        public ICollection<Employee> Subordinates
        { 
            get { return 
            
                // <gefilterte Liste>: suche alle Employees deren Änderungszeitpunkt vor dem (vllt statisch definierten) Grenzzeitpunkt liegen
                // gebe alle Employees aus deren Nachfolger nicht in <gefilterte Liste> liegen
            }
            set { _model.Subordinates = value; }
        }
    }

Dadurch geht der Typ verloren also kein Employee mehr. Weiß nicht ob das ein Problem für Dich ist.

MrSparkle Themenstarter:in
5.657 Beiträge seit 2006
vor 9 Jahren

Hi witte,

soetwas hatte ich auch in Erwägung gezogen. Allerdings müßte ich dann für jedes Model ein solches Proxy erstellen - und da gibt es so viele, daß der Aufwand dafür nicht zu vertreten wäre.

Kann man nicht evtl. steuern, wie der EntityFramework selbst die Objekte lädt und zur Verfügung stellt? Also in gewisser Weise das Standardverhalten überschreibt?

Christian

Weeks of programming can save you hours of planning

T
314 Beiträge seit 2013
vor 9 Jahren

Hmmm du könntest in deinem Repository (sofern generisch) sie defaultmäßig nicht laden.

W
955 Beiträge seit 2010
vor 9 Jahren

Kann man nicht evtl. steuern, wie der EntityFramework selbst die Objekte lädt und zur Verfügung stellt? Also in gewisser Weise das Standardverhalten überschreibt? Hmh, vllt solltest Du die Frage auf Stackoverflow stellen. Noch ein paar Ideen:
* man könnte eventuell mit T4 Templates arbeiten. Also die DB mit DatabaseFirst ziehen und sich dort die T4-Schablone anschauen um damit selber die Klassen bzw Proxies zu generieren.
* eine andere Möglichkeit wäre AOP, also Postsharp oder Castle Windsor Proxies. ich weiß aber nicht wieviel Aufwand es bedeutet das stabil zu kriegen und was mit der Performance passiert.
* eigener Provider? Einen bestehenden im Quelltext anpassen? Hört sich nach viel Arbeit an.

MrSparkle Themenstarter:in
5.657 Beiträge seit 2006
vor 9 Jahren

Hi t0ms3n,

die Werte standardmäßig nicht laden zu lassen hätte zumindest den Vorteil, daß man eine NullReferenceException bekommt, wenn man mal vergisst die richtigen Werte zu filtern bzw. manuell zu laden. Man muß dazu im Model nur das InversePropertyAttribute durch ein NotMappedAttribute vertauschen.

Hi witte,

die Vorschläge sind nicht schlecht, aber für meine Anforderungen einfach zu abwegig oder zu aufwändig. Ich wollte eigentlich einfach nur mit dem Entity Framework arbeiten 😃

Christian

Weeks of programming can save you hours of planning