Multi-Client Synchronisation mit Entity Framework 5 – Teil I

Moderne Technologien machen dem Entwickler das Leben schon einfach. Für fast alles existieren Abstraktionen und Blackboxes, die es nur noch richtig zu nutzen und ggf. zu konfigurieren gilt.

Aber an manchen Stellen ist doch noch die Schaffenskraft von uns Entwicklern gefragt. Ein solches Themengebiet ist die Synchronisation zwischen mehreren Clients einer Anwendung. Zumindest, wenn man basierend auf .NET, SQL Server und einem typischen ORM wie Entity Framework entwickelt, denn da ist mir keine Lösung bekannt, die dies out-of-the-box liefert.

Das Ziel ist recht einfach beschrieben. Gegeben sei eine typische Client-Server-Architektur mit mehreren Clients. Änderungen an der Datenbasis sollten in jedem Client automatisch synchronisiert werden:

Lösungsansätze für Client-Server Anwendungen

Um eine generische Lösung zu implementieren, sollte eine Kapselung im Domain Model bzw. in einer „tiefen“ Schicht stattfinden.

Wenn man nun eine einfache Client/Server Architektur annimmt, existiert prinzipiell erst einmal das Problem, dass keine Kommunikation von Datenbank zu Client geschieht (und natürlich auch keine Peer-to-Peer Kommunikation zwischen Clients).

Prinzipiell sind folgende Ansätze denkbar:

  • Benachrichtigungen von der Datenbank (Notifications)
  • Anwendungsserver-Architektur
  • Polling

Einige Datenbanken unterstützen das Senden von Notifications zu einem Query (z.B. SQL Server, Oracle, PostgreSQL). Jedoch ist dies z.B. für SQL Server Express nicht verfügbar und auch sehr RDBMS-spezifisch, daher wird diese Variante nicht weiter verfolgt.

Falls die Architektur einen Anwendungsserver verwendet (Client-Server-Database), könnte der Anwendungsserver den Client problemlos über einen Rückkanal benachrichtigen (z.B. mit WCF, .NET Remoting). Wenn die Architektur noch keinen Anwendungsserver verwendet, könnte dieser zusätzlich hinzugefügt werden, wobei dieser dann ausschließlich für Synchronisierungsaufgaben zuständig wäre. Auch diese Variante soll nicht weiter verfolgt werden, da hier zusätzliche Anforderungen an das Deployment hinzukommen würden.

Verwendet werden soll der Polling-Ansatz, bei dem der Client periodisch den Datenbank-Server für Änderungen anfragt. Somit wird eine einfache Client-Server Architektur (Client-Database) unterstützt. Prinzipiell sollte sich die Synchronisation nicht grundsätzlich von dem Ansatz des Anwendungsservers unterscheiden, denn lediglich das „Anstoßen“ der Synchronisation erfolgt anders.

Grundsätzlicher Algorithmus

Grundsätzlich lassen sich zwei Bereiche erkennen, die es anzugehen gilt:

  1. Schreiben eines Änderungsjournals / Loggen der Änderungen
  2. Synchronisierung von Änderungen (Lesen des Änderungsjournals, Durchführung der Synchronisierung)

Eine grundsätzliche Entscheidung ist, wo das Änderungsjournal erzeugt wird. Dabei existieren folgende Varianten:

  • In der Datenbank (z.B. durch Trigger oder gekapselt in Stored Procedures oder nativ durch z.B. Change Tracking in SQL Server)
  • Innerhalb des Clients, gekapselt im O/R Mapper (Entity Framework)

Das Kapseln innerhalb der Datenbank hätte den Vorteil, dass jegliche Clients synchronisiert werden könnten. Bei einem Ansatz mit Triggern wäre jedoch viel Codegenerierung nötig, was das Deployment der Datenbank erschweren würde, da deren Struktur extrem aufgebläht würde. Die Logik lässt sich wahrscheinlich besser im Client kapseln, auch wenn man dann die Synchronisation eben nur zwischen den Anwendungen als Clients ermöglicht, welche die Hooks eingebaut haben.

