Muster: Repository mit NHibernate, 1st Edition
Ein Repository stellt einen gekapselten Mechanismus für das Speichern, Laden und Suchen von Aggregates dar. Es ist eine Abstraktion für den Zugriff auf die Persistenzschicht. Das hier dargestellte Muster zeigt die Implementierung eines Repositories als Zugriff mithilfe des objektrelationalen Mappers NHibernate.
1st Edition
Wir haben zwei Varianten von Repository-Implementierungen bei Schleupen im Einsatz. Hier wird die 1st-Edition beschrieben. Diese sind ähnlich bzgl. des Inhalts. Bei neueren Solutions ist die 2nd-Editon zu bevorzugen. Details hierzu siehe Muster: Repository mit NHibernate, 2nd Edition.
Design
Pro Aggregate wird genau ein Repository mit folgender Namenskonvention erstellt:
<AggregateRoot>Repository
Aggregate, die zusammenhängende Objekte darstellen, werden als Einheit verwaltet werden und immer vollständig gelesen und ausgehändigt. Dabei wird stets das AggregateRoot ausgehändigt.
Gemäß unsere Onion-Modells ist ein Zugriff von einem Aggregate auf das Repository nicht erlaubt.

Das folgende Diagramm stellt die zur Verfügung gestellten Schnittstellen und Implementierungen dar, und deutet an, wie diese genutzt werden sollten.

Demnach wird ein Repository wird als Unterklasse von Repository<TAggregateRoot> oder Repository<TAggregateRoot, TKey> aus Schleupen.Framework.Persistence.NHibernate implementiert. Das Mapping wird über ClassMaps<T> implementiert und über einen NHibernateConfigurator angebunden. Um verteilte Transaktionen zu vermeiden, muss einer der Konstruktoren, der Schleupen.CS.PI.Framework.Persistence.NHibernate.IPersistenceContext enthält, verwendet werden (mehr Details weiter unten).
Die rot eingefärbten Klassen werden bei der Implementierung von Repository.ConnectionTests etc. benötigt.
Implementierung
Das eigene Repository erbt von Repository<TAggregateRoot> aus Schleupen.CS.PI.Framework.Persistence.NHibernate. Als generischer Typparameter wird der Typ des AggregateRoot angegeben, sowie der Id-Typ, welcher in der Regel eine EntityId ist.
Die Basisklasse bietet eine Queryable-Eigenschaft an, um darauf die Linq-Abfragen zu formulieren. Ein ToList() führt diese dann aus. Das Repository wird in der *.Repositories-Assembly (siehe Muster - Deklaration von Namensräumen (Namespace)) im fachlichen Namensraum abgelegt.
Wir empfehlen, im Standard HQL-Abfragen mit dem HqlQueryBuilder zu implementieren!
Im Standard sollte als Schlüsseltyp für Ids eine EntityId verwendet werden, da diese zum einen typsicherer sind und zum anderen auf Guids basieren und dies im Allgemeinen für Testszenarien deutliche Vorteile hat (-> Reproduktion von identischen Testdaten).
Es ist auch möglich, als Primärschlüssel eine System.Guid zu verwenden. Das wird allerdings nicht empfohlen.
Ist der Primärschlüssel kein System.Guid, so kann von Repository<TKey, TAggregateRoot> geerbt werden. TKey ist hierbei dann der Typ des Primärschlüssels, also z.B. System.String oder System.Int64.
Das Repository enthält aufgrund einer vereinfachten Testbarkeit alle Abfragen zum Lesen des verwalteten Aggregats - es wird kein Queryable nach außen gegeben, sodass diese im Controller implementiert würde.
So weit wie möglich sollte die direkte Verwendung der NHibernate-Sitzung außerhalb des Repositorys vermieden werden - ggf. ist das in Tests notwendig.
Implementierungsbeispiel
Zum besseren Verständnis implementieren wir ein Repository für das Aggregate Buch, das folgende Struktur aufweist:

