Suchen

Anmeldung



Entity Framework (Teil 4)

E-Mail Drucken

Nachdem wir im dritten Teil bereits einfache Entitäten mit dem Designer des Visual Studio 2010 Beta 2 erstellt haben, soll nun das dort eingeführte Objektmodell ergänzt und mit Relationen angereichert werden. Nicht alles, was der Designer so im Code-behind anstellt muss uns dann aber gefallen, wie wir sehen werden.

Entity Framework (Teil 1)

Entity Framework (Teil 2)

Entity Framework (Teil 3)

Ergänzung des Klassenmodells

Das Schema unserer Beispielanwendung aus Teil 3 beinhaltete noch 2 weitere Entitäten neben Address. Dies sind namentlich Company und Employee. Um diese beiden Entitäten zu erstellen, nutzen wir diesmal nicht die Drag&Drop-Funktionalität des Designers, sondern bemühen die Funktion Add Entity. Der Unterschied ist entgegen unseren Erwartungen erheblich!

ef04_01Nach einem Rechts-Klick im Designer und der Auswahl von Add -> Entity erscheint der Dialog aus der Abbildung rechts. Dieser Dialog erlaubt es nun zunächst bereits vor dem eigentlichen Erstellen der Entities ein paar Einstellungen vorzunehmen.

Ändert man den Eintrag im Feld "Eintity name" so wird zur allgemeinen Überraschung sofort auch eine pluralisierte Benamung des Sets vorgeschlagen. Diese Automatik geht sogar ziemlich intelligent vor und erzeugt aus "Employee" richtigerweise den Vorschlag "Employees", aus "Company" wiederum richtig "Companies". Im englischen funktioniert das also recht zuverlässig. Ist der eingegebene Name bereits einer anderen Entity zugeordnet, wird eine "1" angehängt.

In diesem Fenster kann man auch gleich auf das primary-key-Feld eingehen.

