Thomas Bandt

Über mich | Kontakt | Archiv

Nhibernate und der (Un-)Sinn von Repositories

Vor einem Monat stellte Robert die Sinnfrage bezüglich der Verwendung einer „3-Schichten-Architektur“, dem wohl gängigsten Modell für (kleine) Applikationen und Websites. Auch ich arbeite so. Seit wenigstens 5 Jahren, auch wenn ich damals die Klassen für den Datenzugriff noch nicht Repositories genannt habe.

Vor drei Wochen bin ich an den Punkt gekommen, an dem ich mit meinen Repositories und Linq to Sql als OR-Mapper in ein Dillema kam.  Nehmen wir mal folgendes Szenario: es gibt einen Artikel, der von einem User veröffentlicht worden ist. Dieser Artikel kann von anderen Usern kommentiert werden. In Code ausgedrückt sähe das so aus:

   1:  public class Article
   2:  {
   3:      public int ID { get; set; }
   4:      public string Headline { get; set; }
   5:      public string Text { get; set; }
   6:      public User Author { get; set; }
   7:      public IEnumerable<Comment> Comments { get; set; }
   8:  }
   9:   
  10:  public class User
  11:  {
  12:      public int ID { get; set; }
  13:      public string Name { get; set;
  14:  }
  15:   
  16:  public class Comment
  17:  {
  18:      public int ID { get; set; }
  19:      public User Author { get; set; }
  20:      public string Text { get; set; }
  21:  }

Nun wird der Artikel in unterschiedlichen Formen verwendet. Bei einer Auflistung aller Artikel beispielsweise interessiert als Information lediglich die Überschrift und eventuell noch der Name des Autors und die Anzahl der Kommentare. Nicht aber, wer die Autoren der einzelnen Kommentare sind. Was also tun?

Möglichkeit 1:

An Stelle der „echten“ Objekte werden nur die Foreign-Keys verwendet. Article und Comment enthalten also keine Eigenschaft „User Author“, sondern nur „int AuthorID“. Damit ist die Möglichkeit geschaffen die weiteren Informationen bei Bedarf zu holen, man spart sich aber den zeitintensiven Aufwand dies automatisch zu tun.

   1:  public interface IArticleRepository
   2:  {
   3:      Article  GetArticleByID(int id);
   4:      IEnumerable<Article> GetAllArticles();
   5:  }
   6:   
   7:  public interface IUserRepository
   8:  {
   9:      User GetUserByID(int id);
  10:  }
  11:   
  12:  public interface ICommentRepository
  13:  {
  14:      IEnumerable<Comment> GetCommentsByArticle(int articleID);
  15:  }

Möglichkeit 2:

Man nutzt die Objekte wie oben dargestellt und lädt immer alle Informationen und mappt diese dann auch.

Möglichkeit 3:

Man nutzt „Lazy Loading“, ein Design Pattern, welches kurz gesagt dafür sorgt, dass die benötigten Informationen erst dann geladen werden, wenn sie auch benötigt werden. Bei der Auflistung aller Artikel würden die Autoren der Kommentare zu den Artikeln also nicht geladen, bei der Detaildarstellung des Artikels plus aller seiner Kommentare hingegen schon. Den Aufrufer muss das aber nicht interessieren, er nimmt sich, was er benötigt und was das API hergibt.

Bisher

Die Realität sah bei mir jahrelang so aus, wie in 1. beschrieben. Im Web muss man einfach noch mehr auf Performance achten, als anderswo. Doch das hat natürlich massive Implikationen auf das „Design“ der Anwendung, wenigstens auf die Art und Anzahl der Methoden des Repositories. Einfach weil man für jeden (Sonder-)Fall eigene Zugriffsmethoden braucht, insbesondere bei Collections.

Irgendwann kam mir dann die Erkenntnis, dass „richtige“ Objekte natürlich attraktiver sind und man sich so einen Haufen Methoden spart. Nur ist es ziemlich gefährlich und blödsinnig, beim Kommentar den User nur bei GetArticleByID() zu setzen, nicht aber bei GetAllArticles(). Das funktioniert höchstens so lange, wie man selbst damit arbeitet, und auch nur am Anfang. Nach 2 Monaten bemerkt man erst bei der nächsten NullReferenceException, dass der Autor nicht gesetzt ist. Und welche Implikationen hat es dann, wenn man ihn nun doch setzt? Also erweitert man das Repository um eine weitere Methode ...

Dass das keinen Sinn macht, war schnell klar. Also füllte ich einfach jedes Objekt, so dass es in jeder Situation verfügbar war. Die Performance-Implikationen wurden daraufhin sofort deutlich. Um beim Beispiel zu bleiben: Nehmen wir drei Artikel mit insgesamt 50 Kommentaren. Für die Darstellung dieser drei Artikel in der Übersicht wird nun 53 Mal IUserRepository. GetUserByID() aufgerufen – obwohl 50 Aufrufe davon völlig blödsinnig sind.

Dagegen hilft dann nur massives Caching – was aber nicht die Aufrufe reduziert und damit das Grundübel beseitigt, sondern lediglich den Effekt der einbrechenden Performance kaschiert.

Der neue Ansatz

Da ich nichts davon halte, die vom OR-Mapper generierten Objekte als mein „Model“ in der gesamten Anwendung zu verwenden, habe ich schon immer eigene POCOs verwendet, auf die dann gemappt wurde.

Früher schon mit dem SqlDataReader, in den letzten Jahren dann mit Linq to Sql. Dazu habe ich am Repository immer eine harte Grenze gezogen: das heißt ich habe immer List<> statt IEnumerable oder IQueryable geliefert, um zu verhindern, dass Datenzugriffe zu einem späteren Zeitpunkt ausgeführt werden und zu ungewünschten Seiteneffekten führen (wenn die Verbindung zur Datenbank z.B. wieder geschlossen ist). Das alles verhinderte auch den leichten Einsatz von Lazy Loading.

Also war es an der Zeit sich nach Alternativen umzusehen. Meine Wahl fiel dabei auf Fluent NHibernate und Linq to NHibernate. Das hat mich zwar viele Stunden und sicher einige graue Haare gekostet (insbesondere bei den Mapping-Tests), doch unterm Strich habe ich genau das erhalten, was ich haben wollte: Ich kann meine eigenen Objekte weiterverwenden und dabei zu 100% Lazy Loading nutzen.

Das hat natürlich enorme Auswirkungen auf das Design, da viele Methoden der vorhandenen Repositories wegfallen. Im Grunde benötige ich jetzt nur noch das:

   1:  public interface IArticleRepository
   2:  {
   3:      Article  GetArticleByID(int id);
   4:      IEnumerable<Article> GetAllArticles();
   5:  }

Und wie ist das nun mit den Repositories?

Der Charme, der Repositories innewohnt, bassiert auf dem Gedanken, dass man die dahinter liegende Technologie für den Datenzugriff beliebig austauschen kann. Z.B. habe ich kürzlich eine zwei Jahre alte Komponente erweitert, bei der ich damals noch mit ADO.NET und Stored Procedures arbeitete [sic!]. Hier war es kein Problem neue Funktionen mit Linq to Sql im Repository zu erweitern.

Allerdings funktioniert das immer nur dann, wenn man keine massiven Feature-Unterschiede zwischen den Technologien hat, deren Vorteile man dann hinter den Mauern des Repositories verstecken muss. Ein Punkt, vor dem Ayende gewarnt hat, und der auch Anlass für Roberts Post gewesen ist. Und welcher nun erreicht ist.

Mit jeder Technologie, das wird hier deutlich, ändern sich sehr wahrscheinlich auch die Anforderungen an das Repository bzw. sein API. Wo ich mit Linq to Sql mangels Lazy Loading zwangsläufig andere bzw. mehr Methoden benötigte, reduziert sich das mit NHibernate ganz deutlich.

In diesem, zugegeben sehr reduzierten, Beispiel entfallen nun sogar IUserRepository  und ICommentRepository komplett, da die hier bereitgestellten Methoden nicht mehr benötigt werden – das Mapping von NHibernate macht sie obsolet.

Was mich zur nächsten Frage bringt:

Kann man auf Repositories nicht gänzlich verzichten?

Der Hauptkritikpunkt aus der Praxis, die stupiden Aufrufweiterleitungen aus der Businessschicht, die auf den ersten Blick nur Redundanzen im Code erzeugen, bleibt ja bestehen. Und wenn die einfache Austauschbarkeit entfällt und das API der Repositories und damit auch deren Nutzung in den Business-Klassen bei einer Änderung sowieso mit hoher Wahrscheinlichkeit geändert werden müssen, dann kann man doch gleich darauf verzichten und wie von Ayende vorgeschlagen NHibernate direkt dort verwenden.  Oder?

Ein Punkt bleibt: die Testbarkeit. Der zweite massive Vorteil der Repositories sind ihre lose Koppelung vom Rest der Anwendung, was auch bedeutet, dass sich die Business-Klassen sehr einfach testen lassen. Man mockt den Datenzugriff dort einfach weg und spart sich eine Menge Overhead.

Wie ist das nun mit NHibernate? Ich fasse mich kurz: ISession lässt sich natürlich mocken, da aber der Zugriff auf das mir lieb gewonnene Linq to NHibernate über eine Extension-Method funktioniert, ist es unmöglich den Datenzugriff ohne Verwendung einschlägiger kommerzieller Mocking-Tools zu faken.

Was bliebe wäre der Einsatz einer echten Datenbank, was aus den Unit- nicht nur Integrationstests machen würde sondern auch viel Performance und Zeit kostet. Zumal ich aus meinem steinigen Weg hin zu funktionierenden Mapping-Tests weiß, dass es einen massiven Unterschied macht, ob man nun eine In-Memory-SQLite-Datenbank oder eben eine „richtige“ SQL-Server-Datenbank verwendet (wenn man diese auch produktiv einsetzen möchte).

Fazit

Das Konzept von Fluent NHibernate und NHibernate überhaupt gefällt mir gut, nicht nur weil es mir die Möglichkeiten des Lazy Loadings geschenkt hat und den Code für den Datenzugriff massiv reduziert hat.

Ich werde es definitiv in der Praxis testen, ein größeres Projekt, an dem ich arbeite, ist bereits vollständig umgestellt.

Allerdings werde ich die Zugriffe weiterhin in Repositories kapseln, um meine Business-Klassen so einfach testbar zu halten, wie bisher auch. Zumindest so lang, bis mir jemand einen Weg zeigt, wie die Aufrufe zu mocken sind ;-).

