This is part 3 of the AltNerdDinner Series.
One of the things I always disliked the most when exploring NHibernate was the need of declaring all the mappings in XMhelL. And don’t know about you, but that’s not something I want to be spending my time creating, modifying, hand-refactoring or debugging tedious tons of this crap.
Fast forward to 2008 and say hello to Fluent NHibernate. (And don’t forget to thank Jeremy Miller, James Gregory et al!). It is basically a way to programmatically declare the mappings, allowing you to go from this:
<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
namespace="QuickStart" assembly="QuickStart">
<class name="Cat" table="Cat">
<id name="Id">
<generator class="identity" />
</id>
<property name="Name">
<column name="Name" length="16" not-null="true" />
</property>
<property name="Sex" />
<many-to-one name="Mate" />
<bag name="Kittens">
<key column="mother_id" />
<one-to-many class="Cat" />
</bag>
</class>
</hibernate-mapping>
To this:
public class CatMap : ClassMap<Cat>
{
public CatMap()
{
Id(x => x.Id);
Map(x => x.Name)
.Length(16)
.Not.Nullable();
Map(x => x.Sex);
References(x => x.Mate);
HasMany(x => x.Kittens);
}
}
What it gives you, you may ask? Compile-time checks and refactoring support, for a start.
But wait, there is more!
Fluent NHibernate is built with Convention over Configuration in mind. So, if you adhere to it, you can greatly reduce the amount of mapping needed, by virtue of auto mapping. And you can combine that with explicit ClassMap mappings, like the one above, they are not mutually exclusive.
Mapping your domain can be as simple as:
AutoMap.AssemblyOf<Product>();
And if you call within the next 5 minutes we throw in Conventions for free!
"Ok, that might be great for a greenfield project, but I am working in a big brown piece of … code", I hear you saying. So you may not have the choice to call your Id’s "Id" or name your tables exactly after your classes. Conventions can greatly help here, allowing you to define your own conventions. That is, if your database naming follows any convention at all, and wasn’t defined by a bunch of monkeys typing at random.
For further info, check the site, wiki and mailing list.
AltNerdDinner’s mappings
Here are the mapping files for AltNerdDinner.
public class DinnerMap : ClassMap<Dinner>
{
public DinnerMap()
{
Id(x => x.DinnerID);
Map(x => x.Address).Not.Nullable();
Map(x => x.ContactPhone).Not.Nullable();
Map(x => x.Country).Not.Nullable();
Map(x => x.Description).Not.Nullable();
Map(x => x.EventDate).Not.Nullable();
Map(x => x.HostedBy).Not.Nullable();
Map(x => x.Latitude).Not.Nullable();
Map(x => x.Longitude).Not.Nullable();
Map(x => x.Title).Not.Nullable();
HasMany(x => x.RSVPs).Cascade.AllDeleteOrphan();
}
}
public class RSVPMap : ClassMap<RSVP>
{
public RSVPMap()
{
Id(x => x.RsvpId);
Map(x => x.AttendeeName).Not.Nullable();
References(r => r.Dinner).Nullable();
}
}
One gotcha that had me fiddling around for a while was that Nullable part on the Dinner reference in RSVPMap. If you don’t set it to nullable, you’ll get an exception when trying to persist the entities. And even if you set it to nullable, you’ll find out, NHibernate executes two queries to insert the RSVP.
INSERT INTO [RSVP] (AttendeeName, Dinner_id) VALUES (@p0, @p1);
select SCOPE_IDENTITY();
@p0 = ‘alberto’,
@p1 = 9
UPDATE [RSVP] SET Dinner_id = @p0 WHERE RsvpId = @p1; @p0 = 9, @p1 = 21
You can find more info about this behavior in the official nhibernate site.
So, what I have done instead is to set the RSVP as the managing part of the relationship, changing it back to not nullable and using Inverse() on the Dinner’s mapping side.
It is not possible to make the "many part" responsible for this. What this means is that we have to persist our entities calling:
Dinner dinner = _dinnerRepository.GetDinner(id);
var rsvp = new RSVP{ AttendeeName = username };
rsvp.Dinner = dinner;
dinner.Rsvps.Add(rsvp);
_dinnerRepository.Save(dinner);
That’s not very ideal, so I have included an AddRsvp() method in Dinner to handle that. Now I have:
Dinner dinner = _dinnerRepository.GetDinner(id);
dinner.AddRsvp(new RSVP{ AttendeeName = User.Identity.Name });
_dinnerRepository.Save(dinner);
Much cleaner! In order to avoid confusion when adding an RSVO, I have changed the type of the collection to an IEnumerable<RSVP>, so that the collection cannot be modified directly, but only by means of Dinner (which is a good practice if you want to follow DDD, anyway). Because of that, I had to modified the DinnerMap to use a backing field for it. In the future I might revisit this relationship to include the User in the domain again and change it to a less CRUD-y style. You can read more on Domain-Driven fluency from Udi Dahan.
Anyway, here is the final mapping I have come up with:
public class RSVPMap : ClassMap<RSVP>
{
public RSVPMap()
{
Id(x => x.RsvpId);
Map(x => x.AttendeeName).Not.Nullable();
References(r => r.Dinner).Not.Nullable();
}
}
public class DinnerMap : ClassMap<Dinner>
{
public DinnerMap()
{
Id(x => x.DinnerID);
Map(x => x.Address).Not.Nullable();
Map(x => x.ContactPhone).Not.Nullable();
Map(x => x.Country).Not.Nullable();
Map(x => x.Description).Not.Nullable();
Map(x => x.EventDate).Not.Nullable();
Map(x => x.HostedBy).Not.Nullable();
Map(x => x.Latitude).Not.Nullable();
Map(x => x.Longitude).Not.Nullable();
Map(x => x.Title).Not.Nullable();
HasMany(x => x.Rsvps).Access.CamelCaseField(Prefix.Underscore)
.Inverse().Cascade.AllDeleteOrphan();
}
}