Wir haben ein Basisobjekt mit 10 Kindobjekten und EF6-Code zuerst.
Von diesen 10 untergeordneten Objekten haben 5 nur einige (zusätzliche) Eigenschaften und 5 haben mehrere Eigenschaften (5 bis 20). Wir haben dies als Tabelle-pro-Typ implementiert, also haben wir eine Tabelle für die Basis und 1 pro Kind (insgesamt 10).
Dies erzeugt jedoch HUGE SELECT-Abfragen mit select case
und unions
überall, was ebenfalls die EF 6 Sekunden zum Generieren (beim ersten Mal) benötigt.
Ich habe über dieses Problem gelesen, und das gleiche Problem gilt für das Szenario "Tabelle-für-konkrete".
Was uns übrig bleibt, ist Table-per-Hierachy, aber das erzeugt eine Tabelle mit einer großen Anzahl von Eigenschaften, was auch nicht gut klingt.
Gibt es dafür eine andere Lösung?
Ich habe überlegt, die Vererbung möglicherweise zu überspringen und eine Vereinigungsansicht zu erstellen, wenn ich alle Elemente aus allen untergeordneten Objekten / Datensätzen abrufen möchte.
Irgendwelche anderen Gedanken?
Vielen Dank im Voraus.
Eine andere Lösung wäre, eine Art CQRS-Muster zu implementieren, wo Sie separate Datenbanken zum Schreiben (Befehl) und Lesen haben (Abfrage). Sie können die Daten in der gelesenen Datenbank sogar de-normalisieren, also ist es sehr schnell.
Wenn Sie davon ausgehen, dass Sie mindestens ein normalisiertes Modell mit referenzieller Integrität benötigen, ist Ihre Entscheidung wirklich auf Tabelle pro Hierarchie und Tabelle pro Typ zurückzuführen. TPH ist berichtet von Alex James aus der EF-Team und in jüngerer Zeit auf der Microsoft-Website für Datenentwicklung , um eine bessere Leistung zu erzielen.
Vorteile von TPT und warum sie nicht so wichtig sind wie Leistung:
Höhere Flexibilität, dh die Möglichkeit, Typen hinzuzufügen, ohne eine vorhandene Tabelle zu beeinflussen. Kein Problem, da EF-Migrationen es einfach machen, die erforderliche SQL zu generieren, um vorhandene Datenbanken zu aktualisieren, ohne die Daten zu beeinträchtigen.
Datenbankvalidierung wegen weniger Nullable Felder. Kein großes Problem, da EF Daten anhand des Anwendungsmodells validiert. Wenn Daten auf andere Weise hinzugefügt werden, ist es nicht zu schwierig, ein Hintergrundskript zur Überprüfung der Daten auszuführen. Außerdem sind TPT und TPC für die Validierung bei Primärschlüsseln schlechter, da zwei Unterklassentabellen möglicherweise denselben Primärschlüssel enthalten können. Sie haben das Problem der Validierung auf andere Weise.
Speicherplatz wird reduziert, da nicht alle Nullfelder gespeichert werden müssen. Dies ist nur ein sehr triviales Problem, besonders wenn das DBMS eine gute Strategie für die Behandlung von 'spärlichen' Spalten hat.
Design und Bauchgefühl. Eine sehr große Tabelle zu haben, fühlt sich ein bisschen falsch an, aber das liegt wahrscheinlich daran, dass die meisten db-Designer viele Stunden damit verbracht haben, Daten zu normalisieren und ERDs zu zeichnen. Eine große Tabelle scheint gegen die Grundprinzipien des Datenbankentwurfs zu verstoßen. Dies ist wahrscheinlich das größte Hindernis für TPH. Siehe diesen Artikel für ein besonders leidenschaftliches Argument .
Dieser Artikel fasst das Kernargument gegen TPH wie folgt zusammen:
Es ist nicht einmal im trivialen Sinne normalisiert, es macht es unmöglich, die Integrität der Daten zu erzwingen, und was am "großartigsten" ist: Es ist praktisch garantiert, dass es in großem Maßstab schlecht für alle nicht-trivialen Daten funktioniert.
Diese sind meistens falsch. Leistung und Integrität sind oben erwähnt, und TPH bedeutet nicht notwendigerweise denormalisiert. Es gibt nur viele (nullable) Fremdschlüsselspalten, die selbstreferenziell sind. So können wir die Daten genauso weiter entwickeln und normalisieren wie mit einem TPH. In einer aktuellen Datenbank habe ich viele Beziehungen zwischen Untertypen und habe ein ERD so erstellt, als wäre es eine TPT-Vererbungsstruktur. Dies spiegelt tatsächlich die Implementierung in Code-First Entity Framework wider. Hier ist zum Beispiel meine Klasse Expenditure
, die von Relationship
erbt, die von Content
erbt:
Die InversePropertyAttribute
und die ForeignKeyAttribute
stellen EF die Informationen zur Verfügung, die für die erforderlichen Self-Joins in der einzelnen Datenbank erforderlich sind.
Der Produkttyp wird auch der gleichen Tabelle zugeordnet (erbt auch von Content). Jedes Produkt hat eine eigene Zeile in der Tabelle und Zeilen, die Ausgaben enthalten, enthalten Daten in der Spalte ProductId
, die für Zeilen, die alle anderen Typen enthalten, null ist. Also die Daten ist normalisiert , nur in einer einzigen Tabelle platziert.
Das Schöne an der Verwendung von EF-Code ist, dass wir die Datenbank auf die gleiche Art und Weise gestalten, und wir implementieren sie (fast) auf die gleiche Weise, unabhängig davon, ob TPH oder TPT verwendet wird. Um die Implementierung von TPH in TPT zu ändern, müssen wir einfach eine Annotation zu jeder Unterklasse hinzufügen und sie neuen Tabellen zuordnen. Also, die gute Nachricht für Sie ist, es spielt keine Rolle, welchen Sie wählen. Einfach bauen, einen Stapel Testdaten generieren, testen, Strategie ändern, erneut testen. Ich denke, Sie werden TPH als Gewinner finden.
Nachdem ich ähnliche Probleme selbst erlebt habe, habe ich ein paar Vorschläge. Ich bin auch offen für Verbesserungen bei diesen Vorschlägen, da es ein komplexes Thema ist, und ich habe nicht alles durchgearbeitet.
Das Entity-Framework kann sehr langsam sein, wenn es um nicht-triviale Abfragen komplexer Entitäten geht - dh um solche mit mehreren Ebenen von Child-Collections. In einigen Leistungstests habe ich es schon lange nicht mehr ausprobiert. Theoretisch sollten EF 5 und höher kompilierte Abfragen zwischenspeichern (selbst wenn der Kontext entsorgt und neu instanziiert wird), ohne dass Sie irgendetwas tun müssen, aber ich bin nicht davon überzeugt, dass dies immer der Fall ist.
Ich habe einige Vorschläge gelesen, dass Sie mehrere DataContexts mit nur kleineren Teilmengen Ihrer Datenbank-Entities für eine komplexe Datenbank erstellen sollten. Wenn das für Sie praktisch ist, versuchen Sie es! Aber ich stelle mir vor, dass es bei diesem Ansatz Wartungsprobleme geben würde.
1) Ich weiß, das ist offensichtlich, aber es lohnt sich, dies zu sagen - stellen Sie sicher, dass Sie die richtigen Fremdschlüssel in Ihrer Datenbank für verwandte Entitäten eingerichtet haben, da Entity Framework diese Beziehungen verfolgt und Abfragen schneller generiert Sie müssen mit dem Fremdschlüssel verbinden.
2) Holen Sie nicht mehr als Sie benötigen. Eine Größe, die für alle Methoden geeignet ist, um ein komplexes Objekt zu erhalten, ist selten optimal. Angenommen, Sie erhalten eine Liste von Stammobjekten (um sie in eine Liste aufzunehmen) und Sie müssen nur den Namen und die ID dieser Objekte in der Liste des Stammobjekts anzeigen. Rufen Sie nur das Basisobjekt ab - nicht benötigte Navigationseigenschaften sollten nicht abgerufen werden.
3) Wenn die untergeordneten Objekte keine Sammlungen sind oder Sammlungen sind, Sie aber nur ein Element (oder einen Aggregatwert wie die Anzahl) benötigen, würde ich unbedingt eine Ansicht in der Datenbank implementieren und diese stattdessen abfragen. Es ist viel schneller. EF muss nicht arbeiten - alles in der Datenbank, die für diese Art von Operation besser gerüstet ist.
4) Seien Sie vorsichtig mit .Include () und das geht zurück zu Punkt 2 oben. Wenn Sie ein einzelnes Objekt + eine Child-Collection-Eigenschaft erhalten, verwenden Sie am besten nicht .Include (), da dann, wenn die Child-Collection abgerufen wird, dies als separate Abfrage durchgeführt wird. (Dadurch werden nicht alle Basisobjektspalten für jede Zeile in der untergeordneten Sammlung abgerufen)
BEARBEITEN
Folgende Kommentare hier sind ein paar weitere Gedanken.
Da es sich um eine Vererbungshierarchie handelt, ist es sinnvoll, separate Tabellen für die zusätzlichen Eigenschaften der erbenden Klassen + eine Tabelle für die Basisklasse zu speichern. Wie man Entity Framework gut machen kann, steht noch zur Debatte.
Ich habe EF für ein ähnliches Szenario (aber weniger Kinder) verwendet, (Datenbank zuerst), aber in diesem Fall habe ich die tatsächlichen Entity-Framework-Klassen nicht als Business-Objekte verwendet. Die EF-Objekte beziehen sich direkt auf die DB-Tabellen.
Ich habe separate Business-Klassen für die Basis- und die vererbende Klasse und eine Reihe von Mappern erstellt, die in sie konvertiert werden. Eine Abfrage würde ungefähr wie
aussehen %Vor%Nicht zu sagen, das ist der beste Ansatz, aber es könnte ein Ausgangspunkt sein? Die Abfragen sind in diesem Fall sicherlich schnell kompilierbar!
Kommentare willkommen!
Mit Tabelle pro Hierarchie haben Sie nur eine einzige Tabelle, also sind Ihre CRUD-Operationen natürlich schneller und diese Tabelle wird sowieso von Ihrer Domain-Ebene abstrahiert. Der Nachteil ist, dass Sie die Möglichkeit für NOT NULL-Einschränkungen verlieren, daher muss dies von Ihrer Business-Schicht ordnungsgemäß gehandhabt werden, um potenzielle Datenintegrität zu vermeiden. Das Hinzufügen oder Entfernen von Entitäten bedeutet auch, dass sich die Tabelle ändert. aber das ist auch etwas, das überschaubar ist.
Mit Tabelle pro Typ haben Sie das Problem, dass je mehr Klassen in der Hierarchie Sie haben, desto langsamer werden Ihre CRUD-Operationen.
Da Performance wahrscheinlich die wichtigste Überlegung hier ist und Sie viele Klassen haben, denke ich, dass Tabelle pro Hierarchie sowohl hinsichtlich der Leistung als auch der Einfachheit und der Berücksichtigung ein Gewinn ist Ihre Anzahl an Klassen.
Sehen Sie sich auch diesen Artikel an, genauer in Kapitel 7.1.1 (Vermeiden von TPT in Model First oder Code First-Anwendungen), wo sie angeben: "Wenn Sie eine Anwendung mit Model First oder Code First erstellen, sollten Sie die TPT-Vererbung aus Gründen der Leistung vermeiden."
Das EF6 CodeFirst-Modell, an dem ich arbeite, verwende ich mit Generics und einer abstrakten Basisklasse namens "BaseEntity". Ich verwende auch Generics und eine Basisklasse für die EntityTypeConfiguration-Klasse.
Für den Fall, dass ich einige "Spalten" von Eigenschaften für einige Tabellen wiederverwenden muss und es nicht sinnvoll ist, dass sie auf BaseEntity oder BaseEntityWithMetaData stehen, mache ich eine Schnittstelle für sie.
z. Ich habe einen für Adressen, die ich noch nicht beendet habe. Wenn also eine Entität Adressinformationen hat, wird IAddressInfo implementiert. Wenn Sie eine Entity an IAddressInfo übergeben, erhalten Sie ein Objekt mit nur AddressInfo.
Ursprünglich hatte ich meine Metadaten-Spalten als eigene Tabelle. Aber wie andere schon erwähnt haben, waren die Anfragen horrend und langsamer als langsam. Also dachte ich, warum benutze ich nicht einfach mehrere Vererbungswege, um zu unterstützen, was ich tun möchte, so dass die Spalten auf jeder Tabelle sind, die sie brauchen, und nicht auf den, die das nicht tun. Außerdem verwende ich mysql, das eine Spaltenbegrenzung von 4096 hat. Sql Server 2008 hat 1024. Selbst bei 1024 sehe ich keine realistischen Szenarien, um das auf eine Tabelle zu übertragen.
Und nicht meine Objekte erben auf eine Weise, dass sie Spalten haben, die sie nicht brauchen. Wenn diese Notwendigkeit entsteht, erstelle ich eine neue Basisklasse auf einer Ebene, um die zusätzlichen Spalten zu verhindern.
Hier sind genug Schnipsel aus meinem Code, um zu verstehen, wie ich meine Vererbung eingerichtet habe. Bis jetzt funktioniert es wirklich gut für mich. Ich habe nicht wirklich ein Szenario produziert, das ich mit diesem Setup nicht modellieren konnte.
%Vor%Nun, Code im Kontext von ModelCreating sieht so aus,
%Vor%Der Grund, warum ich Basisklassen für die Konfiguration meiner Entitäten erstellt habe, war, dass ich, als ich diesen Pfad gestartet habe, ein lästiges Problem hatte. Ich musste die gemeinsamen Eigenschaften für jede abgeleitete Klasse immer wieder konfigurieren. Und wenn ich eine der flüssigen API-Mappings aktualisiert habe, musste ich den Code in jeder abgeleiteten Klasse aktualisieren.
Bei Verwendung dieser Vererbungsmethode für die Konfigurationsklassen werden die beiden Eigenschaften jedoch an einer Stelle konfiguriert und von der Konfigurationsklasse für abgeleitete Entitäten übernommen.
Wenn PeopleConfig konfiguriert ist, wird also die Logik der BaseEntityWithMetaData-Klasse ausgeführt, um die beiden Eigenschaften zu konfigurieren, und wenn UserConfig ausgeführt wird usw. usw.
Drei verschiedene Ansätze haben unterschiedliche Namen in M. Fowlers Sprache:
Single Table inheritance
- Gesamte Vererbungshierarchie in einer Tabelle. Keine Joins, optionale Spalten für untergeordnete Typen. Sie müssen unterscheiden, welcher Kindtyp es ist.
Concrete Table inheritance
- Sie haben für jeden konkreten Typ eine Tabelle. Joins, keine optionalen Spalten. In diesem Fall wird eine Basistyp-Tabelle nur benötigt, wenn der Basistyp eine eigene Zuordnung benötigt (Instanz kann erstellt werden).
Class Table inheritance
- Sie haben eine Basistyp-Tabelle und untergeordnete Tabellen - die jeweils nur zusätzliche Spalten zu den Spalten der Basis hinzufügen. Joins, keine optionalen Spalten. In diesem Fall enthält die Basistypen-Tabelle immer eine Zeile für jedes Kind; Sie können jedoch allgemeine Spalten nur dann abrufen, wenn keine untergeordneten Spalten erforderlich sind (Rest kommt möglicherweise mit Lazy Loading?).
Alle Ansätze sind praktikabel - sie hängen nur von der Menge und Struktur der Daten ab, die Sie haben, so dass Sie zuerst die Leistungsunterschiede messen können.
Die Auswahl basiert auf der Anzahl der Joins im Vergleich zur Datenverteilung und den optionalen Spalten.
Obwohl die Tabelle pro Hierarchie (TPH) ein besserer Ansatz für schnelle CRUD-Operationen ist, ist es in diesem Fall unmöglich, eine einzelne Tabelle mit so vielen Eigenschaften für die erstellte Datenbank zu vermeiden. Die von Ihnen erwähnten case- und union-Klauseln werden erstellt, da die resultierende Abfrage effektiv eine polymorphe Ergebnismenge mit mehreren Typen anfordert.
Wenn EF jedoch eine geglättete Tabelle zurückgibt, die die Daten für alle Typen enthält, wird zusätzliche Arbeit geleistet, um sicherzustellen, dass für Spalten, die für einen bestimmten Typ irrelevant sein können, Nullwerte zurückgegeben werden. Technisch gesehen ist diese zusätzliche Validierung mit case und union nicht erforderlich Das folgende Problem ist ein Performance-Fehler in Microsoft EF6 und sie sind Ziel dieses Update in einer zukünftigen Version zu liefern.
Die folgende Abfrage:
%Vor%kann sicher ersetzt werden durch:
%Vor%Sie haben also gerade das Problem und den Fehler der aktuellen Version von Entity Framework 6 gesehen. Sie haben die Wahl, entweder einen ersten Modellansatz zu verwenden oder einen TPH-Ansatz zu verwenden.
Tags und Links c# entity-framework entity-framework-6