Kommentare

  1. Ken schrieb am Sonntag, 5. September 2010 20:28:00 Uhr:

    Was mir bei der ganzen Diskussion um den (Un)sinn von Repositories immer wieder auffällt: Es wird immer nur von Datenbanken gesprochen. Was ist aber mit den ganzen Webservices und Active Directories dieser Welt, die auch nichts anderes als Repositories sind!? Gilt dort etwa auch Ayendes Zitat: "Data access is a solved problem"? Verzichtet man hier auch auf Repositories? Nein, das würde sicherlich keiner von uns tun. Oder wollen wir das DirectoryServices-API immer wieder neu ansteuern?

    Zu dem Problem mit den dutzenden Methoden in den Repositories:

    Was hab ich getan, um die Repositories für diverse Anforderungen seitens Business Logik abdecken zu können?
    Bleiben wir bei Thomas' Szenario. Ich will Artikel laden, mal mit Kommentaren, mal ohne.

    Mein IArticleRepository bietet mir die Methoden GetArticle(), GetArticles(), GetArticleCount(). Als Parameter kann ich mitgeben, was ich bei den einzelnen Artikel gern sehen würde - LoadWithComments, LoadWithAuthor, LoadWithCommentsAuthors.
    Anhand dieser Parameter sagt die Business Logik meinen Repositories, wie der zu lieferende Artikel auszusehen hat. Und genauso bekommt sie es auch.
    Damit habe ich einen für mich akzeptablen Weg gefunden, meine Repositories auf 3-Get-Methoden zu reduzieren und hab trotzdem die Möglichkeit, alle Anforderungen damit abzudecken.
    Ein weiterer Parameter enthält dann noch die Filterkriterien - WithId, WithAutorId etc, über den ich die Abfrage genaueren spezifizieren kann.

    Aktuell ist das für mich die beste Lösung.
  2. Thomas schrieb am Montag, 6. September 2010 12:19:00 Uhr:

    Den ersten Punkt verstehe ich nicht.

    Zum zweiten: sieht nach einem definitv gangbaren Weg aus. Genauso wenig spricht gegen mehr Methoden im Repository. Alles ok.

    Der Kern ist nur:

    Egal ob 10 Methoden oder 3 mit Filterkriterien, wechselst du den dahinter liegenden OR-Mapper, kann es dir passieren, dass welche davon obsolet werden und/oder, wenn du sie weiter konsequent einsetzt, du auf konkrete Vorteile des ORM verzichtest. So wie ich es bisher getan habe.

    Daraus folgt, dass es meist eine Illusion ist, zu glauben, dass der ORM einfach getauscht werden könnte. Und da der sowieso schon für sich ne Menge kapselt, kann man im Prinzip auch auf die Repositories verzichten und den ORM direkt in der "Business Schicht" verwenden.

    Wenn, ja wenn sich das dann eben vernünftig mocken lässt.
  3. Ken schrieb am Montag, 6. September 2010 13:37:00 Uhr:

    Die Argumentation, dass man hinter den Repositories einige Vorteile des ORM versteckt, kann ich nachvollziehen. Bisher kann ich damit leben. Wie lange, wird sich zeigen.

    Punkt 1 ist eher ein grundsätzliches Problem, was ich mit der gesamten Diskussion habe. Wir reden darüber, Repositories sein zu lassen und Abfragen direkt in der Business Schicht zu machen. Abfragen über einen ORM gegen eine Datenbank.

    Wie sieht es aber mit anderen "Repositories" auf, von denen zusätzlich Daten geholt werden sollen? Nehmen wir das DirectoryServices-API, über das wir beispielsweise Metadaten eines Benutzers aus einem Active Directory auslesen wollen. Verzichten wir hier dann auch auf ein IUserRepository und sprechen direkt aus der Business Schicht mit dem Active Directory?

    Es gibt nun mal nicht nur Datenbanken, welche eine Anwendung mit Daten füttern können. Für diese zieht das Argument des ORM nicht mehr.

    Was ich damit sagen will: Mit zunehmender Anzahl an Datenquellen haben Repositories in meinen Augen ihre Daseinsberechtigung. Bei kleinen Projekten mit nur einer Datenbank im Hintergrund mag das natürlich anders aussehen.
  4. Thomas schrieb am Montag, 6. September 2010 15:34:00 Uhr:

    Na zumindest ich bin ja nun kein Gegner von Repositories. Ich werde sie weiterhin einsetzen, vor allem weil ich nicht vorhabe alles auf NHibernate umzustellen und gleichzeitig keine Lust die Objekte von Linq to SQL als Model zu nutzen.

    Bzgl. des AD hast du natürlich recht, genauso verhält es sich imho mit Text-, XML-, CSV, whatever für Files, aus denen man sich bedient oder in die man schreibt.
  5. Timur Zanagar schrieb am Samstag, 25. September 2010 14:03:00 Uhr:

    Ich bin auch der Meinung das Repositories nicht gestorben sind, sondern sehr sinnvoll. Sieht man auch an deinem Beispiel das du von SqlDataReader zu Linq2Sql und dann NHibernate teilweise migriert bist. Es gibt immer irgendwelche Beweggründe Repositories auszutauschen. Man muss nicht immer alles in einer Anwendung kapseln, aber man sollte nicht vergessen das Anforderungen sich gerne ändern.
  6. Jörn von Holten schrieb am Samstag, 12. April 2014 17:18:00 Uhr:

    Interessante Gedanken! Meine etwas spitze Anregung wäre, sich dazu durchzuringen, die Nutzungsschicht (View und High-Level Businesslogik) nicht auf POCOs sondern durchgängig auf Interface-Typen (echte Domain-Objekte mit echtem semantischen live cycle) zu formulieren und das Repository-Pattern durchgängig bei allen Collections/Repositories anzuwenden. Alles was danach kommt ist Implementierung und wird Injected (StructureMap z.B.). Nun kann man zwar die Goodies der (möglichen) Implementierung da "oben" hochziehen, aber der ganze Schmonz mit Lazy oder Eager sollte aus dieser Ebene rausgehalten werden, weil schlicht diese Denke nicht Domain sondern Technik ist und die Domain Denke "verschmutzen". Ob das dann (zufällig oder absichtlich) in einer Datenbank landet bleibt dem Implementierer des injekteten Repositories sein "süßes Geheimnis" und solange er nicht die Domain-Objekt Semantik bricht kann er cachen bis der Arzt kommt und natürlich auch nHibernate verwenden, allein um das Rad nicht neu zu erfinden.
    Am Ende muss man "nur" die DB-techies überzeugen, die eigentlich sofort jedes Thema mit Roh-Daten-Töpfen mit Kipp-rein-und-Fummel-drin-rum-nach-Bedarf lösen wollen, weil sie das so unguided überall so machen (wilde Excels, wildes Filesystem und wilde DB-Tabellen) und nHibernate und diese Dinge so geil finden weil man damit noch schneller sowas bauen kann.


« Zurück  |  Weiter »