Ich belasse die Einstellung bei "Id" und Int32, weil sie eigentlich ganz logisch ist. (In meinen Projekten nenne ich das Feld aus keinem bestimmten Grund immer "ID" mit großem "D". Es sieht für mich einfach logischer aus, auch wenn der vom Designer vorgeschlagene Ansatz konsequenter ist.

Ich nutze im weiteren Verlauf für beide Klassen dieses Fenster und füge je eine Entity für sie hinzu.

Company bekommt wie in Teil 3 gesehen die Eigenschaft "Label" vom Typ String. Die Eigenschaft "Address" aus unserem Objektmodell wird keine Scalar-Eigenschaft und daher hier noch nicht hinzu gefügt.

Employee bekommt die beiden String-Eigenschaften "Firstname" und "Lastname". Zum Schluß sieht das Objektmodell wie in der Abbildung links unten aus.

ef04_02

ef04_03Bevor wir uns im weiteren Verlauf den Beziehungen zwischen den drei Entitäten widmen, noch ein kritischer Blick auf die Eigenschaften.

Die Eigenschaft Label erlaubt im Moment eigentlich alles an Werten, solange sie in einem String daher kommen. Ein verantwortungsvoller Klassen-Designer sollte sich meiner Meinung nach aber nicht einzig und allein auf die Datenbank verlassen, was Validierungen geht. Der EF-Designer unterstützt einen Entwickler bei diesem Arbeitsschritt auf den ersten Blick. Wie wir noch sehen werden, ist das Ergebnis aber eher durchwachsen.

Klickt man eine Eigenschaft einer Entität an, werden im Eigenschaften-Bereich die hier möglichen Einstellungen sichtbar (siehe Abb. rechts).

Einige der Optionen sind selbst erklärend. Wie in der Abbildung gezeigt, schränke ich die Länge auf maximal 50 Zeichen ein. Da die Eigenschaft "Nullable" auf False steht, muss ich also mindestens 1, kann aber maximal 50 Zeichen in die Eigenschaft geben.

Die einzelnen Eigenschaften erkläre ich in der folgenden Tabelle:

Eigenschaft Beschreibung Standardwert
ConcurrencyMode Legt fest, ob vor dem Speichern auf zwischenzeitliche Änderungen in der Datenbank geachtet werden soll. Setzt man diese Eigenschaft auf "Fixed", überprüft EF vor dem Speichern die Datenbank und löst ggf. eine OptimisiticConcurrencyException aus.
None)
Default Value Legt den Standard-Wert für die Eigenschaft fest. (None)
Documentation Erlaubt Einfluss auf die ///-Kommentierungen der Code-Behind-Property einzugehen.
Entity Key Stellt ein, ob die Eigenschaft zum Primärschlüssel der Entität gehört. Es können auch zusammengesetzte Primärschlüssel erstellt werden, indem mehrere Eigenschaften hier den Wert True bekommen. False
Fixed Length Gibt bei String-Feldern an, der bestimmt, ob der Inhalt immer die gleiche Anzahl Bytes hat. Wenn dieser Eigenschaft eine Zahl zugewiesen wird, entspricht das dem Datentyp CHAR statt VARCHAR in SQL. Nicht zugeordnete Stellen werden dann mit Leerzeichen aufgefüllt. Die Leerzeichen kommen derzeit immer ans Ende des Strings. Die Länge des Strings wird über Max Length definiert. (None)
Getter Bestimmt die Sichtbarkeit des Get-Teils der Code-Behind-Property. Public
Max Length Die Anzahl der Stellen, die bei einem String maximal akzeptiert werden. (None)
Name Der Name der Eigenschaft.
Nullable Legt fest, ob das Feld auch leer sein darf. False
Setter Bestimmt die Sichtbarkeit des Set-Teils der Code-Behind-Property. Public
StoreGeneratedPattern Diese Eigenschaft ist extrem wichtig! Sie steuert, wer für die Erzeugung des Inhaltes einer Eigenschaft verantwortlich ist. Zur Auswahl stehen hier die Datenbank (Identity), die Entität selbst (Computed) oder niemand, falls die Eigenschaft quasi von selbst eineindeutig ist oder nicht zum Key der Entity gehört (None) (siehe Text). None
Type Der .NET-Datentyp. Bei Skalar-Eigenschaften ist dies einer der Werte-Datentypen. Bei anderen Eigenschaften können dies auch andere Objekte sein, z.B. bei ComplexType (siehe Text) String
Unicode Wird diese Eigenschaft auf True gesetzt, legt man explizit fest, dass die Eigenschaft datenbankseitig als Unicode-Datentyp gehalten werden soll (2 Byte/Zeichen). (None)

Bei Betrachtung der Tabelle muss man sich immer vor Augen halten, dass man Aussagen über Klassen trifft, die in der Designer-Datei (also Code-Behind) erzeugt werden. Die Eigenschaft Label der Company-Klasse sieht nach dem Screenshot oben wie folgt aus:

Listing #1: Eigenschaft Label
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/// <summary>
/// No Metadata Documentation available.
/// </summary>
[EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=false)]
[DataMemberAttribute()]
public global::System.String Label
{
get
{
return _Label;
}
set
{
OnLabelChanging(value);
ReportPropertyChanging("Label");
_Label = StructuralObject.SetValidValue(value, false);
ReportPropertyChanged("Label");
OnLabelChanged();
}
}
private global::System.String _Label;
partial void OnLabelChanging(global::System.String value);
partial void OnLabelChanged();

Jeder, der jetzt erwartet hätte, der Setter der Property würde die Länge auf die Einhaltung des Maximalwertes hin prüfen, wird leider enttäuscht. Hier passiert genau das, was ich eingangs kritisiert hatte. Auch der EF-Designer meint also mit Max Length die Ausprägung des späteren Feldes in der Datenbank. Meiner Meinung nach ein deutlicher Kritikpunkt und verbesserungswürdig.

Übrigens: Wer glaubt SetValidValue würde die Prüfung vielleicht intern vornehmen, der irrt. Ein kleiner Test, der bereits an dieser stelle der Programmierung lauffähig ist, demonstriert dies:

Listing #2: Testen des Versagens
1
2
Company c = new Company();
c.Label = "1234567890123456789012345678901234567890123456789012";

