HowToCode: Macht ein eigenes Model ohne Linq2Sql/ADO.NET EF Magic!
Der Einsatz von Linq2Sql oder der Einsatz vom ADO.NET Entity Framework ist sehr einfach – die Einstiegshürden sind erstmal niedrig und man bastelt seine Datenbank, generiert daraus sein Model und dann kann man schon loslegen und alles ist heile Welt. Der Titel hier ist etwas provokant, allerdings muss ich immer wieder erleben dass der Einsatz solcher Tools/Frameworks (es gibt sicherlich noch andere dieser Art) auch richtig schön schief gehen kann.
Erstmal langsam… es gibt ja auch gute Seiten
Ich bezieh mich hier nur auf Linq2Sql oder dem ADO.NET EF, sicherlich gibt es noch andere Beispiele wo es ähnlich ist. Beide Technologien sind eigentlich ganz nett, das EF wird sicherlich in Version 2 dann fortschritte machen und Linq2Sql ist halt sehr einfach.
Das tolle ist ja, dass ich mir keine Gedanken mehr um die SQL Code Generierung machen muss und (ich habe jetzt mal die Northwind DB genommen) auch alles schön da:
Und im Code hat man ebenfalls den kompletten Zugriff auf die Verzweigtesten Sachen (auch wenn das so kein Sinn macht) :
Products test = new Products();
test.Categories.CategoryName;
test.Suppliers.Address;
Customers cust = new Customers();
cust.Orders.First().Order_Details.First().Products;
Durch den Context kann man auch jedes Teil entsprechen nachladen oder beim EF kann man über "Load" Daten nachladen, wenn diese noch nicht geladen wurden.
Die Probleme dabei…
1. Problem ist, dass mein Model aufgebläht ist mit Sachen, die mich nicht interessieren:
Darunter "EntityKey", "EntityState", Orders (was nützlich ist) und "OrdersReference" – wenn man diese Objekte nun z.B. nach JSON serialisieren will, kann man in arge Schwierigkeiten kommen.
2. Problem ist, dass man durch "Load()" oder durch den Einsatz des Contextes überall Daten laden kann:
Wenn man aus jeder Schicht (sei es Businesslogik oder UI Schicht) Daten nachladen kann, dann kommt am Ende sowas raus:
Das Problem kann man umgehen, wenn man stark darauf achtet, dass es nur eine Stelle gibt an der Daten geladen werden. So wird auch im MVC Buch von ScottGu etc. empfohlen:
"For small applications it is sometimes fine to have Controllers work directly against a LINQ to SQL DataContext class, and embed LINQ queries within the Controllers. As applications get larger, though, this approach becomes cumbersome to maintain and test. It can also lead to us duplicating the same
LINQ queries in multiple places.
One approach that can make applications easier to maintain and test is to use a “repository” pattern. A repository class helps encapsulate data querying and persistence logic, and abstracts away the implementation details of the data persistence from the application. In addition to making application
code cleaner, using a repository pattern can make it easier to change data storage implementations in the future, and it can help facilitate unit testing an application without requiring a real database."
3. Problem ist, dass es trotzdem sehr leicht ist die Architektur kaputt zu machen:
Selbst wenn man ein Repository Pattern implementiert, bleiben viele EF oder Linq2Sql Sachen am Objekt hängen und man kommt sehr leicht dazu, "einfach mal so" auf die DB zu gehen. In einem größeren Team kann das durchaus recht schnell passieren.
4. Problem ist, dass nicht alles mit dem Entity Framework / Linq2Sql abgedeckt werden kann:
Selbst wenn ich mit den vielen Objektreferenzen leben kann und auch wirklich sehr sauber Arbeite, habe ich ein Problem wenn man z.B. plötzlich ein Webservice mit einbindet. Plötzlich hat man ein Teil der Klassen durch das EF generiert und ein anderer Teil kommt irgendwo anders her und kann sich auch anders Verhalten.
5. Problem ist, dass man dadurch sich sehr schnell festlegt und später Probleme bekommt:
Auch wenn momentan das Entity Framework von Microsoft weiterentwickelt wird, heisst das ja nicht, dass es sich nicht in 2 Jahren wieder ändern könnte. Wenn man nun in seiner Applikation überall mit dem Context rumgespielt hat und fiese Tricks angewandt hat, steht man vielleicht irgendwann vor einem Problem.
Daher: Von den Abhängigkeiten lösen und eine Abstraktionsschicht mehr
Ich mach es bei meinen Applikationen immer nach diesem Muster:
Das Mapping ist im Grund auch sehr einfach und hab ich mir damals bei Rob Conerys MVC Storefront abgeschaut – der Code kommt auch aus diesem Projekt:
public IQueryable<ProductReview> GetReviews() {
return from rv in _db.ProductReviews
select new ProductReview
{
ID = rv.ProductReviewID,
Author = rv.Author,
Body = rv.Body,
CreatedOn = rv.ReviewDate,
Email = rv.Email,
ProductID = rv.ProductID
};
}
Man macht einen ganz normalen Query und macht dann im select Statement sein Mapping auf seine POCOs – das wars
Fazit:
Es gibt sicherlich Nachteile bei der Variante, insbesondere wenn eine Persistenzschicht haben möchte und die mit Events um sich wirft sobald irgendwas geändert wurde, aber das juckt mich nicht.
Die Vorteile überwiegen meiner Meinung nach mehr. Man ist völlig flexibel und hat auch die volle Kontrolle, auch wenn man etwas mehr Tippen muss. Aber ich für meinen Teil habe festgestellt, dass Tipparbeit wesentlich unaufwändiger ist als rauszubekommen, warum man diverse Sachen nicht einfach serialisieren kann




