Zu Zeiten von .NET 1.x und 2.0 war die Welt noch einfach. Frei Haus gab es genau Eine Datenzugriffstechnologie, nämlich ADO.NET. Die gibt es natürlich immernoch, aber mittlerweile sind noch Linq2SQL und das Entity Framework dazugekommen. Die Beiden Neuen sind "echte" OR-Mapper, da sie Resultsets in Form von herkömmlichen .NET Objekten ausspucken. Das Entity Framework arbeitet sogar mit einer eingezogenen Abstraktionsebene und verspricht Datenzugriffslogik unabhängig vom RDBMS implementieren zu können. SQL ist out, Linq ist in! Visual Studio rundet das Ganze mit komfortablen Designern und Assistenten ab. Auch die SOAP-Freunde machen die neuen Datenschaufler glücklich, müssen sie sich doch nicht mehr mit XML-Serialisierung von properitären DataSets herumschlagen, die alles andere als interoperabel sind. Man kann also durchaus sagen: Das lange warten auf die OR-Mapping-Lösung von Microsoft hat sich gelohnt. Viele Probleme können mit den neuen Datenzugriffstechnologieen elegant gelöst werden.
Ganz so rosarot ist die Welt dann aber doch nicht. Eine Sache stößt mir sehr bitter auf, wenn ich z.B. mit dem Entity Framework arbeite: Es gibt keinen RowState!
Im guten alten ADO.NET ist RowState der Name einer Eigenschaft der Klasse DataRow. Eine DataRow beschreibt einen einzelnen Datensatz, der von einer Datenquelle abgerufen wurde und innerhalb einer DataTable lokal im Arbeitsspeicher liegt. Aber das ist wohl den meisten bekannt. Zurück zum RowState. Der RowState gibt an, ob die DataRow geändert, gelöscht, neuangelegt oder unverändert ist. Man kann deshalb auch sagen: Eine DataTable weiß, welche ihrer DataRows, geändert, gelöscht oder neuangelegt sind. Das ist sehr nützlich. Wenn ich z.B. eine DataTable an ein DataGridView (Windows.Forms) gebunden habe und dort als Benutzer einer Zeile lösche, ist diese Zeile in der DataTable nicht verloren, sondern der RowState wird auf "Gelöscht" (deleted) gesetzt. Wenn ich im selben DataGridView nun eine neue Zeile eingebe, erhält die dadurch erzeugte DataRow den RowState "Neuangelegt" (added). Ändere ich einen Wert in einer vorhanden Zeile, hat diese danach logischerweise den RowState "Geändert" (modified). Die DataRow weiß aber noch mehr über sich selbst. Sie kennt nicht nur ihren aktuellen Zustand, sondern merkt sich auch die Originalwerte (also z.B. vor der Änderung durch den Benutzer). Das ermöglicht es wiederum durch einfaches aufrufen der RejectChanges-Methode an der DataTable, alles sofort Rückgängig zu machen. Die neuangelegte Zeile verschwindet wieder, die Gelöschte taucht wieder auf und die die Geänderte hat auch wieder ihre alten Werte. "Na und? Das ist doch alles ein alter Hut. Das ist doch allgemein bekannt!" werden jetzt einige denken. Ich stimme zu. Es ist ein alter Hut. Das gab es auch schon alles bei Classic ADO. Also ist es nach so vielen Jahren doch eine Selbstverständlichkeit.
Jetzt aber die große Frage: Wie mache ich das, wenn ich einen OR-Mapper benutze? Platte Objekte haben keinen RowState! Sie wissen nicht, ob sie gelöscht oder neuangelegt wurden. Sie wissen auch nicht, ob sie geändert wurden. Noch nicht mal ihre alten Werte vor der Änderung können sie sich merken. Wenn es die Objekte aber selber nicht können, muss es jemand Anderes für sie machen. Genau! Die OR-Mapping-Infrastruktur. Die wischt unaufgefordert jede kleine Träne von unseren Wangen. Beim Entity Framework kümmert sich der ObjectContext um diese RowState-Angelegenheiten. Als aufmerksamer Kontext kennt er alle Objekte, die er erzeugt hat beim Namen und führt über jedes Einzelne genau Protokoll. Das funktioniert auch super. Aber was ist mit der RejectChanges-Methode? Dafür gibt es leider beim DataContext keine Entsprechung. Wenn ich meine Änderungen verwerfen will, muss ich entweder vorher eine komplette Kopie der Objektliste erstellen und diese für eine mögliche Wiederherstellung des Originalzustandes im Speicher halten. Oder ich rufe einfach die Daten nochmal vom Datenbank-Server ab. Letzteres ist verglichen mit meiner RejectChanges-Lösung aber sehr ressourcenfressend. Scheidet damit aus. Ersteres ist Umständlich und erfordert einiges an Zusatzcode (Die Sicherheitskopie der Objektliste muss dann ja dem ObjectContext erst wieder beigebracht werden). Wie sieht es mit dem DataBinding in meinem Windows.Forms-Formular aus? Ähm... Genau! Auch da muss ich rumfummeln, da meine Controls an die alte Objektliste aber nicht an die Sicherheitskopie gebunden sind. Wieder muss ich Code schreiben, um das in den Griff zu bekommen. Aber Linq und die abstrakte Modellierung sind so toll, dass ich das jetzt mal noch durchgehen lasse.
Dass der ObjectContext den RowState-Job übernommen hat, wäre ja okay. Aber was passiert, wenn ich dahin gehe, wo ich den ObjectContext nicht mitnehmen kann? Wenn ich Resultsets über Prozess- oder gar Maschinengrenzen hinweg übertragen muss? Ich denke da z.B. an eine verteilte Anwendung mit schönen, neuen, tollen WCF-Diensten. Aber auch da kann ich erstmal aufatmen, denn der ObjectContext kann auch loslassen. Objekte können nämlich vom Kontext "detached" (getrennet) und "attached" (angefügt) werden. Die Vorgehensweise ist damit - auf den Ersten Blick - ähnlich wie bei meinen ADO.NET-DataSets:
-
WCF-Dienstseitig Daten von Datenquelle abrufen (Als Objektliste)
-
Objektliste detachen
-
Objektliste serialisieren und zum Client überragen
-
Auf dem Client mit den Objekten arbeiten
-
Ggf. geänderte Objektliste serialisieren und zurück zum Server übertragen, um die Änderungen zu persistieren
-
Ojektliste attachen
-
Änderungen Persistieren
Klingt gut, nicht? Aber leider nur in der Theorie. Was passiert denn, wenn ich auf meinem Client eine Zeile in einem DataGridView lösche, welches an die Objektliste gebunden ist, die ich vorher vom WCF-Dienst abgerufen habe? Ganz klar, das DataBinding entfernt das Objekt aus der Auflistung. Nur ist diesmal kein ObjectContext da, der Protokoll führt. Angenommen mein DataGridView zeigt gerade zehn Zeilen an und ich lösche davon drei. Anschließend klicke ich auf Speichern. Wie soll der Server aber nun wissen, welche Datensätze er in der Datenbank löschen muss? Der ObjectContext weiss es nicht, da die betroffene Objektliste zum Löschzeitpunkt detatched war und außerdem auf einem ganz anderen Computer gelaufen ist. Ups! Jetzt fängt es an weh zu tun, denn ich muss mich auf dem Client selber darum kümmern, dass ausstehende Löschungen protokolliert werden. Außerdem muss das selbstgebastelte Löschprotokoll auch noch an den Server geschickt werden. Und zwar zusammen im selben Dienstaufruf, der auch neue und geänderte Datensätze persistiert, denn ich muss ja möglicherweise serverseitig eine Transaktion aufspannen. Die Persistenz-Odysee ist aber nocht nicht vorbei. Wenn die geänderte Objektliste und ggf. das Löschprotokoll wieder beim WCF-Dienst angekommen sind, muss die Objektliste zuerst wieder am ObjectContext attached werden. Dabei generiert der Kontext das Protokoll der Änderungen quasi nachträglich. Allerdings kann auch der ObjectContext die fehlenden RowState-Informationen nicht aus dem Ärmel schütteln. Er braucht dazu entweder eine Sicherheitskopie der ursprünglichen Objektliste oder er schaut in der Datenbank nach (will heißen, die Daten alle nochmal aus der Datenbank abrufen und vergleichen). Gelöschte erkennt er beim Attachen nur, wenn man eine Sicherheitskopie angibt. Ansonsten selbstgebasteltes Löschprotokoll mit Schleife durchgehen und für jeden DeleteObject aufrufen.
Ich finde, da machen die "fetten" typisierten DataSets von ADO.NET eine ganz gute Figur neben den hochgelobten schlanken Objekten. Wenn ich zusätzliche DB-Roundtrips bzw. Netzwerklast für Sichungskopien der Objektlisten im Ursprungszustand mitzähle, ist an OR-Mapping nichts schlankes mehr zu erkennen. Nur wesentlich mehr Arbeit und auch noch weniger Komfort beim clientseitigen Databinding.
Aber auch Typisierte DataSets sind nicht immer die Guten. Wenn es z.B. um Interoperabilität geht, fallen sie sofort durch. Ein Java-Client, der z.B. via SOAP auf einen WCF-Service zugreift, wird wenig Freude haben, wenn er ein DataSet deserialisieren soll. Leider enthält auch die XML-Serialisierung von DataSets .NET spezifische Inhalte. Auch Unabhängigkeit der DAL-Implementierung von einem bestimmten RDBMS ist mit normalem ADO.NET fast komplett Handarbeit (Und zwar wesentlich mehr, als ein bischen Detach- und Attach-Aufwand). Da macht die abstrakte Modellierung der DAL mit dem Entity Framework mehr Spaß. Es schreibt auch nicht jeder verteilte Anwendungen. Bei eine Standard-ASP.NET-Lösung ist es z.B. überhaupt nicht nötig Objektlisten zu detachen.
Am Ende kommt es auf das konkrete Projekt und dessen Umgebung an, welche Technologie sinnvoller ist.