Hier wird keine Exception geworfen.

Das wichtige StoreGeneratedPattern

Um diese Eigenschaft komplett verstehen zu können, muss man sich mit Primary Keys bei relationalen Datenbanken auseinander setzen. Der primary key ist das einzige nach außen sichtbare Kriterium, dass einen Datensatz sicher von einem anderen der gleichen Tabelle unterscheidet. Somit sind primary keys DB-intern immer als UNIQUE CONSTRAINTS definiert. Schnell wurde klar, dass professionelle RDBMS eigene Mittel und Wege bieten müssen, um solche keys erstellen zu können.

Der SQL Server verfolgt immer schon einen sehr einfachen Ansatz. Man stellt die Eigenschaft IsIdentity eines Feldes auf true und kann dann über AutoIncrement steuern, dass sich das System selbst um das hochzählen einer somit immer eindeutigen Zahl kümmern soll. Diese Technik ist allerdings im ANSI-SQL-Standard nicht definiert und Oracle geht traditionell einen anderen Ansatz. Zu jeder Tabelle definiert man eine sog. Sequenz und über einen BEFORE-INSERT-Trigger holgt man sich deren nächsten Wert und setzt davon ausgehend den Inhalt des PK-Feldes.

Bis hierher wäre die Welt komplett in Ordnung, denn man könnte als Gesetz festlegen, dass sich Entitäten niemals selbst um die PKs kümmern und dies immer der Datenbank überlassen. Das geht aber nicht, weil es eine Menge DB-Systeme gibt, die eine solche Technik nicht anbieten. Manche haben sich als so unzuverlässig erwiesen (Access), dass Profis hier immer lieber einen Weg gehen. Wie auch immer: Man braucht also technische Möglichkeiten, um es den Entitäten selbst zu überlassen, die eindeutigen Werte zu generieren.

StoreGeneratedPattern erlaubt es nun für EF einzustellen, welche Seite sich um die Generierung kümmern wird. "None" heißt zunächst einmal, dass keiner sich hier um irgend etwas kümmert. Bei allen Nicht-PK-Eigenschaften ist das wohl die sinnvollste Option. Der Wert "Identity" soll in etwas aussagen: "Liebes EF-Modell, lass diesen Wert bloß in Ruhe!". Man sagt dem EF-Modell also, dass dieses Feld z.B. kein Bestandteil von INSERT- und UPDATE-Anweisungen wird (beim SELECT wird es aber befüllt). Die Option "Calculated" sagt schlussendlich aus, dass sich auf der Gegenseite niemand um das Feld kümmert und wir das übernehmen müssen.

Aufpassen: Keine dieser Einstellungen sorgt für automatische Generierung der entsprechenden Infrastruktur! Man kann also nicht einfach "Identity" setzen und die Datenbank macht sofort den Rest.

ComplexType

Bisher haben wir uns immer nur Scalar-Eigenschaften erstellt. Der EF-Designer lässt aber auch zu, komplexe Typen als Eigenschaft zu definieren. Der entsprechende "Add"-Punkt heißt folgerichtig "Complex Property". Wer, wie ich, erwartet hätte, dass man hier nun jede Klasse oder Struktur verwenden kann, wird leider, aber verständlicherweise, enttäuscht.

ef04_04Man muss dem EF zunächst komplexe Typen in der CSDL-Definition bekannt machen. Am einfachsten geht das im sog. Model Browser (siehe Bild rechts).

Ähnlich der Klassen-Ansicht in den bekannten Projekten zeigt er einen strukturierten Überblick über den Inhalt der CSDL-Defintiion, die letztlich hinter dem EF-Designer steckt. Im Screenshot habe ich bereits einen neuen eigenen komplexen Typen hinzugefügt, indem ich nach einem Rechtsklick auf "Complex Types" "Create ComplexType" ausgewählt. habe. Die beiden Eigenschaften "Key" und "Value" sind Strings, die ich als Skalar-Eigenschaften dem ComplexType hinzugefügt habe.

