Prolog
Als erstes gilt es an dieser Stelle eine Entschuldigung auszusprechen: im letzten Beitrag habe ich die ArrayDependencyFacility sowie den ArraySubDependencyResolver benutzt, ohne die Angabe zu machen, dass die Quellen aus der Feder von Alexander Groß stammen. Mea culpa und Asche auf mein Haupt.
Um es wieder gut zu machen, stammen viele der folgenden Vorgehensweisen, Methoden und Klassen auch von ihm.
Gesteckte Ziele
Was soll erreicht werden? Wir haben Daten. Meistens. Wie im ersten Teil schon erwähnt, soll mittels NHibernate darauf zugegriffen werden. Um der Konfiguration durch XML-Dateien zu entgehen, wird für das Mapping FluentNHibernate eingesetzt. Es sollte allerdings kein statisches Mapping sein, sondern die Module sollen ihre Mappings selbst mitbringen. Die Konfiguration der Datenverbindung soll auch irgendwie erfolgen. Tests sind nötig, sie sollten gegen SQLite gefahren werden (Speicher-Datenbanken), das Programm selbst wird durch einen Connection-String konfiguriert. Man bräuchte auch eine allgemeine Möglichkeit, mir der Datenbank zu sprechen, CRUD-Befehle abzusetzen – womöglich innerhalb von Transaktionen – aber auch Queries werden benötigt.
Eine lange Liste… fangen wir einfach mit dem an, was wir immer speichern wollen: einen Teil unserer Domäne.
Eine Domänen-Entität
Ungeachtet vieler Diskussionen, ob die ID eines Datenbank-Objektes nun besser durch eine GUID oder einen Integer abgebildet werden sollte, ist der allgemeine Konsens doch der, dass die ID selbst nichts mit der Businesslogik zu tun haben sollte. Die DomainEntity stellt sie in unserem Fall als int zur Verfügung.
Ebenfalls wird im Allgemeinen als unschön angesehen, wenn eine Änderung eine andere überschreibt. Hierfür bzw hiergegen wird ein Versionsfeld eingesetzt.
Damit wir 2 Entities miteinander vergleichen können, implementieren wir noch IEquatable und mappen im wesentlichen die Gleichheit auf die Gleichheit der Ids.
Insgesamt sieht unsere Basisklasse also so aus:
public class DomainEntity : IEquatable<DomainEntity>
{
int _id;
protected DomainEntity()
{
}
protected DomainEntity(int id)
{ _id = id;
}
public virtual int Id
{
get { return _id; }
protected set { _id = value; }
}
public virtual int Version { get; protected set; }
public virtual bool Equals(DomainEntity other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return Id.Equals(default(int)) ? base.Equals(other) : other.Id.Equals(Id);
}
public override bool Equals(object obj)
{
return Equals(obj as DomainEntity);
}
public override int GetHashCode()
{
return Id.Equals(default(int)) ? base.GetHashCode() : Id.GetHashCode();
}
public static bool operator ==(DomainEntity left, DomainEntity right)
{
return Equals(left, right);
}
public static bool operator !=(DomainEntity left, DomainEntity right)
{ return !Equals(left, right);
}
}
Datenbank-Zugriffe
Wir wissen nun also ungefähr, welche Daten wir mit unserer Datenquelle austauschen wollen, aber noch nicht, wie genau das funktionieren soll. Uns fehlt ein Gespräch mit dem Server, also nehmen wir doch genau das als Interface: IDbConversation. Die Gesprächsoptionen sind ja vorgegeben, wir nehmen InsertOnCommit als Create- und Update-Funktion, DeleteOnCommit als Delete-, sowie GetById als Read-Funktion. Zusätzlich untersützen wir Transaktionen und Queries:
public interface IDbConversation : IDisposable
{
TResult Query<TResult>(IDomainQuery<TResult> query);
void UsingTransaction(Action action);
TResult GetById<TResult>(object key);
void InsertOnCommit(object instance);
void DeleteOnCommit(object instance);
}
Aber Moment – was ist denn nun ein IDomainQuery? Sicherlich eine Abfrage, die ein irgendwie geartetes Ergebnis liefert, wie sich unschwer am Interface ablesen läßt:
public interface IDomainQuery<TResult>
{
TResult Execute(ISession session);
}
Nun mappen wir unsere IDbConversation auf unsere NHibernate-Session unter der Annahme, wir hätten eine INHibernateSessionFactory, die uns eine solche Session erzeugen kann:
public class DbConversation : IDbConversation
{
readonly ISession _session;
public DbConversation(INHibernateSessionFactory sessionFactory)
{
_session = sessionFactory.CreateSession();
}
public void Dispose()
{
if (_session != null)
_session.Dispose();
}
public TResult Query<TResult>(IDomainQuery<TResult> query)
{
return query.Execute(_session);
}
public void UsingTransaction(Action action)
{
using( var transaction=_session.BeginTransaction())
{
try
{
action();
transaction.Commit();
}
catch (Exception)
{
transaction.Rollback();
throw;
}
}
}
public TResult GetById<TResult>(object key)
{
return _session.Load<TResult>(key);
}
public void InsertOnCommit(object instance)
{
_session.SaveOrUpdate(instance);
}
public void DeleteOnCommit(object instance)
{
_session.Delete(instance);
}
}
Der Code dürfte wenig Raum für Überraschungen lassen, und das Interface INHibernateSessionFactory wurde ja quasi schon verweggenommen:
public interface INHibernateSessionFactory : IDisposable
{
ISession CreateSession();
}
Zwischenstand: wir haben einen Weg gefunden mit der Datenbank zu kommunizieren, wir haben ein Basis-Objekt, das wir abrufen und abspeichern können. Jetzt fehlt noch die Konfiguration des Datenbankzugriffs.
Datenbank-Konfiguration
Was kann es an einer Datenbank-Verbindung schon viel zu konfigurieren geben? Man nehme einen Connection-String, übergebe ihn NHibernate, und fertig. Nun ja, fast fertig. Man könnte zum Beispiel noch ein paar Module mitkonfigurieren wollen, an die man vorher nicht gedacht hat. Aber nur im Debug-Modus. Oder den Aufbau zur Datenbank mitprotokollieren. Und wo kommen eigentlich die Tabellendefinitionen ins Spiel?
Fangen wir mit dem Connection-String an. Die Persistenz sollte konfigurierbar sein, durch eine IPersistenceConfiguration:
public interface IPersistenceConfiguration
{
IPersistenceConfigurer GetConfiguration();
}
Im Falle eines SQL-Servers könnte die Implementierung der Schnittstelle etwa so aussehen:
public class SqlServerPersistenceConfiguration : IPersistenceConfiguration
{
readonly string _connectionString;
public bool ShowSql { get; set; }
public SqlServerPersistenceConfiguration(string connectionString)
{
_connectionString = connectionString;
}
public IPersistenceConfigurer GetConfiguration()
{
var configuration = MsSqlConfiguration
.MsSql2005
.ConnectionString(c => c.Is(_connectionString))
.ProxyFactoryFactory(typeof(ProxyFactoryFactory).AssemblyQualifiedName)
.AdoNetBatchSize(10)
.UseReflectionOptimizer()
.UseOuterJoin();
if (ShowSql)
{
configuration.ShowSql();
}
return configuration;
}
}
Auf die selbe Art und Weise soll das Persistenzmodell selbst konfigurierbar sein. Der Unterschied ist, dass es im Gegensatz zur Persistenzkonfiguration mehrere Mappings gibt, die es zu berücksichtigen gilt. Nachdem diese allerdings gesammelt worden sind, sollen sie zusammen angewandt werden. Was zu folgendem Interface führt:
public interface INHibernatePersistenceModel
{
void AddMappings(MappingConfiguration configuration);
}
und wie oben bereits angesprochen sammelt die Implementierung lediglich alle vorkommenden Mappings:
public IMappingContributor[] MappingContributors
{
get;
set;
}
public void AddMappings(MappingConfiguration configuration)
{
MappingContributors.Each(x => x.Apply(configuration));
}
Ein MappingContributor hat nur eine Aufgabe, nämlich seine eigenen Mappings der aktuellen Konfiguration hinzuzufügen:
public interface IMappingContributor
{
void Apply(MappingConfiguration configuration);
}
Jetzt wird es allerdings so sein, daß alle weiteren MappingContributors in externen Assemblies hinzugefügt werden. Um nicht alle Mappings – also ClassMap<Type> – einzeln angeben zu müssen, lassen wir sie uns einfach hereinreichen: wir lassen FluentNHibernate selbst danach suchen:
public class FluentMappingFromAssembly : IMappingContributor
{
readonly Assembly _assembly;
public FluentMappingFromAssembly(string assembly)
{
_assembly = Assembly.LoadFrom(assembly);
}
public void Apply(MappingConfiguration configuration)
{
configuration.FluentMappings.AddFromAssembly(_assembly);
}
}
Es fehlt noch der letzte Punkt: wie könnte man mitprotokollieren, wie weit die Konfiguration der Datenbankverbindung fortgeschritten ist? Man könnte die Fortschrittspunkte als Zustände definieren, und für jeden Zustand eine eigene Methode aufrufen. In etwa:
public interface INHibernateInitializationAware
{
void BeforeInitialization();
void Configuring(NHibernate.Cfg.Configuration configuration);
void Configured(NHibernate.Cfg.Configuration configuration);
void Initialized(NHibernate.Cfg.Configuration configuration, ISessionFactory sessionFactory);
}
Mit Hilfe dieses Interface könnte man den NHProfiler im Debug-Modus aktivieren, sobald die Initialisierung abgeschlossen ist:
public class NhProfilerInitializer : INHibernateInitializationAware
{
public bool Enabled { get; set; }
public NhProfilerInitializer()
{
EnableProfiler();
}
[Conditional("DEBUG")]
void EnableProfiler()
{
Enabled = true;
}
public void BeforeInitialization() { }
public void Configuring(NHibernate.Cfg.Configuration configuration) { }
public void Configured(NHibernate.Cfg.Configuration configuration) { }
public void Initialized(NHibernate.Cfg.Configuration configuration, ISessionFactory sessionFactory)
{
if (!Enabled)
return;
NHibernateProfiler.Initialize();
}
}
Mit Hilfe dieser Vorbereitungen können wir nun endlich die Konfiguration der Datenbank selbst angehen. Wir konfigurieren die Datenbankverbindung, fügen die Tabellendefinitionen an, lassen uns im Debug-Modus die Tabelle nach neuester Definition erzeugen, und erhalten als Gegenleistung eine INHibernateSessionFactory, die uns ISessions erzeugen kann:
public class NHibernateSessionFactory : INHibernateSessionFactory
{
static readonly object InitializationSynchronization = new object();
readonly IPersistenceConfiguration _persistenceConfiguration;
readonly INHibernatePersistenceModel _persistenceModel;
ISessionFactory _sessionFactory;
public NHibernateSessionFactory(IPersistenceConfiguration persistenceConfiguration,
INHibernatePersistenceModel persistenceModel)
{ _persistenceConfiguration = persistenceConfiguration;
_persistenceModel = persistenceModel;
}
public INHibernateInitializationAware[] Initializers
{
get;
set;
}
ISessionFactory CreateSessionFactory()
{
Initializers.Each(x => x.BeforeInitialization());
var configuration = Fluently.Configure()
.Database(_persistenceConfiguration.GetConfiguration())
.Mappings(_persistenceModel.AddMappings);
configuration.ExposeConfiguration(c => Initializers.Each(x => x.Configuring(c)));
var actualConfiguration = configuration.BuildConfiguration();
Initializers.Each(x => x.Configured(actualConfiguration));
CreateDatabaseWhenDebug(configuration);
_sessionFactory = configuration.BuildSessionFactory();
Initializers.Each(x => x.Initialized(actualConfiguration, _sessionFactory));
return _sessionFactory;
}
~NHibernateSessionFactory()
{
Dispose(false);
}
protected virtual void Dispose(bool @explicit)
{
if (@explicit)
{
if (_sessionFactory != null)
{
_sessionFactory.Dispose();
_sessionFactory = null;
}
}
}
public ISession CreateSession()
{
if (_sessionFactory == null)
{
lock (InitializationSynchronization)
{
if (_sessionFactory == null)
{
_sessionFactory = CreateSessionFactory();
}
}
}
var session = _sessionFactory.OpenSession();
session.FlushMode = FlushMode.Commit;
return session;
}
[Conditional("DEBUG")]
static void CreateDatabaseWhenDebug(FluentConfiguration configuration)
{
if (configuration == null) throw new ArgumentNullException("configuration");
configuration.ExposeConfiguration(
config => new SchemaUpdate(config).Execute(false, true));
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
public void Configure()
{
CreateSessionFactory();
}
}
Die Persistenz-Konfiguration sowie das Persistenz-Modell werden per Construktor-Injektion vom IoC-Container hineingereicht, die optionalen Initialisierer hingegen per Property-Injection. Schön zu sehen an dieser Stelle, wie beim Erzeugen der Session Factory die einzelnen Stadien durchgereicht werden.
Und wofür das alles?
Jetzt haben wir diese Unmengen an Code erzeugt, und können noch nicht einmal eine einfache Tabellenabfrage generieren – kann das gut sein?
Fügen wir erst einmal die in der CastleBootstrapper.cs fehlende Funktion hinzu, die im letzten Teil noch auskommentiert blieb:
static IEnumerable<IRegistration> GetPersistenceRegistrations()
{
yield return Component
.For<IPersistenceConfiguration>()
.ImplementedBy<SqlServerPersistenceConfiguration>()
.Parameters(Parameter.ForKey("connectionString").Eq(ConfigurationManager.AppSettings["DbConnection"]));
yield return AllTypes
.FromAssemblyContaining(typeof (IMappingContributor))
.BasedOn(typeof (IMappingContributor))
.WithService.Base();
yield return Component
.For<INHibernatePersistenceModel>()
.ImplementedBy<NHibernatePersistenceModel>();
yield return AllTypes
.FromAssemblyContaining(typeof (INHibernateInitializationAware))
.BasedOn(typeof (INHibernateInitializationAware))
.WithService.Base();
yield return Component
.For<INHibernateSessionFactory>()
.ImplementedBy<NHibernateSessionFactory>();
yield return Component
.For<IDbConversation>()
.ImplementedBy<DbConversation>()
.LifeStyle.Transient;
}
Man sieht auf den ersten Blick, wie die einzelnen Module der Datenbankanbindung getreu der Separation of Concerns aufgeteilt wurden. Jede Komponente ist für sich testbar, und das Gesamtpaket ist offen für Erweiterungen.
Bereits oben haben wir gesehen, wie einfach wir das NHibernate-Profiling aktivieren können, ohne die Software ändern zu müssen. Und es stimmt, mangels Tabellendefinitionen können wir nicht einmal eine Tabelle ausgeben. Das soll sich nun ändern.
Ein Beispielmodul
Wir fügen jetzt eine neue Tabellendefinition hinzu, ohne dass die bisherigen Projekte davon betroffen sind.
Beginnen wir zB mit einer Benutzerverwaltung für die einzelnen Module des im ersten Teil angedeuteten Backends, dem sogenannten User Management System. Wir fügen im Projekt Lucifer.Ums.Model die beiden Entities User und UserRole hinzu:
public class User : DomainEntity
{
public virtual string Name { get; set; }
public virtual UserRole UserRole { get; set; }
}
public class UserRole : DomainEntity
{
public virtual string Name { get; set; }
}
Da wir wahrscheinlich auch Listenansichten befüllen möchten, erzeugen wir auch die beiden Queries hierfür:
public class AllUsersQuery : IDomainQuery<IEnumerable<User>>
{
public IEnumerable<User> Execute(ISession session)
{
return session.Query<User>();
}
}
public class AllUserRolesQuery : IDomainQuery<IEnumerable<UserRole>>
{
public IEnumerable<UserRole> Execute(ISession session)
{
return session.Query<UserRole>();
}
}
Nun machen wir im Projekt Lucifer.Ums.Mapping der Datenbank die beiden Tabellen per FluentNHibernate bekannt:
public class UserMap : ClassMap<User>
{
public UserMap()
{
Id(d => d.Id).GeneratedBy.HiLo("10");
Map(d => d.Name).Length(40);
References(d => d.UserRole).Not.Nullable();
Version(d => d.Version);
}
}
public class UserRoleMap : ClassMap<UserRole>
{
public UserRoleMap()
{
Id(d => d.Id).GeneratedBy.HiLo("10");
Map(d => d.Name).Length(40);
Version(d => d.Version);
}
}
Und damit der IoC-Container unsere Implementierungen auch findet, registrieren wir unsere MappingContributors:
public class UmsWindsorInstaller : IWindsorInstaller
{
public void Install(IWindsorContainer container, IConfigurationStore store)
{ GetRegistrations().ForEach(x => container.Register(x));
}
static IEnumerable<IRegistration> GetRegistrations()
{ yield return Component .For<IMappingContributor>()
.ImplementedBy<FluentMappingFromAssembly>()
.Parameters(Parameter.ForKey("assembly").Eq(typeof(UserRoleMap).Assembly.CodeBase))
.Named("UmsMappingsFromAssembly");
}
}
Hier sehen wir den Anwendungsfall des oben erwähnten FluentMappingFromAssembly: FluentNHibernate wird sich durch diese Registrierung die ClassMaps UserMap und UserRoleMap selbst aus Lucifer.Ums.Mapping auflösen.
Und das war schon die Anbindung eines neuen Moduls an die Datenbank. Die geeigneten Initialisierungen vorausgesetzt, könnten wir nun eine Abfrage aller Benutzer in einer Konsole ausgeben:
IDbConversation dbConversation;
dbConversation
.Query(new AllUsersQuery())
.Each(x =>
{
System.Console.WriteLine("Id {0}, Name {1}", x.Id, x.Name);
});
Nur bräuchte man hierfür kaum diesen Aufwand 
Ausblick
Was noch fehlt sind weitere Module, damit die Benutzerverwaltung auch Sinn macht, der Ausbau der Benutzerverwaltung selbst, und endlich auch sichtbare Ergebnisse. So wären Dialoge und Listen etwas für einen nächsten Teil in dieser Reihe.
Wie bereits im letzten Teil erwähnt, ist der Quelltext auf GitHub verfügbar.