After a very long spell of inactivity, we’re back for Part 7 of the series!
Last time we left off with a decent domain model, a database, and the beginnings of NHibernate.
Since the last article, the NHibernate team has produced the ever-so-fantastic v1.2.0 beta 1. This release supports numerous new exciting features. Topping the list of cool new features is: native support for .NET Generics! Native SQL 2005 is also supported. Another feature was just enabled that will really help bridge the gap for those folks who will clutch to stored procedures forever: Stored Procedure / Custom Query Support.
I’ll start today by slapping in this version of NHibernate and do a little bit of housekeeping. With this new version of NHibernate, I no longer have a need for NHibernate.Generics (and thus I will remove it). Some people might still use it, as it has the nice side-effect of automatically managing bi-directional relationships (which we’ll visit later), however I’m going to show this manually in this series.
I’ve changed my EntityList<T> / EntityRef<T> / EntitySet<T> to normal generic collections, and NHibernate will use them automatically. These classes do provide you with an extra nifty feature (explained later), but it’s important to understand what it does under the hood, so I will implement the functionality the manual way, and later I’ll reference these classes to show how you might still want to use them.
So I’ve remove the old dll of NHibernate and I referenced in the new one. I also updated my nhibernate schema file in Visual Studio so that I get the updated intellisense. I compile, nothing is broken, and we’re ready to test! The beauty of having our unit tests is that we have much more confidence that we know exactly what to fix, and we also know the instant that we are done fixing any breaking changes. Our application returns to a known state of behavior very quickly.
Since I’ve made persistence level changes (the new NHibernate dll) as well as domain model changes (replacing NHibernate.Generics.Entity*<T> with standard .NET generic lists) I need to run both test assemblies.
Here are the immediate results of the tests:
TestCase 'Flux88.Videocracy.Persistence.Tests.PersonTester.TestCanListPeople' failed: NHibernate.InvalidProxyTypeException : Type 'Flux88.Videocracy.DomainModel.Person' cannot be specified as proxy: method get_Id should be virtual
c:\net\nhibernate-1.2.0.Beta1\nhibernate\src\NHibernate\Proxy\ProxyTypeValidator.cs(28,0): at NHibernate.Proxy.ProxyTypeValidator.Error(Type type, String reason)
c:\net\nhibernate-1.2.0.Beta1\nhibernate\src\NHibernate\Proxy\ProxyTypeValidator.cs(76,0): at NHibernate.Proxy.ProxyTypeValidator.CheckMethodIsVirtual(Type type, MethodInfo method)
c:\net\nhibernate-1.2.0.Beta1\nhibernate\src\NHibernate\Proxy\ProxyTypeValidator.cs(63,0): at NHibernate.Proxy.ProxyTypeValidator.CheckEveryPublicMemberIsVirtual(Type type)
c:\net\nhibernate-1.2.0.Beta1\nhibernate\src\NHibernate\Proxy\ProxyTypeValidator.cs(22,0): at NHibernate.Proxy.ProxyTypeValidator.ValidateType(Type type)
c:\net\nhibernate-1.2.0.Beta1\nhibernate\src\NHibernate\Cfg\Configuration.cs(915,0): at NHibernate.Cfg.Configuration.ValidateProxyInterface(PersistentClass persistentClass)
c:\net\nhibernate-1.2.0.Beta1\nhibernate\src\NHibernate\Cfg\Configuration.cs(891,0): at NHibernate.Cfg.Configuration.Validate()
c:\net\nhibernate-1.2.0.Beta1\nhibernate\src\NHibernate\Cfg\Configuration.cs(1035,0): at NHibernate.Cfg.Configuration.BuildSessionFactory()
c:\sandbox\Videocracy\Flux88.Videocracy.Persistence\SessionSource.cs(55,0): at Flux88.Videocracy.Persistence.SessionSource.Initialize()
c:\sandbox\Videocracy\Flux88.Videocracy.Persistence\SessionSource.cs(81,0): at Flux88.Videocracy.Persistence.SessionSource.GetSession()
c:\sandbox\Videocracy\Flux88.Videocracy.Persistence.Tests\PersonTester.cs(27,0): at Flux88.Videocracy.Persistence.Tests.PersonTester.TestCanListPeople()
We have a few failing tests, however they all relate to the same problem. NHibernate 1.2.0 beta 1 now sets everything to lazy-load by default. This means that our objects must be able to be inherited so that NHibernate can inject proxy subclasses of our objects (the consumer doesn’t know or care that this happens). To do this all of the methods and properties need to be virtual. This is a large change, and it’s not something I entirely agree with, but it’s very easy to turn off. (It pays to read the release notes of these 3rd party products!) We’ll discuss how to effectively lazy-load a bit further on.
In our mapping file, we can specify default-lazy=”false” to override this behavior. At a later time we may investigate and find that the class will benefit from lazy loading and we’ll revert it, but until then I’ll just make all the classes default-lazy=”false”.
Here’s the change:
<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.0"
namespace="Flux88.Videocracy.DomainModel"
assembly="Flux88.Videocracy.DomainModel"
default-lazy="false"
>
<class name="Person" table="People">
…
</class>
</hibernate-mapping>
Once that change is in place, all 5 of my persistence tests are passing. I have 6 failing tests in my domain model, mostly due to my careless replacement of the EntityRef stuff with standard .NET collections. The tests expose my oversights and once I fix them I’m back on track.
Ok, now where were we? (It’s been a while, so I open NUnit and see that we have Save/List/Load for a Person object, but nothing else). We introduced some simple mapping concepts, and this time we’ll dig in a bit deeper.
How does NHibernate deal with associations? Lets start with Account. Account has a collection of Customer objects that represent its members. In standard database terms, Account is a one-to-many with Customer. How do we map this kind of relationship with NHibernate?
There are a few choices when looking at the documentation for NHibernate. To understand them you must understand the semantics of each kind of list. Ask yourself these questions:
- is it one-to-one? many-to-one? many-to-many?
- does order matter?
- is it bidirectional?
From the NHibernate FAQ, we find this informative nugget, that explains the collections available to us (and their .NET counterparts):
- The <list> maps directly to an IList.
- The <map> maps directly to an IDictionary.
- The <bag> maps to an IList. A <bag> does not completely comply with the IList interface because the Add() method is not guaranteed to return the correct index. An object can be added to a <bag> without initializing the IList. Make sure to either hide the IList from the consumers of your API or make it well documented.
- The <set> maps to an Iesi.Collections.ISet. That interface is part of the Iesi.Collections assembly distributed with NHibernate.
Our Account -> Customer relationship is one-to-many. I could choose to allow one customer to have many accounts, but I don’t see any real use for that yet. Order does not matter either. I could have 4 members on an account, and it doesn’t really matter what order they are given to my in a list.
We also have to consider what each end of the association looks like. Right now I’m looking at it from the perspective of Account, where Account has many Customers (members). On the customer object, the customer only has one Account, so this isn’t a bidirectional association. (We’ll see an example of this later). Techically our collection maps to an ISet, but I’d rather not depend on the Iesi.Collections.dll, so I’m going to map it to a list (the only drawback is that the list will allow the same member twice, so I’ll just get around this in our model manually).
Here’s how we’ll deal with that:
/// <summary>
/// Adds a member to this account
/// </summary>
/// <param name="customer"></param>
public void AddMember(Customer customer)
{
//ignore multiple adds for the same customer.
if(_members.Contains(customer)) return;
_members.Add(customer);
//make sure that our association is handled on both sides
customer.Account = this;
}
/// <summary>
/// Removes a member from this account.
/// </summary>
/// <param name="customer"></param>
public void RemoveMember(Customer customer)
{
//ignore the call if the customer isn't a member of this account
if (!_members.Contains(customer)) return;
_members.Remove(customer);
//make sure that our association is handled on both sides
customer.Account = null;
}
The rest of the class is self-explanatory, so I won’t include it here. You’ll be able to download the project at the end of today’s article, however.
Next up is the mapping. Let’s take another look at the table structure for reference:
![CropperCapture[4]](http://www.flux88.com/uploads/CropperCapture[4]_small.jpg)
This is slightly different than what was in the last archive. Instead of an account having a single primary customer, any customer can be the ‘Owner’ of his/her account. I chose to keep the association simple for this demo. I also wanted to allow multiple ‘Owners’ for a single account. I’ve updated the tests to reflect this change.
Here you see that many customers belong to exactly one account. This is a one-to-many relationship from Account to Customer.
If we stop and think about the domain for a minute, we can determine that the Customer will be identified first (by his/her ID or Account #) and that needs to be able to pull up the account. I’d also like to keep this relationship bidirectional. That means that I need to map the relationship on both classes so that we can do this:
customer.Account ….
or this:
for each Customer c in _account.Members { … }
Here is the relevant mapping for Customer:
<!-- CUSTOMER -->
<joined-subclass name="Customer" table="Customers">
<key column="PersonId" />
<!-- todo: map this to an enumeration -->
<property name="RentalRestriction" type="Int32" column="rentalRestrictionId" />
<many-to-one name="Account" class="Account" column="accountId" cascade="save-update" />
</joined-subclass>
This tells NHibernate that this is the *many* end of a one-to-many association. We tell it the property name, type, foreign key column name, and our cascade strategy. This basically means, whenever we save or update our Customer, cascade the save/update to the Account as well. You can also specify to cascade deletes as well if you need to.
The Account mapping looks like this:
<class name="Account" table="Accounts" where="Active=1">
<!-- ... -->
<bag name="Members" access="nosetter.camelcase-underscore" lazy="true"
cascade="save-update" inverse="true">
<!-- this key refers to the Account key in the Customer table -->
<key column="accountId" />
<one-to-many class="Customer" />
</bag>
</class>
This section tells NHibernate to map this association as a bag. Lazy=true means that the collection will only actually be retrieved from the database once it is accessed. NHibernate does this by silently replacing our IList with a proxy that knows about the session and how to hydrate the collection. This is 100% transparent to our consumers, so make sure you are using this in appropriate places. Lazy=false will force NHibernate to pre-load the entire association (and it’s associations(and it’s associations(…. you get the point…))).
To test that this association worked, I wrote a simple test:
[Test]
public void CanSaveCustomerAndAccount()
{
Customer c = TestHelper.GetTestCustomer();
Account a = new Account();
a.AddMember(c);
using (ISession session = SessionSource.Current.GetSession())
{
using (ITransaction tx = session.BeginTransaction())
{
session.SaveOrUpdate(c);
session.Flush();
session.Evict(c); //make sure we don't get an object from the cache
Assert.AreNotEqual(0, c.Id, "Customer didn't get an Id after save");
Assert.AreNotEqual(0, a.Id, "Account didn't get an Id after save");
Customer customerFromDb = session.Get<Customer>(c.Id);
Assert.AreNotSame(c, customerFromDb, "Got same instance of customer!");
Assert.AreEqual(a, customerFromDb.Account, "Account didn't get saved");
tx.Rollback();
}
}
}
This test is passing and we have our first association mapped!
I think this is a good stopping point. We have migrated our version of NHibernate and our tests forced us to fix any outstanding issues before continuing. We also identified our first association, mapped it, and tested it.
From here on out I will step up the pace and start glossing over the details a bit and focus on getting working product done using this domain model. As I begin writing some UI components I will come across some features I have not developed, which may require more mapping changes and tests. In order to finish this series sometime this year I’ll be speeding up a bit. I will still continue to post the source so that you may follow along. If you have comments or questions, feel free to comment.
Here is the latest source:
File Attachment: Videocracy_07.zip (44 KB)