Die Arbeit mit ComplexType ist nicht absolut frei und unterliegt somit gewissen Einschränkungen. Zum einen dürfen Eigenschaften vom Typ ComplexType auf der Entität niemals NULL sein. Würde man das vergessen und SaveChanges aufrufen, bekommt man dann eine InvalidOperationException um die Ohren.

Komplexe Typen können außerdem nicht voneinander erben, was wahrscheinlich eher untergeordnet ist, aber u.U. wichtig. Was noch viel wichtiger ist, ist die Tatsache dass der Inhalt einer ComplexType-Eigenschaft quasi immer per lazy loading daher kommt. Ist also die Entität geladen, ist der Inhalt einer ihrer ComplexType-Eigenschaften noch leer (nicht NULL). Vor allem bei Vergleichen sollte man dies berücksichtigen.

Relationen

Entitäten für sich machen i.d.R. kaum einen Sinn. So richtig nützlich wird ein Modell erst durch die Abbildung von Beziehungen zwischen den Entitäten. Da man nach erfolgter Abbildung aller Beziehungen i.d.R. durch das komplette Modell navigieren kann (Man springt halt von Entität zu Entität) fallen die Eigenschaften, die die Beziehungen (oder auch Relationen) abbilden in die Kategory der Navigation Properties.

Anders, als beim Design der Datenbank, legen wir also keine Scalar-Eigenschaften an und helfen diesen dann die Relation über. Der Designer gibt uns vielmehr die Möglichkeit, Relationen über das Objekt "Association" anzulegen. Eine einzelne Association kann immer zwei Entitäten miteinander in Beziehung setzen.

Gleich hier wieder eine Kritik: Warum es Micrsoft für weise gehalten hat, einen von der relationalen Lehre abweichende neue Bezeichnung für Relationen einzuführen wird aus meiner Sicht ein Geheimnis des Konzerns bleiben!

ef04_06ef04_05Wir könnten nun wieder das Element "Association" aus der Toolbox nehmen und nacheinander zwei Entitäten anklicken, hätten aber erhebliche Nachteile. Stattdessen bringt ein Rechtsklick auf einen freien Bereich im Designer das Kontextmenu und dort wählt man "Add -> Association...". Es erscheint wieder ein genausso freundliches, wie hilfreiches Fenster (Abbildung).

Ich finde das Fenster freundlich, weil es bereits beim Erscheinen zeigt, was man tun soll/kann. Der Dialog hat sich meine ersten beiden Entitäten genommen und bietet gleich an, eine Beziehung zwischen ihnen anzulegen. Das einzige kleine Problemchen ist der verdrehte Name, aber das kann man ohne weiteres ändern.

Eine Association hat, wie gesagt, zwei Enden.Das "rechte" Ende im Dialog ist immer die Entität, die den Foreign-Key aufnehmen soll. Bei 1:1-Multiplizität ist das natürlich egal, weil beide Entitäten einen Verweis auf die andere bekommen. Ich wähle trotzdem die 1:1-Variante, weil eher unwahrscheinlich ist, dass eine Company sich die Adresse mit einer anderen teilt und anders herum (siehe Bild).

Extrem hilfreich ist das Fenster, weil es anbietet, eine sichtbare Foreign-Key-Eigenschaft in der jeweils rechten Klasse zu erstellen, die den ID-Wert des Datensatzes der linken Klasse speichert. Das kann enorm vorteilhaft sein, weil man später nicht die komplette Adresse laden muss, um z.B. zu erkennen, ob zwei Elemente die gleiche nutzen.

ef04_08ef04_07Im Designer selbst ergibt sich nach einem Klick auf "OK" das Bild wie in der ersten Abbildung links. Beide Entitäten erhalten je eine Navigation Property mit dem Namen der jeweils anderen. Bei Company ist eine Scalar-Eigenschaft "AddressId" vom Typ Int32 hinzugekommen.

Beide Entitäten werden mit einer Association - dargestellt durch eine gestrichelte Linie - miteinander verbunden.

Menschen, die ohnehin viel mit Design-Tools arbeiten (z.B. Visio, Rational usw.), fühlen sich hier sehr schnell heimisch. Allerdings sollte man das Ergebnis nicht einfach so hinnehmen, wie es ist. Dazu ein kleiner Blick auf die Code-Implementierung:

