Erweiterte Programmierung


Folgendes Klassendiagramm wird zur Erklärung verwendet:

 


"gip.core.datamodel.Database" ist das Datenbankmodell für iPlus. Es enthält Tabellen/Entitäten für das iPlus-Laufzeitsystem, Rechteverwaltung und Entwicklung.

"gip.mes.datamodel.DatabaseApp" ist das Datenbankmodell für Varobatch.MES. Es enthält Tabellen/Entitäten für die Produktpalette vom MES.

Beide sind Ableitungen von "System.Data.Objects.ObjectContext" (Entity-Framework 4) bzw. Microsoft.EntityFrameworkCore.DbContext (EF Core) und implementieren die Schnittstelle "gip.core.datamodel.IACEntityObjectContext". IACEntityObjectContext dient zur Abstrahierung von gemeinsamen Datenbankfunktionalitäten die für das iPlus-Framework benötigt werden (Rechteverwaltung, Logging von Datenbankfehlern, Initialisierung von neuen Entity-Objekten mit Defaultwerten, Validierung von Feldwerten und Längen...). IACEntityObjectContext reflektiert zudem auch einen großen Teil der Methoden, die in "System.Data.Objects.ObjectContext" definiert sind. Wenn Sie ihr eigenes Datenbankmodell entwickeln und in iPlus integrieren möchten, dann müssen Sie diese Schnittstelle implementieren. Die dazu benötigte Logik ist in der Hilfsklasse "gip.core.datamodel.ACObjectContextHelper" implementiert, die jeder Kontext als Member hat und an die alle Methodenaufrufe delegiert werden.

 

--V5 Version--

Im iPlusV5 teilen beide Datenbankmodelle eine gemeinsame Vererbung von "Microsoft.EntityFrameworkCore.DbContext". Die Kernfunktionalitäten bleiben unverändert, es gibt jedoch einige wenige Änderungen zu beachten, während Sie Ihre Modelle erstellen. Eine der Änderungen besteht darin, dass Sie eine Datenbankabfrage nicht direkt an WPF-Steuerelemente wie ListView, DataGrid oder ListBox binden können. Dies würde zu einer Ausnahme mit der Meldung führen: "Direktes Binden an eine Datenbankabfrage (DbSet, DbQuery, DbSqlQuery) wird nicht unterstützt."

Um dies zu umgehen, empfiehlt es sich, für jedes Entitätsobjekt eine List-Variable zu deklarieren. Anschließend wenden Sie in der Datenbankklasse die ToArray() Methode auf jedes dieser Objekte an.


Eine gestartete iPlus-Anwendung (Windows-Dienst oder Desktopanwendung) instanziiert vier Langzeit-Datenbankkontexte (siehe die vier Nummern oben im Bild).

(1) Eine globale Instanz von "gip.core.datamodel.Database", auf die über die statische Eigenschaft static Database GlobalDatabase zugegriffen werden kann. Sie wird ausschließlich für das Typsystem von iPlus benötigt. Vermeiden Sie, diesen Kontext für Abfragen zu benutzen! Jede Abfrage muss mittels Threadsperre abgesichert werden: using (ACMonitor.Lock(QueryLock_1X000)) {...} 

(2) Eine weitere Instanz von gip.core.datamodel.Database, die für das Lesen und Schreiben von 

(3) Für serverseitige Anwendungsprojekte wir eine globale Datenbankinstanz generiert. Dies kann entweder eine Instanz von Database, DatabaseApp oder eines anderen Datenbankkontextes, das IACEntityObjectContext implementiert hat. Dies wird über die Konfigurationseigenschaft "gip.core.autocomponent.ACRoot.TypeNameOfAppContext" festgelegt, in die der AssemblyQualifiedName eingetragen werden muss. Auf diese Instanz kann über die Eigenschaft static RootDbOpQueue.AppContextQueue.Context zugegriffen werden.

(4) Gemeinsam verwendeter Datenbankkontext für Businessobjekte, die DatabaseApp als Kontext benötigen. Dieser Kontext wird dynamisch generiert, sobald er benötigt wird und auch wieder geschlossen, wenn alle Businessobjekte geschlossen wurden. Dies bewerkstelligt der ACObjectContextManager:

Der ACObjectContextManager ist eine statische Klasse, die für die Verwaltung von Langzeitkontexte zuständig ist. Die vier zuvor genannten Kontexte verwenden diese Klasse. Mittels der diversen überladenen Methode "GetOrCreateContext()" sucht der ACObjectContextManager über einen Schlüsselnamen, ob schon ein Langzeitkontext vorhanden ist und gibt ihn als Rückgabewert zurück. Falls kein Kontext in seiner Liste vorhanden ist, generiert er eine entsprechende Instanz. Mittels DisposeAndRemove() entfernt er alle Objekte aus dem ObjectStateManager (Change Tracking) und entfernt den Kontext aus seiner Liste.


Die Datenbankkontexte (2) und (3) werden indirekt erzeugt über die Instanziierung einer Datenbank-Queue. Datenbank-Queues sind Klassen vom generischen Typ "ACEntityOpQueue<T>", die von ACDelegateQueue abgeleitet sind. Delegate-Queues bestehen aus einer Queue vom Typ "Queue<Action>" und einem internen Thread, der diese Queue abarbeitet, sobald neue Einträge vorhanden sind. Die Einträge sind ".NET-Delegaten". Der generische Typ einer Datenbank-Queue muss vom Typ IACEntityObjectContext sein. Datenbank-Queues ermöglichen eine threadsichere Nutzung von Datenbankkontexten, indem alle LINQ-Operationen in die Queue gestellt werden (Per Methode: void Add(Action action)) und nach Abarbeitung der Queue mittels der Methode "IACEntityObjectContext.ACSaveChanges()" eine automatische Speicherung durchgeführt wird. Der Vorteil darin ist, dass der Anwendungsthread nicht blockiert mit Aufgaben blockiert wird, die asynchron ablaufen können. Falls man jedoch unmittelbar (synchron) eine LINQ-Operation ausführen möchte, kann man dies mittels der Methode void ProcessAction(Action action) tun.


