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.
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
Gibt es evtl. eine weitere Option, die ich bisher übersehen habe?
Christian
Weeks of programming can save you hours of planning
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)
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.
- performance is a feature -
Microsoft MVP - @Website - @AzureStuttgart - github.com/BenjaminAbt - Sustainable Code
Das könnte vll. auch ein gutes Szenario für CQRS + EventStore sein. Der EventStore kann ja auch durchaus mit EF implementiert sein.
ImageTools for Silverlight: http://imagetools.codeplex.com | http://www.silverdiagram.net | http://www.cleancodedeveloper.de b:::
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
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
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.
- performance is a feature -
Microsoft MVP - @Website - @AzureStuttgart - github.com/BenjaminAbt - Sustainable Code
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
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.
- performance is a feature -
Microsoft MVP - @Website - @AzureStuttgart - github.com/BenjaminAbt - Sustainable Code
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
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...
- performance is a feature -
Microsoft MVP - @Website - @AzureStuttgart - github.com/BenjaminAbt - Sustainable Code
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
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().
- performance is a feature -
Microsoft MVP - @Website - @AzureStuttgart - github.com/BenjaminAbt - Sustainable Code
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
Jop 😃
Wenn das funktioniert, dann macht es vieles einfacher - auch in Hinblick des Schemas. Das deaktivierte Tracking würde ein Neuanlegen statt Aktualisieren hervorrufen.
- performance is a feature -
Microsoft MVP - @Website - @AzureStuttgart - github.com/BenjaminAbt - Sustainable Code
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
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
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?
- performance is a feature -
Microsoft MVP - @Website - @AzureStuttgart - github.com/BenjaminAbt - Sustainable Code
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.
@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
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.
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?
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
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
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.
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.
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
Hmmm du könntest in deinem Repository (sofern generisch) sie defaultmäßig nicht laden.
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.
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