Auf das Change Tracking Feature von SQL Server (unterstützt ab der Version 2008) bin ich leider etwas spät gestoßen. Es ist durchaus sehr interessant für die Implementierung, jedoch hat eine eine Umsetzung im O/R Mapper den Vorteil, dass man konzeptionell auf der Ebene der Entities bleiben kann und kein Mapping von Tabellenänderung auf Eigenschaften und Collections durchführen muss, was sich im späteren Verlauf als gar nicht so trivial herausgestellt hat.

Prototyp des Basisalgorithmus

Zugegeben habe ich einige Anläufe benötigt, um einen Algorithmus mit Entity Framework umzusetzen. Das Synchronisieren von Änderungen am Einzelobjekt ist dabei trivial, kompliziert wird es bei den Beziehungen.

Die Änderungshistorie wird in den drei Entitäten Submit, ChangedEntityEntry und ChangedCollectionEntry gespeichert. Die Ähnlichkeit zu einem Undo-Redo Journal ist übrigens verblüffend.

Die zwei Hauptbelange Logging und Synchronisierung sind in zwei entsprechende Klassen ausgelagert. Sämtlicher Datenzugriff wird durch ein Interface gekapselt, sodass die Synchronisierung für verschiedene Datenbanksysteme laufen kann. Ich habe mich aus Performancegründen für eine Grassroots-Implementierung mittels ADO.NET entschieden. Da nur wenige Inserts und Selects nötig sind, ist der Implementierungsaufwand hier kein Problem.

Interessant ist noch die Unterteilung von Synchronizer und AutoSynchronizer. Synchronizer ist eine Klasse, welche die eigentliche Synchronisation übernimmt. AutoSynchronizer stößt dies über einen Timer über einen festen Intervall automatisch an.

Abgebildet in der Solution ergibt sich dann folgendes Bild:

Der Quellcode ist auf Sourceforge in dem Projekt Entity Framework Sync veröffentlicht.

Ich möchte jetzt die Implementierung im Detail nicht mit Code-Postings wiederkäuen, wo man den Quellcode dort auf Sourceforge so schön browsen kann.

Daher nur ein paar “Impressionen” vom Quellcode:

Neben Unit-Tests habe ich auch eine Dummy-WPF-Anwendung erstellt, welche die Concurrency von 3 Clients simuliert (ob real 3 Clients oder nur 3 Instanzen eines EF-Contexts existieren, ist im Endeffekt egal):

Bei der Implementierung wurden zwangsläufig einige Design-Entscheidungen getroffen:

  • Entitäten müssen vorerst immer einen Primary Key ID vom Typ integer besitzen und das Interface IEntity implementieren
  • Collections werden als Ganzes neu geladen, die zugrundeliegenden Add/Remove Operationen werden nicht einzeln aufgezeichnet
  • Die Verarbeitung der Metadaten, z.B. das Aufzeichnen von Änderungen der inversen Seite von Collections, erfolgt bereits beim Loggen; für das Synchronisieren sind die Änderungsdaten bereits “mundgerecht” vorbereitet, sodass nur noch die entsprechende Entity bzw. Collection geladen und aktualisiert werden muss

Zwar habe ich zwischendurch das Metadatensystem von Entity Framework verflucht, aber dennoch funktioniert der Grundalgorithmus recht gut. Durch das Beispiel von BlogPost, Comment, Tag und Post-has-Tag sind auch die grundsätzlichen Beziehungen gut getestet, dennoch handelt es sich noch um einen frühren Prototyp, den man wohl nicht produktiv einsetzen kann.

Ausblick

Bei der weiteren Entwicklung möchte ich nun einen Risiko-basierten Ansatz wählen, d.h. untersuchen welche Risiken mit welchen Auswirkungen der aktuelle Prototyp besitzt und dann basierend auf dieser Analyse entscheiden, welche neuen Features oder Modifikationen der Algorithmus benötigt. Genug Inhalt also für einen weiteren Post.

Serie

Projekt bei Sourceforge