Listing #3: Implementierung in Company
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/// <summary>
/// No Metadata Documentation available.
/// </summary>
[XmlIgnoreAttribute()]
[SoapIgnoreAttribute()]
[DataMemberAttribute()]
[EdmRelationshipNavigationPropertyAttribute("TestModel", "AddressCompany", "Address")]
public Address Address
{
get
{
return ((IEntityWithRelationships)this).RelationshipManager.GetRelatedReference<Address>("TestModel.AddressCompany", "Address").Value;
}
set
{
((IEntityWithRelationships)this).RelationshipManager.GetRelatedReference<Address>("TestModel.AddressCompany", "Address").Value = value;
}
}
/// <summary>
/// No Metadata Documentation available.
/// </summary>
[BrowsableAttribute(false)]
[DataMemberAttribute()]
public EntityReference<Address> AddressReference
{
get
{
return ((IEntityWithRelationships)this).RelationshipManager.GetRelatedReference<Address>("TestModel.AddressCompany", "Address");
}
set
{
if ((value != null))
{
((IEntityWithRelationships)this).RelationshipManager.InitializeRelatedReference<Address>("TestModel.AddressCompany", "Address", value);
}
}
}
 

Es werden also zwei Eigenschaften generiert, die es mir später erlauben, direkt auf das verknüpfte Objekt zuzugreifen. Die AddressReference stellt dabei die eigentliche Verbindung zur Adress-Entity her, wird per Browsable-Attribut aber versteckt. Die Address-Eigenschaften kapselt diesen Zugriff nun auf sehr eingängliche und für .NET-Programmierer vor allem gewohnte Weise.

Jetzt erkennt man auch, dass die Eigenschaft Address nichts anderes als die gleichnamige NavigationProperty im Entity Designer ist.

Ausgehend von dieser Erkenntnis habe ich also die Eigenschaft Company in der Entity Address angeklickt und durch beherztes Drücken der Entf-Taste gelöscht. Aus Sicht meines Modelles macht es keinen Sinn, von einer Adresse auf eine Firma zu verweisen, was aber theoretisch machbar wäre.

Es sollte nun kein Problem darstellen, das gleiche auf die Entität Employee anzuwenden.

Fehlt eigentlich nur noch die Beziehung zwischen einer Firma und ihren Angestellten. Hier stelle ich das Fenster wie in der folgenden Abbildung ein:

ef04_09ef04_10

Eins ist aber doch anders, als bei der Beziehung zwischen Company und Address. Es wird referenzielle Integrität hinzugefügt. Wenn man die Associaton zwischen Employee und Address anklickt, wird im Eigenschaften-Fenster ein Wert bei Referential Constraint angezeigt. Das verkompliziert die Sache im Moment nur unnötig. Klickt man die Association doppelt an, erscheint ein Fenster, in dem man die Einschränkung per "Delete" entfernen kann.

Mein Modell ist nun fertig bzw. entspricht der Planung.

Rüber in die Datenbank

Wer bis hierher mitgemacht hat, wird feststellen, dass sich das ganze ohne weiteres Kompilieren lässt. Sollten hier Fehler auftauchen, haben sie wahrscheinlich den letzten Schritt nicht durchgeführt und die referentielle Integrität ist noch drin. Nutzen tut es nur noch nicht viel, weil es keine Datenbank gibt, die unserem Modell entspricht.

Der Designer bietet eine integrierte Funktion an, um das Modell in SQL-Code zu verwandeln. Ein Rechtsklick auf einen freien Bereich des Designers und Auswahl von "Generate Database from Model..." startet den "Generate Database"-Assistenten, der aus zwei Dialogen besteht:

ef04_11ef04_12

Schritt 1 ist dazu da, entweder eine bestehende DB-Verbindung als Ziel anzugeben, oder eine neue im VS anzulegen. Im wichtigeren zweiten Step kann man nun erstmals Einblick darin nehmen, was mit "Generate" gemeint ist. Die Unterstützung, die der Designer bisher bietet, beschränkt sich auf die Erzeugung eines ausführbaren SQL-Scripts. Klickt man im zweiten Fenster auf "OK" wird eine entsprechende SQL-Datei dem Projekt hinzugefügt.

