Zweiter Start mit NHibernate
Es ist eine ganze Weile her seit ich mir das letzte Mal NHibernate angeschaut habe. Damals hielt ich NHibernate für zu unflexibel und zu kompliziert, bzw. aufwändig in der Konfiguration. Zumindest damals musste das Mapping der Datenbank auf die einzelnen Objekte in XML-Dateien geschrieben werden. Anschließend wurde per externes Programm, aus den Mapping Klassen generiert, die dann für den Zugriff auf die Datenbank genutzt werden konnten. Gerade die Nutzung eines externen Tools zur Generierung der nötigen Klassen erschien mir sehr umständlich.
Vor ein paar Wochen war es wieder so weit, dass ich mir NHibernate angeschaut habe.
Der Grund waren ein paar positive Äußerungen in der .NET Community. Dort war immer wieder zu lesen, wie gut NHibernate im Zusammenhang mit Fluent NHibernate und LINQ to NHibernate sei. Alleine schon die Aussicht auf LINQ und de Konfiguration über Fluent Interfaces machten mich wieder neugierig auf den am häufigsten erwähnten OR-Mapper.
Enttäuscht wurde ich nicht, ganz im Gegenteil. Mit dem Fluent NHibernate und LINQ to NHibernate ist es eine Freude mit NHibernate zu arbeiten. Nicht nur dass man ganz ohne XML-Konfigurationen auskommt, mit Hilfe des Fluent NHibernate lässt sich das Mapping und die Konfiguration per .NET Code schreiben. Also ob das nicht genug wäre, lässt sich mit Fluent NHibernate sogar die Datenbank dann aus dem erstellten Mapping und der Konfiguration generieren.
Hier mal das Ergebnis meiner ersten Spielerei.
Benötigt werden Fluent NHibernate (welches unter folgender URL heruntergeladen werden kann: http://fluentnhibernate.org/, die NHibernate.dll ist dort bereits enthalten) und LINQ to NHibernate (http://sourceforge.net/projects/nhibernate/files/
Vorbereitung
Angefangen hab ich mit der Erstellung meiner Daten-Objekte
Person.cs
public class Person: Entity<Person>
{
protected override void Initialize()
{
Projects = new List<Project>();
Tasks = new List<Task>();
}
public virtual List<Project> Projects { get; set; }
public virtual List<Task> Tasks { get; set; }
public virtual string FirstName { get; set; }
public virtual string LastName { get; set; }
public virtual string Email { get; set; }
public virtual string Telefone { get; set; }
}
Projekt.cs
public class Project: Entity<Project>
{
protected override void Initialize()
{
Persons = new List<Person>();
Tasks = new List<Task>();
}
public virtual List<Person> Persons { get; set; }
public virtual List<Task> Tasks { get; set; }
public virtual string Name { get; set; }
public virtual string Description { get; set; }
public virtual DateTime StartDate { get; set; }
public virtual DateTime EndDate { get; set; }
}
Task.cs
public class Task: Entity<Task>
{
public virtual Person Person { get; set; }
public virtual Project Project { get; set; }
public virtual string Name { get; set; }
public virtual string Description { get; set; }
public virtual DateTime StartDate { get; set; }
public virtual DateTime EndDate { get; set; }
}
Wie man jetzt evtl. erkennen kann ist eine kleine, einfache Aufgabenverwaltung geplant. Alle drei Objekte erben von einer generischen Basisklasse die weitere Funktionen und die Eigenschaft Id, vom Typ Guid – die in jeder Klasse enthalten ist – bereitstellt.
Mapping
Jetzt wird es dann eigentlich erst interessant: per Fluent NHibernate müssen die Mappings erzeugt werden. Als erste für die Person-Klasse:
public class PersonClassMap: ClassMap<Person>
{
public PersonClassMap()
{
Table("Persons");
Id(d => d.Id).GeneratedBy.Guid();
Map(d => d.FirstName).Not.Nullable().Length(50);
Map(d => d.LastName).Not.Nullable().Length(50);
Map(d => d.Email).Not.Nullable().Length(50);
Map(d => d.Telefone).Not.Nullable().Length(50);
HasMany(d => d.Tasks)
.Cascade.All()
.KeyColumn("PersonId")
.Inverse();
HasManyToMany(d => d.Projects)
.Cascade.All()
.Table("Person2Project")
.ParentKeyColumn("PersonId")
.ChildKeyColumn("ProjectId");
}
}
Als erste wird hier der Name der Datenbank-Tabelle definiert. Anschließend mit der Methode Id() der Primärschlüssel. GeneratedBy.Guid() gibt an dass es sich um einen Autogenerierten Guid-Wert handeln soll.
Die Methode Map ist die einfachste, sie mappt lediglich die Eigenschaften auf die Felder in der Datenbank. Not.Nullable()und Length(50) sind sprechend.
Interessant wird es mit HasMany(), hier wird definiert, dass einer Person mehrere Aufgaben zugewiesen sein können. KeyColumn("PersonId") zeigt auf den Fremdschlüssel in der Tabelle „Tasks“. Inverse zeig an dass es das Gegenstück zur Referenzierung in der Klasse Task ist. Dort sieht die Referenz wie folgt aus:
References(r => r.Person, "PersonId");
Die Angabe des Spaltennamens (2. Parameter) ist notwendig wenn man diesen nicht autogeneriert haben möchte, einem bestimmten Namensschema in der Datenbank folgen möchte oder bereits einen Namen in der Datenbank vergeben hat Kniffliger als HasMany() ist HasManyToMany() hier wird eine n:m Beziehung dargestellt und definiert, dass mehrere Personen mehreren Projekten zugeordnet werden können. Wenn der Tabellenname nicht autogeneriert werden soll muss dieser angegeben werden, genauso wie die Spalten. Erscheint zunächst einfach, allerdings muss man beachten, dass das Gegenstück (im Mapping für die Projekte) rückwärts verweist:
HasManyToMany(d => d.Persons)
.Cascade.All()
.Table("Person2Project")
.ParentKeyColumn("ProjectId")
.ChildKeyColumn("PersonId")
.Inverse();
Man sieht, die Spaltennamen für ParentKeyColumn sind vertauscht und es muss wieder ein Inverse() angehängt sein.
Die anderen beiden Mappings sehen wie folgt aus:
public class ProjectClassMap: ClassMap<Project>
{
public ProjectClassMap()
{
Table("Projects");
Id(d => d.Id).GeneratedBy.Guid();
Map(d => d.Name).Not.Nullable().Length(50);
Map(d => d.Description).Not.Nullable().Length(4000);
Map(d => d.EndDate).Not.Nullable();
Map(d => d.StartDate).Not.Nullable();
HasManyToMany(d => d.Persons)
.Cascade.All()
.Table("Person2Project")
.ParentKeyColumn("ProjectId")
.ChildKeyColumn("PersonId")
.Inverse();
HasMany(d => d.Tasks).Cascade.All().KeyColumn("ProjectId").Inverse();
}
}
public class TaskClassMap: ClassMap<Task>
{
public TaskClassMap()
{
Table("Tasks");
Id(d => d.Id).GeneratedBy.Guid();
Map(d => d.Name).Not.Nullable().Length(50);
Map(d => d.Description).Not.Nullable().Length(4000);
Map(d => d.EndDate).Not.Nullable();
Map(d => d.StartDate).Not.Nullable();
References(r => r.Project, "ProjectId");
References(r => r.Person, "PersonId");
}
}
Konfiguration
Im nächsten Schritt muss die Verbindung zur Datenbank konfiguriert werden und bei Bedarf eine SessionFactory erzeugt werden. Dazu habe ich mit eine statische Klasse angelegt die wiederum zwei Methode enthält: Die erste Methode erstellt die Konfiguration und die Zweite Methode erstellt die SessionFactory anhand dieser Konfiguration.
public static FluentConfiguration CreateConfiguration()
{
return Fluently.Configure()
.Database(MsSqlConfiguration.MsSql2005
.ConnectionString(c => c.FromAppSetting("DbConnection"))
.ShowSql())
.Mappings(m => m.FluentMappings
.AddFromAssemblyOf<IEntity>());
}
Im ersten Teil der Fluently Configuration wird die Verbindung zur Datenbank definiert. Hierfür gibt es verschiedenste Möglichkeiten. (Mehr Möglichkeiten lassen sich aus der Dokumentation entnehmen: http://wiki.fluentnhibernate.org/Main_Page) Ich habe MS SQL 2005 als Datenbankserver gewählt und lese den ConnectionString aus den App.Settings, bzw. der Web.Config aus. Außerdem möchte ich bei Fehlern den SQL-String sehen: ShowSql()
Anschließend wird das Mapping zugewiesen. Das geschieht ganz einfach indem man dem Fluent NHibernate sagt, wo die Assembly mit den Mappings liegen. Es werden automatisch alle Klassen als Mapping erkannt die von ClassMap<T> erben.
public static ISessionFactory CreateSessionFactory()
{
return CreateConfiguration().BuildSessionFactory();
}
Die zweite Methode ruft die erste auf und erstellt eine neue SessionFactory. Ich habe die Configuration von der Erstellung der Session getrennt um für die Generierung der Datenbank einen Konfigurationspunkt – für die Generierung der Datenbank – dazwischen zuhängen, die ich für die einfache Erzeugung der SessionFactory nicht benötige.
public void SetupDatabase()
{
FluentConfiguration conf = NHConfiguration.CreateConfiguration();
conf.ExposeConfiguration(BuildSchema).BuildSessionFactory();
}
private static void BuildSchema(Configuration conf)
{
new SchemaExport(conf).Drop(false, true);
new SchemaExport(conf).Create(false, true);
}
Mit der Methode ExposeConfiguration() werden Anweisungen zum Exportieren des Datenbankschemas eingebunden. Das Schema wird in dem Fall in die Datenbank selber exportiert. Es wird also die Datenbank erstellt.
Es empfiehlt sich die SessionFactory global, anwendungsweit bereit zu halten, da die Erzeugung der Factory sonst recht unperformant ist. Schließlich muss das Schema erst anhand des erstellten Mappings generiert werden
private static ISessionFactory sessionFactory;
public static ISessionFactory SessionFactory
{
get
{
if (sessionFactory == null)
sessionFactory = NHConfiguration.CreateSessionFactory();
return sessionFactory;
}
}
LINQ to NHibernate
Eine Session kann dann innerhalb eines Using-Blocks geöffnet werden:
using(var session = sessionFactory.OpenSession())
{
}
Innerhalb des Blocks kann dann endlich auch LINQ to NHibernate zum Einsatz kommen:
var q = from a in session.Linq<Person>()
where a.FirstName != ""
select a;
Die generische Methode Linq<T>() liefert ein Ergebnis vom Typ INHibernateQueryable<T> das wiederum von IQueryable<T> und IEnumerable<T> und somit wiederum bequem per LINQ abgefragt werden kann.
Fazit
NHibernate ist dadurch natürlich recht schnell einsetzbar, bequemer und auch etwas flexibler, denn das Mapping und auch die Konfiguration lassen sich theoretisch so natürlich auch dynamisch erzeugen. Die Benutzung von LINQ erleichtert die Abfragen ungemein. Fluent NHibernate macht die Konfiguration und das Mapping lesbarer.
Ich denke mit den beiden oben vorgestellten Erweiterungen ist NHibernate wirklich eines der genialsten OR-Mapper.
Update:
Das Beispielprojekt gibt es hier zum herunterladen: NHibernateTestApplication.zip (2.4MB)