Hierfür implementieren wir eine Klasse mit dem Namen BuchRepository:
public sealed class BuchRepository : Repository<Buch, BuchId>, IBuchRepository
{
public BuchRepository(IDomainEventNHibernateSessionFactory nhibernateSessionFactory,
IBuchRepositoryConfigurator configurator,
IPersistenceContext persistenceContext)
: base(nhibernateSessionFactory, configurator, persistenceContext)
{
}
}
Des weiteren benötigen wir einen sogenannten NHibernateConfigurator, um im Repository insbesondere das Mapping anzubinden:
public sealed class BuchRepositoryConfigurator : NHibernateConfigurator, IBuchRepositoryConfigurator
{
...
protected override IEnumerable<Type> MappingTypes
{
get
{
yield return typeof(BuchMap);
yield return typeof(InhaltsangabeMap);
yield return typeof(SeiteMap);
}
}
}
Dabei sind die MappingTypes die Typen, die pro Entity das Mapping konfigurieren. Details hierzu sind in Muster: Aggregate-Persistence-Mapping zu finden.
Das eigentliche objektrelationale Mapping wird in Klassen beschrieben, die von ClassMap<T> erben.
public sealed class BuchMap : ClassMap<Buch>
{
...
}
Pro Entity wird eine ClassMap erstellt:
public sealed class InhaltsangabeMap : ClassMap<Inhaltsangabe>
{
...
}
public sealed class SeiteMap : ClassMap<Seite>
{
...
}
Die zu persistierenden Klasse sind prinzipiell POCOs (plain old csharp objects - simple Klassen). Details sind aber auch hier zu beachten und in Muster: Aggregate-Persistence-Model beschrieben.
Wenn ValueObjects per 0..1-Beziehung angebunden werden, wird in der Regel keine eigene ClassMap erstellt. Sind diese per *-Beziehung angebunden, so hängt dies vom speziellen Fall ab und wird weiter unten beschrieben! Siehe hierzu Muster: Aggregate-Persistence-Mapping.
Auch NHibernate-Konventionen können über die Implementierung von NHibernateConfigurator angebunden werden. Der Anschluss erfolgt über die Property ConventionTypes, wobei die Basisklasse bereits einige Konventionen bereitstellt. Siehe hierzu Muster: Aggregate-Persistence-Mapping.
Wir erstellen noch ein Marker-Interface, um den Konfigurator einfach per DI-Konvention anbinden zu können.
public interface IBuchRepositoryConfigurator : INHiberateConfigurator
{
}
Die Repository-Basisklasse stellt viele Methoden zur Unterstützung bereit, wobei am meisten Abfragen codiert werden. In dem folgenden Beispiel kann das Repository Bücher anhand ihres Namens abfragen:
public sealed class BuchRepository : Repository<Buch>, IBuchRepository
{
public BuchRepository(IConnectionInfoProvider provider, IBuchRepositoryConfigurator configurator)
: base(provider, configurator)
{
}
public Buch QueryByTitel(string titel) // Query wird innerhalb des Repositorys erstellt
{
HqlQuery hqlQuery = ((HqlQuery)HqlQuery
.Select<Buch>()
.From<Buch>()
.LeftOuterJoin<Buch>(x => ((IBuchData)x).Inhaltsangabe, true);
.LeftOuterJoin<Inhaltsangabe>(x => ((IInhaltsangabeData)x).Seiten, true)
.WhereOrAnd<Buch>(x => ((IBuchData)x).Titel)).IsLike(() => titel, SearchMode.Wildcard);
return Query(hqlQuery);
}
}
Beachtet, dass bei Abfragen keine SELECT N+1-Probleme etc. implementiert werden. Siehe hierzu unbedingt Muster: Effiziente Abfragen mit NHibernate.
Für das Repository ist immer eine eigene Schnittstelle zu verwenden, also in unserem Beispiel IBuchRepository. In dieser befinden sich dann zum einen die benötigen Methoden und zum anderen ist die Registrierung im DI-Container per Konvention dann trivial.
Anbindung im DI-Container
Der ConnectionInfoProvider aus der Assembly Schleupen.CS.PI.SB.Client muss für die Nutzung von Service-Implementierung im DI-Container wie folgt konfiguriert werden, um insbesondere das korrekte Datenbank-Schema anzubinden. Dazu im ContainerConfigurator der Service Assembly folgendes ergänzen:
protected override void OnContainerConfiguring(Castle.Windsor.IWindsorContainer windsorContainer)
{
if (windsorContainer == null) { throw new ArgumentNullException(nameof(windsorContainer)); }
windsorContainer.Install(new ServiceBusClientInstaller("Schleupen.AS.MT.BIB", IsInsideWcfOperation) { DatabaseSchema = "MT_BIB" });// Hier das zu verwendende Schema in der Datenbank eintragen.
...
base.OnContainerConfiguring(windsorContainer);
}
Hinweise
Die direkte Verwendung der NHibernate-Session außerhalb des Repositorys muss unbedingt vermieden werden!
Manchmal ist es notwendig, auch ein select count(*) zu implementieren. Dies ist zwar definitiv ein Ausnahmefall, wird aber dennoch im Repository implementiert.
Bei der Implementierung von 1:n-Beziehungen (z.B. eine Inhaltsangabe hat n Seiten) muss ISet<> anstelle von IList<> verwendet werden! Ursache ist hier, dass ab NHibernate 4, die NHibernate-Logik bzgl. der sog. ResultTransformer, die das kartesische Produkt in Objekt-Hierarchien übersetzen (Stichwort DistinctJoinResultTransformer) dahingehend überarbeitet worden, dass dieser Mechanismus nun automatisch funktioniert.
Anschluss des Datenänderungsprotokolls [Optional]
Das Datenänderungsprotokoll ist bei vielen Entitäten obligatorisch - bitte kontaktiert hierzu binden die Systemanalyse, euren Analyste oder Teamleiter! Die Implementierung ist in Muster: Anschluss Datenänderungsprotokoll beschrieben.
Erweiterte Konfiguration
Im Folgenden werden erweiterte Konfigurationen beschrieben.
Rein lesender Zugriff
Möchte man nur lesend auf die Aggregate zugreifen können, so kann die Eigenschaft IsReadOnly in der Konfiguratorklasse auf true gesetzt werden. Dadurch werden in der NHibernate-Event-Listenern sämtliche schreibenden Operationen mit einer Ausnahme abgebrochen. Dies kann z.B. für den Zugriff auf Stammdaten nützlich sein, wenn diese nicht geändert werden dürfen.
public sealed class BuchRepositoryConfigurator : NHibernateConfigurator, IBuchRepositoryConfigurator
{
public BuchRepositoryConfigurator()
{
IsReadOnly = true; // Nur lesender Zugriff erlaubt
}
...
}
Konfiguration des Ladeverhaltens
Möchte man das Ladeverhalten eines Repositorys ändern, so gibt es die Möglichkeit Eager Loading des gesamten Objektbaums zu erzwingen, indem man die Eigenschaft ForceEagerLoading im Konfigurator auf true setzt.
public sealed class BuchRepositoryConfigurator : NHibernateConfigurator, IBuchRepositoryConfigurator
{
public LibraryRepositoryConfBuchRepositoryConfiguratorigurator()
{
ForceEagerLoading = true; // Versuch, eager Loading zu erzwingen -> riskant!
}
...
}
Das Eager Loading per ClassMaps oder wie folgend funktioniert allerdings nicht wie erhofft - NHibernate sorgt zwar dafür, dass alle Objekte geladen werden, sorgt aber für ein SELECT N+1-Problem, da zunächst die definierte Abfrage ausgeführt wird und erst dann die fehlenden Entitäten einzeln nachgeladen werden.
Soll eine feingranulare Konfiguration des Ladeverhaltens vorgenommen werden, so kann die NHibernate-Konfiguration manuell bearbeitet werden, indem man sich im Konfigurator das Ereignis OnConfiguring abonniert.
public sealed class BuchRepositoryConfigurator : NHibernateConfigurator, IBuchRepositoryConfigurator
{
protected override void OnConfigurating(object sender, ConfiguratingEventArgs e)
{
foreach (PersistentClass persistentClass in e.Configuration.ClassMappings)
{
persistentClass.IsLazy = false;
foreach (Property property in persistentClass.PropertyIterator)
{
property.IsLazy = false; // hier könnten z.B. nur bestimme Propertys eager geladen werden
}
}
foreach (Collection collectionMapping in e.Configuration.CollectionMappings)
{
collectionMapping.IsLazy = false; // hier könnten z.B. nur bestimme Collections eager geladen werden
}
}
protected override IEnumerable<Type> MappingTypes
{
get
{
yield return typeof(BuchMap);
yield return typeof(InhaltsangabeMap);
yield return typeof(SeiteMap);
}
}
...
}
Transaktionen
Am Ende der Verwendung eines Repositorys ist immer die Methode Flush() aufzurufen, damit alle Änderungen des Repositorys in die Datenbank geschrieben werden. Dies stellt die korrekte Verwendung in verteilten Transaktionen sicher.
Außerdem ist zu beachten, dass ein Repository (= NHibernate-Sitzung) nicht in verschiedenen Transaktionen verwendet werden kann. Daher ist es empfehlenswert, den TransactionScope oberhalb des Repositorys zu definieren und die Repositorys nur als lokale Variable zu halten. Grund dafür ist, dass die NHibernate-Sitzung nicht thread-safe ist. Z.B. kann das Transaction.TransactionCompleted-Ereignis in einer verteilten Transaktion durchaus in einem anderen Thread aufgerufen werden. Dies führt dann zu inkonsistentem Verhalten bzw. Ausnahmen in der NHibernate-Sitzung.
Um verteilte Transaktionen zu vermeiden, muss ein Konstruktor mit dem Parametertype IPersistenceContext verwendet werden. Über Dependency Injection wird dann sichergestellt, dass stets dieselber Datenbank-Verbindung verwendet wird.
Im Repository.ConnectionTest wird beschrieben, wie dies im Tests erfolgen kann.
Performantes Persistieren / Batching beim Persistieren
NHibernate stellt mit Batch Inserts ein Feature bereit, mithilfe dessen die Performance um bis zu 50% erhöht werden konnte.
Das Batching ist nur für NHibernate 5 verfügbar! Das Batching funktioniert nicht für ID-Spalten, die durch die DB generiert werden (siehe https://stackoverflow.com/questions/58798633/why-nhibernate-doesnt-allow-to-make-batch-insert-during-stateless-session-with)!
Im Standard verwenden wir .GeneratedBy.Assigned() (Syntax FluentNHibernate), was als Konvention angebunden ist und somit Batch Inserts genutzt werden können.
Inserts können nach wie vor durch die Add()-Methode des Repositories geschehen. Hierbei erzeugt NHibernate SqlClientCommandSets, die jeweils mehrere SqlCommands beinhalten. In einem Profiler Trace kann man dann die einzelnen SQL Statements einsehen.
Die Add()-Methode von Repositories nutzt intern die NHibernate-Methode Save(). Diese überprüft die Existenz von Kindobjekten und setzt dazu SELECT-Statements ab.
SELECT inhaltsang_.Id, inhaltsang_.Buch_Id AS buch2_1_ FROM mt_bib.Inhaltsangabe inhaltsang_ WHERE inhaltsang_.Id=@p0 @p0=a2c0aec5-01b0-4fc0-9584-f0e9d59fda6a SELECT seite_.Id, seite_.Buch_Id AS buch2_2_, seite_.Inhalt AS inhalt3_2_, seite_.Seitenzahl AS seitenzahl4_2_ FROM mt_bib.Seite seite_ WHERE seite_.Id=@p0 @p0=d1a494ae-a767-4be4-8079-4c17412914fa SELECT seite_.Id, seite_.Buch_Id AS buch2_2_, seite_.Inhalt AS inhalt3_2_, seite_.Seitenzahl AS seitenzahl4_2_ FROM mt_bib.Seite seite_ WHERE seite_.Id=@p0 @p0=b10774c8-d651-477e-948d-8d177526d386 INSERT INTO mt_bib.Inhaltsangabe (Buch_Id, Id) VALUES (@p0, @p1);@p0 = d47d84f5-b208-4be3-b4ce-a8466c92ce8e [TYPE: Guid (0:0:0)], @p1 = a2c0aec5-01b0-4fc0-9584-f0e9d59fda6a [TYPE: Guid (0:0:0)] ... INSERT INTO mt_bib.Seite (Buch_Id, Inhalt, Seitenzahl, Inhaltsangabe_id, Id) VALUES (@p0, @p1, @p2, @p3, @p4);@p0 = d47d84f5-b208-4be3-b4ce-a8466c92ce8e [TYPE: Guid (0:0:0)], @p1 = NULL [TYPE: AnsiString (8000:0:0)], @p2 = 1 [TYPE: Int32 (0:0:0)], @p3 = a2c0aec5-01b0-4fc0-9584-f0e9d59fda6a [TYPE: Guid (0:0:0)], @p4 = d1a494ae-a767-4be4-8079-4c17412914fa [TYPE: Guid (0:0:0)] ... INSERT INTO mt_bib.Buch (Version, HiddenAt, Autor, Titel, Erscheinungsdatum, Zusammenfassung, Genre, Raumnummer, AusgeliehenAm, IstVerfuegbar, Inhaltsangabe_id, Verlag_id, Isbn, Sprache, Id) VALUES (@p0, @p1, @p2, @p3, @p4, @p5, @p6, @p7, @p8, @p9, @p10, @p11, @p12, @p13, @p14);@p0 = 1 [TYPE: Int64 (0:0:0)], @p1 = NULL [TYPE: DateTimeOffset (10:0:0)], @p2 = 'autor7fb7dbe2-3ac1-48c8-b059-0e368f8f40cb' [TYPE: AnsiString (8000:0:0)], @p3 = 'titel9ab69b73-f5d8-469d-b09f-ef9957659684' [TYPE: AnsiString (8000:0:0)], @p4 = 2020-03-09T07:44:43.5049959+01:00 [TYPE: DateTimeOffset (10:0:0)], @p5 = 'Zusammenfassung3c1835b3-d890-41cd-bea9-682df0928c08' [TYPE: AnsiString (8000:0:0)], @p6 = 'Krimi' [TYPE: String (4000:0:0)], @p7 = 'A.0.0' [TYPE: String (4000:0:0)], @p8 = NULL [TYPE: DateTimeOffset (10:0:0)], @p9 = TRUE [TYPE: BOOLEAN (0:0:0)], @p10 = a2c0aec5-01b0-4fc0-9584-f0e9d59fda6a [TYPE: Guid (0:0:0)], @p11 = NULL [TYPE: Guid (0:0:0)], @p12 = '3 0' [TYPE: AnsiString (8000:0:0)], @p13 = 'Deutsch' [TYPE: AnsiString (8000:0:0)], @p14 = d47d84f5-b208-4be3-b4ce-a8466c92ce8e [TYPE: Guid (0:0:0)] ...
Das gleiche Verhalten tritt auch bei Attach/NHibernateSession.Merge auf.
Wichtig: Für eine bessere Performance beim Einfügen großer Batches empfiehlt es sich daher, die NHibernate-Methode Persist() wie folgt zu verwenden.
public override BuchId Add(Buch buch)
{
// NHibernateSession.Persist wirft einen Fehler, wenn die Version von außen vorgegeben wird.
// Wenn es wirklich notwendig sein sollte, die Version von außen vorzugeben, muss man sich hier etwas überlegen.
((IBuchData)item).Version = null;
NHibernateSession.Persist(buch);
return buch.Id;
}
Die führt zu folgenden SQL-Statements:
INSERT INTO mt_bib.Inhaltsangabe (Buch_Id, Id) VALUES (@p0, @p1);@p0 = 9bd56389-272d-44ab-a607-0a977c0c28c6 [TYPE: Guid (0:0:0)], @p1 = f6f7fab1-dd06-4ffa-b0c2-975e1ce2957a [TYPE: Guid (0:0:0)] … INSERT INTO mt_bib.Seite (Buch_Id, Inhalt, Seitenzahl, Inhaltsangabe_id, Id) VALUES (@p0, @p1, @p2, @p3, @p4);@p0 = 9bd56389-272d-44ab-a607-0a977c0c28c6 [TYPE: Guid (0:0:0)], @p1 = NULL [TYPE: AnsiString (8000:0:0)], @p2 = 1 [TYPE: Int32 (0:0:0)], @p3 = f6f7fab1-dd06-4ffa-b0c2-975e1ce2957a [TYPE: Guid (0:0:0)], @p4 = 4f0d15b7-8e83-475e-957d-6aa0089715f6 [TYPE: Guid (0:0:0)] … INSERT INTO mt_bib.Buch (Version, HiddenAt, Autor, Titel, Erscheinungsdatum, Zusammenfassung, Genre, Raumnummer, AusgeliehenAm, IstVerfuegbar, Inhaltsangabe_id, Verlag_id, Isbn, Sprache, Id) VALUES (@p0, @p1, @p2, @p3, @p4, @p5, @p6, @p7, @p8, @p9, @p10, @p11, @p12, @p13, @p14);@p0 = 1 [TYPE: Int64 (0:0:0)], @p1 = NULL [TYPE: DateTimeOffset (10:0:0)], @p2 = 'autor7847955c-f30d-4417-a430-1c160d447c5c' [TYPE: AnsiString (8000:0:0)], @p3 = 'titel0f35b1ee-3597-48e8-8b31-cb725b4e808c' [TYPE: AnsiString (8000:0:0)], @p4 = 2022-02-08T18:19:16.5618826+01:00 [TYPE: DateTimeOffset (10:0:0)], @p5 = 'Zusammenfassung77e9dd47-88af-405b-8494-fc1b9cd7a4f6' [TYPE: AnsiString (8000:0:0)], @p6 = 'Krimi' [TYPE: String (4000:0:0)], @p7 = 'A.0.0' [TYPE: String (4000:0:0)], @p8 = NULL [TYPE: DateTimeOffset (10:0:0)], @p9 = TRUE [TYPE: BOOLEAN (0:0:0)], @p10 = f6f7fab1-dd06-4ffa-b0c2-975e1ce2957a [TYPE: Guid (0:0:0)], @p11 = NULL [TYPE: Guid (0:0:0)], @p12 = '2534115145' [TYPE: AnsiString (8000:0:0)], @p13 = 'Deutsch' [TYPE: AnsiString (8000:0:0)], @p14 = 9bd56389-272d-44ab-a607-0a977c0c28c6 [TYPE: Guid (0:0:0)] …
Hier prüft NHibernate nicht, ob Entitäten bereits vorhanden sind, sondern führt direkt ein Insert aus.
Auch bei der Verwendung von Persist() wird Datenänderungsprotokoll geschrieben.
Batching für Inserts aktivieren
Die Anbindung geschieht über den BatchProcessingNHibernateExtender. Der Extender kann regulär in einem RepositoryConfigurator eingebunden werden. Als Konstruktor-Parameter wird die Batch-Größe angegeben, die angibt, wieviele Insert-Anweisungen auf einmal zum SQL-Server gesendet werden sollen.
public class BuchRepositoryConfigurator : NHibernateConfigurator
{
...
protected override IEnumerable<INHibernateExtender> Extenders
{
get
{
const int BatchProcessingSize = 100; // Siehe https://nhibernate.info/doc/nhibernate-reference/batch.html: "Set the ADO batch size to a reasonable number (say, 10-50)"
List<INHibernateExtender> extenders = new(base.Extenders);
extenders.Add(new BatchProcessingNHibernateExtender(BatchProcessingSize));
return extenders;
}
}
}
Batching bei Updates nutzen
Auch bei Updates können wie in https://nhibernate.info/doc/nhibernate-reference/performance.html, Kapitel 21.6. Batch updates beschrieben, Batches verwendet werden. Beachte hierbei, dass der Coarse Grained Lock bspw. nicht funktioniert! Dies ist also ein Trade-off.
Batching bei Deletes nutzen
Das Batching per Delete führt ein entsprechendes Delete-Statement aus, um mehrere Entitäten per Schlüssel performant zu löschen (DELETE FROM . WHERE ID IN (..)).
Das Löschen per Batch unterstützt kein kaskadierenden Verhalten beim Löschen. Möchte man also ein gesamtes Aggregat löschen, müssen die bereitgestellten Methoden DeleteByBatch() sowie DeleteChildrenByBatch() für jede Entität in geeigneter Reihenfolge aufgerufen werden.
Des Weiteren unterstützt das Löschen per Batch kein Coarse-Grained Lock und kein Optimistic Locking.
Auch das Datenänderungsprotokoll ist hierdurch abgeklemmt.
Löschen von Aggregate-Root Objekten
BuchId buchId = ...
using BuchRepository repository = new();
repository.DeleteByBatch(new[] { buchId });
repository.Flush();
Löschen von Aggregate-Child Objekten
using BuchRepository repository = new();
repository.DeleteChildrenByBatch<Seite, Guid>(new[] { seiteId }); // bei Verwendung von EntityId entsprechend DeleteChildrenByBatch<Seite, SeiteId>(...)
repository.Flush();
Batching bei Abfragen
Über den BatchFetchSizeExtender wird für alle Mappings ein Batch-Size für ClassMaps und Assoziationen (HasMany und References) über den NHibernateConfigurator in einer Größe von 2000 gesetzt.
Diese kann wie folgt über den Extender umkonfiguriert werden:
public class BuchRepositoryConfigurator : NHibernateConfigurator, IBuchRepositoryConfigurator
{
...
protected override IEnumerable<INHibernateExtender> Extenders
{
get
{
foreach (INHibernateExtender nHibernateExtender in base.Extenders)
{
if (nHibernateExtender is BatchFetchSizeExtender batchFetchSizeExtender)
{
batchFetchSizeExtender.DefaultBatchFetchSize = 200;
}
}
return base.Extenders;
}
}
}
Die Batch-Größe kann auch in der ClassMap einzeln wie folgt übersteuert werden:
public sealed class BuchMap : ClassMap<Buch>
{
public BuchMap()
{
Schema(DatabaseInfo.DatabaseSchema);
BatchSize(2); // => Konvention: wie kann man das deaktivieren? => BatchSize(1)
...
}
}
Auch bei HasMany ist dies wie folgt möglich:
HasMany(x => ((IInhaltsangabeData)x).Seiten)
.BatchSize(2) // Übersteuerung der Konvention. Anmerkung: hier wirkt dann dieser Batch-Size, nicht der von SeiteMap
.Not.KeyNullable()
.Not.KeyUpdate()
Performante Abfragen
Das performante Abfragen von Objekten ist mit NHibernate ein etwas komplexeres Thema. Siehe hierzu unbedingt
Validierung
AggregateRoot-Klassen sollten die Schnittstelle ISupportsValidation implementieren; die Methode ValidateRegardingPersistence() wird dann im NHibernate PreUpdate- bzw. PreSaveEventListeners aufgerufen, um sicherzustellen, dass die zu speichernde Aggregat gültig bezüglich Persistenz ist. Wenn das Aggregat nicht gültig ist, wird eine Validierungsausnahme geworfen. Die Validierung ist hierarchisch; d.h. die Gültigkeit die Kind-Entitäten sollen auch geprüft werden.
Eine Registrierung im Repository ist nicht mehr notwendig, es reicht die Implementierung von ISupportsValidation.
Logging
Hier gibt es mehrere Möglichkeiten:
- Muster: SQL-Logging
- Vewendung des
StatementExecutionsScopeim Code - NHibernate-Logging
- Logging über die app.config
- hierbei werden im Debug-Modus die SQL-Ausgaben in die Ausgabe des Visual Studio geschrieben
<configuration>
...
<system.diagnostics>
<switches>
<add name="Schleupen.CS.PI.Framework.DependencyInjection" value="4" />
<add name="Schleupen.CS.PI.Framework.ErrorHandling" value="4" />
<add name="Schleupen.CS.PI.Framework.Persistence" value="4" /> <!-- Dies ist die relevante Stelle -->
<add name="Schleupen.CS.PI.Framework.Testing" value="4" />
</switches>
<performanceCounters filemappingsize="1048576" />
</system.diagnostics>
...
</configuration>
Dies kann auch in der web.config der Webapplikation erfolgen. Möchte man bspw. auch Log-Ausgaben auf dem Sprintintegrationsrechner erhalten, so kann dies wie folgt konfiguriert werden:(hierzu muss das Compiler-Flag TRACE im Studio eingeschaltet sein (was ggf. ein Sicherheitsloch darstellt!)):
<configuration>
...
<system.diagnostics>
<trace autoflush="false" indentsize="4">
<listeners>
<remove name="Default" />
<add name="myListener" type="System.Diagnostics.TextWriterTraceListener" initializeData="c:\tmp\myListener.log" />
</listeners>
</trace>
</system.diagnostics>
...
Siehe https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.tracelistener?redirectedfrom=MSDN&view=net-5.0
https://stackoverflow.com/questions/5931702/wheres-result-of-system-diagnostics-trace-writeline
Benutzte Muster
- Muster: EntityId
- Muster: Aggregate
- Muster: Entity
- Muster: ValueObject
- Muster: Aggregate-Persistence-Model
- Muster: Aggregate-Persistence-Mapping
- Muster: Dependency Injection
- Muster: Effiziente Abfragen mit NHibernate
- Muster: Aggregate-Änderungen per Repository propagieren
- Muster: Anschluss Diagnoseprotokollierung
- Muster: Anschluss Datenänderungsprotokoll
- Muster: SQL-Logging
- Muster: Effiziente Abfragen mit NHibernate
- Muster: Effiziente Abfragen mit Future Queries
Stärken und Schwächen
Stärken
- Vereinfachter Zugriff auf die Datenbank
- Flexibilität, Funktionalität von NHibernate kann voll genutzt werden
Schwächen
- Erhöhter Aufwand bei der Implementierung
- Detaillwissen von NHibernate notwendig