GarlandGreene
13. March 2009
Manche ORM erfordern gar keine Änderung der zu persistierenden Objekte (z. B. NHibernate). NH hat auch effektive Mittel (Lazy Loading) gegen das n+1-Problem, wenn man Entity-Beziehungen in sein Modell integriert. Das einzige Problem, das man damit auch nicht in den Griff bekommt, ist Serialisierung. Hier muss man weiterhin genau überlegen, was man serialisiert und was nicht, sonst hat man im Anhang eines einzelnen kleinen Objekts den ganzen Objektbaum (und das kann schnell der gesamte Datenbankinhalt sein) mitserialisiert.
Matthias
13. March 2009
Hallo Robert,
Beim Mapping muss man aber auch immer aufpassen, wo man es einsetzt. Meiner Meinung nach sollte das kein Selbstläufer sein, ein Mapping führt immer auch zu einer höheren Codemenge und im Zuge dessen auch zu einer etwas schlechteren Änderbarkeit und Wartbarkeit. Gerade bei kleinen Anwendungen kann das schnell zu viel des Guten sein. Klar hat man dann keine enge Kopplung mehr und Dinge können leicht ausgetauscht werden, aber wenn keine Anforderung danach besteht, sollte man immer zweimal überlegen, ob man es einsetzt. Schließlich sollte auch KISS nicht vergessen werden. Ich will dir damit nicht widersprechen, sondern nur etwas relativieren…
EF in v2 soll ja POCO-Support besitzen, sodass man da vielleicht weniger Probleme und eine recht lose Kopplung bekommt. Ich freu mich drauf und bin gespannt, ob das so klappt, wie man sich das vorstellt
Bei EF v1 gibt es ein kleines Zusatzprojekt "EF Poco Adapter" (http://code.msdn.microsoft.com/EFPocoAdapter), mit dem sich sowas realisieren lässt. Ist aber ziemlich mühsam zu handhaben… also dann doch eher auf v2 warten.
Cu, Matthias
Thomas
13. March 2009
ACK, mache das genauso.
Und, was die Theorie noch bekräftigt: ich mache das so seit Jahren. Vorher mit IDataReader und Stored Procedures, jetzt mit LINQ to SQL, nächstes Jahr dann vielleicht mit dem ADO.NET EF oder nHibernate, je nachdem ob ich dann noch den SQL-Server oder eine andere Datenbank, oder sogar mehrere verwende.
Die Anwendung interessiert das dann alles nicht, denn die weiß nichts von einer Datenbank und spricht ja nur mit den Repository-Interfaces.
Insgesamt bin ich damit in den aktuellen Projekten sehr zufrieden, einziger Nachteil ist ab und zu fehlendes Lazy-Loading, weil ich meine Model-Objekte dumm halte. Das erfordert dann die ein oder andere GetXYZ() Methode mehr, womit sich aber leben lässt.
Robert Mühsig
13. March 2009
Rob Conery hatte in seinem MVC Storefront eine Art "Lazyloading" eingebaut. Dazu hat er einen eigene "LazyList" implementiert, den man einen LINQ Query geben konnte und sobald der Iterator aufgerufen wird, wird der Query gegen das Repository gemacht.
Andre Loker
13. March 2009
Ich arbeite ausschließlich mit NHibernate, da ich die fine Kontrolle über das Mapping zu schätzen weiß und ich es nicht mag, wenn mir ein ORM in meinem Domain Model "herumdoktert". Durch den Einsatz von Repositories kann ich dann auch sehr gut kontrollieren, wie viel von meinem Aggregat-Baum ich direkt laden möchte (JOIN fetching) – zur Vermeidung des SELECT 1 + N Problems.
Mein Domain Modell wird von einem Servicelayer nach außen hin abgekapselt, Kommunikation vom und zum UI oder von und zu Web Services geschieht über Data Transfer Objects. Das ist zwar extra Code, erlaubt mir aber wieder mehr Kontrolle über die Daten, die ausgetauscht werden. Außerdem kann ich nötigenfalls mein Domain Modell ändern ohne die DTOs ändern zu müssen, lediglich die DTO Builder müssen dann angepasst werden. Durch den Einsatz von DTOs habe ich auch kein Problem mit Serialisierung.
Was allerdings noch immer problematisch ist, sind Sachen wie das Decorator Pattern bei Domain Entities, da dadurch ja die Objekt Instanz verändert wird, was dem ORM sauer aufstößt. Ich muss allerdings zugeben, dass ich mich noch nicht wirklich mit der Lösung des Problems auseinandergesetz habe.
Thomas
13. March 2009
@Robert: muss ich mir nochmal anschauen – weißt du zufällig in welcher Folge das beschrieben wurde?
Was mir bei ihm und seinen Filtern überhaupt nicht gefiel war der fehlende Blickwinkel aus Performance- und damit Datenbanksicht.
Auch wenn wir heute die Anwendungen nicht datenzentriert entwickeln, spielt die Datenbank hinsichtlich Performance vor allem bei Webanwendungen später doch eine riesige Rolle. So kann ich beispielsweise nicht mal einfach 200.000 User-Datensätze auslesen um am Ende mit einem Filter 10 davon wirklich zu verwenden, sondern ich muss ein spezifisches Select auf diese 10 fahren.
Robert Mühsig
13. March 2009
Das mit der Lazylist: http://blog.wekeroad.com/blog/lazy-loading-with-the-lazylist/
Soweit ich weiß bauen die Filter nur ein Query zusammen – d.h. wenn man GetUsers().WithName("Hans").ToList() macht, dann werden nicht erst alle geholt und dann codeseitig gefiltert, sondern es wird direkt ein sauberer SQL Query zusammengebaut mit "…WHERE name=’hans’…"
Andreas
17. March 2009
Gehen einem nicht viele integrierte Funktionen verlohren, wenn man beim Auslesen gleich Umwandlung in POCO macht?
Habe dazu noch keine Tests gemacht, aber kann Context Änderungen von POCO beim Rückschreiben direkt verwalten oder muß jeder einzelne Wert wieder auf orginal Contextobjekt geschrieben werden?
Robert Mühsig
17. March 2009
Der Persistenzlayer geht dabei natürlich verloren. Der Context hat keine Ahnung von den POCOs und auch die anderen "Features" vom EF gehen verloren, aber man gewinnt Kontrolle. Daher auch mein Fazit
Jens
11. April 2010
Kannst du dazu ein kleines Beispielprojekt für eine N-Tier-Anwendung posten? Das wäre toll!
Robert Mühsig
13. April 2010
Ich hab hier gerade kein Beispielcode zur Hand, könnte aber wenn ich Zeit finde mal ein Beispiel darüber posten.
Schau dir doch mal diese zwei Screencasts von Rob Conery an – der hat das super erklärt (dauert auch nicht lang) :
http://blog.wekeroad.com/mvc-storefront/mvcstore-part-3/
http://blog.wekeroad.com/mvc-storefront/mvcstore-part-4/
Jens
13. April 2010
Hallo Robert,
erst nochmal danke für den tollen Artikel.
Ich versuche gerade eine kleine Beispielanwendung mit einer sauberen Schichtenarchitektur zu programmieren und dabei hat mir dein Post hier bereits sehr geholfen. Meine Schwierigkeiten liegen momentan hauptsächlich im DataAccessLayer (DAL). Ich möchte gerne einen unabhängigen DAL implementieren, mit dem es später einfach wird, die verwendete DataAccess-API (Linq2Sql, EF, NHibernate, etc.) auszutauschen. Ich würde gerne ein kleines Beispiel mit Linq2Sql und NHibernate oder EF (jeweils in eigener DLL) schreiben.
Leider bekomme ich das nicht hin. Ich habe im Netz bereits viele Ansätze gefunden. So wird im Zusammenhang mit Linq2Sql meist das repository-Pattern eingesetzt. Andere Ansätze versuchen einen gemeinsamen DataContext zu definieren. Ich bekomme das alles aber irgendwie nicht zusammen. Hast du eine Idee oder evtl. sogar ein kleines Beipsiel, wie man einen solchen DAL aufbauen könnte?
Danke und Gruß
Jens