Berechtigerweise warnt der Designer vorher noch, dass man nicht einfach gedankenverloren das Script ausführt, was im Visual Studio ohne weiteres möglich ist. Ist eine SQL-Datei geladen, zeigt das VS eine entsprechende Toolbar an:

ef04_13

Ich habe den Button "Execute SQL" grün hervorgehoben. Klickt man diesen an, versucht VS den Inhalt der SQL-Datei auf dem ausgewählten Server auszuführen.

Der Ergebnis-SQL-Code ist in keiner weise auf das verwendete Datenbank-Backend hin angepasst. Es werden (logischerweise) keine Indizes angelegt, es gibt kein Auto-Increment usw.

Bewertung und Einordnung

Es ist nun für mich an der Zeit, Farbe zu bekenennen und die Technologie Entity Framework bis hierher zu bewerten. Ich möchte betonen, dass es sich hier um meine persönliche Meinung handelt und natürlich dem Umstand geschuldet ist, dass auch ich meine Art zu programmieren habe. Trotzdem :-).

Entity Framework funktioniert genauso gut, wie andere OR-Mapper auch. Es bietet sehr ausgefeilte Tools, um nach eigenen Vorlieben zu arbeiten, egal, ob man mit der Datenbank beginnt oder mit dem Datenmodell.

Ich glaube trotzdem nicht, dass sich dieser Ansatz (Assistenten, Codegenerierung usw.) in mittleren bis großen Szenarien produktiver einsetzen lässt, als althergebrachte Techniken. Dazu müssten die Tools mindestens in der Lage sein, spezieller auf die Eigenheiten der DBMS einzugehen. Außerdem ist das Ergebnis einer einfachen SQL-Datei, die alle Elemente einfach immer wieder überbügelt zu schwach und praxisfern.

Ein Einsatzgebiet sind die vielen kleinen Anwendungen, die durch das Aufkommen von Webseiten, Silverlight usw. auch in Intranets immer häufiger werden. Ein Programmierer, der nur relativ schnell eine weitere Sicht auf bestehende Daten bauen soll, wird definitv gut bedient sein. Ebenfalls sehe ich Einsatzgebiete im semi-professionellen bzw. Amateur-Umfeld. Die Einstiegshürden in die DB-Programmierung sinken und Techniken können schneller erlernt werden.

Damit komme ich aber auch schon auf meinen Hauptkritikpunkt und der trifft gar nicht mal die Technik an sich. Leider wird wohl auch dieser gut gemeinte Ansatz in vielen Fällen zu noch mehr Ahnungslosigkeit und damit einhergehender Fahrlässigkeit im Umgang mit Datenbanken führen. In meinem beruflichen Leben werde ich beinahe täglich mit schrecklich konzipierten Datenbanken konfrontiert. Und warum? Weil es geht und weil Drag&Drop zwar schnell aber nicht stabil und gut ist.

Ich möchte mit diesem Unterpunkt einfach dafür sorgen, dass Technik, die gut funktioniert nicht über die Maßen gelobt wird und wende mich damit vor allem an weniger erfahrene Anwender, die u.U. glauben, mit EF den Stein der Weisen gefunden zu haben. Bitte glauben Sie mir: das ist EF nicht!

Resumé und Ausblick

EF funktioniert. Auch der Designer arbeitet annehmbar, wenn man die kleineren Fallstricklein kennt und vermeidet. Vielleicht wird Microsoft oder ein Drittanbieter in Zukunft fehlende Features hinzufügen und das Produkt damit alltagstauglicher machen. Gerade für größere Projekte wäre das extrem wichtig.

Im nächsten Teil wollen wir wieder mehr dem Titel codingfreaks gerecht werden und das Modell in C# benutzen.

 

 

 

 

Zuletzt aktualisiert am Donnerstag, den 18. Februar 2010 um 22:16 Uhr