"gip.core.datamodel.Database" ist das Datenbankmodell für iPlus. Es enthält Tabellen/Entitäten für das iPlus-Laufzeitsystem, Rechteverwaltung und Entwicklung.

Tabellen:


"gip.mes.datamodel.DatabaseApp" ist das Datenbankmodell für Varobatch.MES. Es enthält Tabellen/Entitäten für die Produktpalette vom MES.

Tabellen:


Vorkompilierte Abfragen

Vorkompilierten Abfragen sind schneller, weil der Expression-Tree nur einmal vom Compiler erstellt werden muss. Falls Sie nur lesend auf die Daten zugreifen möchten, dann präferieren Sie "new" in der Select-Anweisung anstelle der Materialisierung ganzer Entity-Objekte. 

 

Datensätze hinzufügen

Neue Entity-Objekte erzeugen Sie mit der statischen Methode "NewACObject()", die jede Entity-Klasse im iPlus-Framework bereitstellen sollte. Dadurch werden alle Eigenschaften initialisiert und mit Standard-Werten belegt, die in der Entwicklungsumgebung eingestellt worden sind. Rufen Sie danach die AddObject-Methode auf, um das neue Objekt der Änderungsnachverfolgung bekannt zu geben. Zum Speichern rufen Sie ACSaveChanges() auf anstatt der gewohnten SaveChanges()-Methode. ACSaveChanges() validiert zuvor alle geänderten Objekte gegenüber den eingestellten Min/Max-Werten. Ist die Validierung oder der anschließende Speichervorgang fehlgeschlagen, erhalten Sie ein Msg-Objekt mit Fehlerinformation und Schweregrad zurück. Mit ACUndoChanges() machen Sie Ihre Änderungen rückgängig. Dies ist bei Langzeitkontexten notwendig, damit nicht jedes Mal mit erneutem Aufruf von ACSaveChanges() die vergangenen Fehler den Speichervorgang für die neuen Änderungen verhindern.

 

Optimierte Abfragen und Löschen

Verwenden Sie die Include()-Erweiterungsmethode, um mit einer Abfrage weitere in Beziehung stehende Entity-Objekte zu laden. Im nachfolgenden Beispiel muss Entity-Framework kein Lazy-Loading durchführen, sobald auf die Eigenschaft MDMaterialGroup zugegriffen wird:

Ein Löschvorgang wird mit DeleteACObject() durchgeführt. Der zweite Parameter gibt an, ob Validierungen auf Client-Seite durchgeführt werden sollen (z.B. wegen referentieller Integrität). Der dritte Parameter gibt an, ob der Datensatz physikalisch in der Datenbank gelöscht werden soll oder nur stattdessen ein Löschkennzeichen gesetzt wird. Im Beispiel wird zuerst versucht physikalisch zu löschen und im Fehlerfall ein Soft-Delete.

 

Abfragen auf dem globalen Datenbankkontext (1)

Verwenden Sie diesen Kontext nur dann, wenn Sie Informationen von Typsystem benötigen (Infos über Klassen, Eigenschaften, Methoden, XAMLDesigns...). Bitte beachten Sie, dass Langzeitkontexte immer threadsafe abgefragt werden müssen, weil Sie nie wissen können, ob zeitgleich eine andere Abfrage in einem anderen Thread durchgeführt wird. Verwenden Sie dafür die ACMonitor-Klasse und übergeben das Sperrobjekt "QueryLock_1X000", das wegen der IACEntityObjectContext-schnittstelle jede EF-Kontext-Klasse bereitstellen muss:

 

Grundsätzlich ziehen Sie bitte "using() mit einem eigenen Kontext" vor, weil damit die Threadproblematik umgangen und der Speicher danach wieder freigeben wird.

 

Arbeiten mit Datenbank-Queues (2), (3)

Jegliche Abfragen und Änderungen an Entity-Objekten müssen bei Datenbankkontexten die einer Queue angehören, in eine anonyme Methode eingebaut und als delegat der Add()- oder ProcessAction()-Methode übergeben werden:

Weil die Add()- und ProcessAction()-Methode intern selbst eine Threadsperre mittels QueryLock_1X000 durchführt, müssen Sie dies als Anwendungsprogrammierer nicht mehr tun.

 

Langzeit-Datenbankkontexte

Falls aus technischen Gründen ein Langzeitkontext benötigt wird, dann instantiieren Sie den Kontext mittels dem ACObjectContextManager. Halten Sie die Referenz zu dem neuen Kontext in einem privaten Feld und geben ihn in der überschriebenen Eigenschaft "public override IACEntityObjectContext Database" zurück. Alle Kind-Instanzen, die auf Ihre Database-Eigenschaft zugreifen erhalten diesen Kontext zurück, weil in der Basisimplementierung immer der Kontext der ParentACComponent zurückgegeben wird. Wenn keine Klasse die Database-Eigenschaft überschreibt, dann erhalten Sie den Datenbankontext der root-Instanz zurück. In einer Anwendung ist es immer der AnwendungsManager (Klasse "gip.core.autocomponent.ApplicationManager"), der "RootDbOpQueue.AppContext (3)" zurückgibt. Wenn der Langzeitkontext nicht mehr benötigt wird, dann entfernen sie ihn vom ACObjectContextManager mittels "DisposeAndRemove()".