maandag 30 april 2007

NHibernate Session Management in Domain Driven App's

As I've posted earlier, Billy McCafferty has written an excellent article on using NHibernate within ASP.NET applications.
In this article, he describes how to use the 'Open Session in View' pattern. This is perfectly useable and a very clean way of working with NHibernate in a DDD ASP.NET application, unfortunately, it is -imho- less (or not) useable in a WinForms application.
The 'Open Session in View' pattern implies that a new NHibernate ISession object is created at each request, and closed at the end of each request. (Changes are committed as well).

It is very important that the Repositories keep their hands off of Transaction Management. In other words: a repository should not start, commit or rollback a transaction. This is natural, since the repository doesn't know anything of the 'context' of the Unit of Work. Therefore, transaction managment should be a responsability of the client (the UI, the Service layer, whatever).
To achieve this, Billy stores the current ISession in the CallContext or HttpContext (he has made abstraction of which implementation is used offcourse. CallContext is used in the case of a WinForms app or a Unit-Test project, HttpContext is used in case of an ASP.NET app; see the source-code that accompagnies the article for more details).
In this way, the repositories can easily access the current ISession when they need it by just getting it out of the CallContext or HttpContext, and the ASP.NET application (the ASP.NET webpages) have access to the ISession as well, so they can start, commit and rollback the unit-of-work.
This is the ideal solution in ASP.NET, since you only need one 'ISession' on each request. At the end of the request, the Session can be closed (committed / rollbacked), so they're short-lived, and this is perfect.

However, in a WinForms application, you'll probably want more then one 'Unit of Work' at the same time, and therefore, have more then one open ISession at the same time.
An example will clarify this a bit more:

Suppose you've a WinForms application which consists of a form which lists all Customers. You can edit a customer by double-clicking it; a 'Customer-Detail' form will open up, where you can make changes to the Customer.

Now, what happens if you edit a Customer ? You'll retrieve the ISession from the CallContext, load the Customer from the DB, make some changes and confirm the changes; closing the form will close the ISession.
So far, no problem. However, what happens if you open another Customer-Detail form before you've committed the changes to the first Customer ?
What if you have 2 Customer Detail Forms open, make changes to Customer 1, save the changes and close the form. Afterwards, you want to save the changes to Customer 1... The problem now is that you've closed the ISession when you've closed the first Customer-Detail form. Customer 2 was attached to the same session which is now closed. If you want to save the changes to customer 2, you'll need an ISession, so you'll require a new ISEssion in this case. Then, NHibernate will think that Customer 2 is a new Customer, and will try to insert a new record to the DB where in fact, the existing Customer needs to be updated.

In my opinion, it is better to have a seperate Unit Of Work (ISession) for each instance of the CustomerDetail form in this case.
This implies that you should create a new ISession for each instance of the CustomerDetail form: this means that the ISession cannot be kept in the CallContext since otherwise, each form would share the same ISession.
Then... how should this be solved ? In my humble opinion, the best way to solve this is to create an ISession when an instance of a form is created, pass it to the repositories, and use it to handle the transaction(s) in the form:

public class CustomerDetailForm : Form
{
private ISession _unitOfWork;

private CustomerRepository _custRep;

private Customer _customer;

public void ShowCustomer( int customerId )
{
_unitOfWork = AppSettings.Instance.SessionFactoryObj.OpenSession();

_custRep = new CustomerRepository(_unitOfWork);

_customer = _custRep.GetCustomer(customerId);

// Disconnect from the DB, since we do not need DB action at this time
// anymore.
_unitOfWork.Disconnect();

}

public void OKButton_Click(object sender, EventArgs e )
{
// Reconnect to the DB
_unitOfWork.Reconnect();

ITransaction tx = _unitOfWork.BeginTransaction();
_custRep.Save (_customer);
tx.Commit();
}
}

I don't know if there's any better / cleaner way of doing this... If you know one, please let me know. :) It is offcourse cleaner if you could abstract the use of NHibernate's ISession by creating a class which encapsulates the ISession, but I wanted to keep things a bit sparse. :)

3 opmerkingen:

Anoniem zei

Some how I was thinking the same thing after reading Bill's article

Anoniem zei

Hi,
How would you approach keeping dirty or deleted flags on domain objects? I think they shouldn't contain these flags. But then how would you persist the objects? I guess, with hibernate's saveorupdate method, it's not necessary. But for other persistence methods what do you advise on this?

Frederik Gheysels zei

Hi,

NHibernate handles the state-tracking for you. In other words, NHibernate determines whether an object is new, updated or deleted.
It does this by keeping a copy of the 'original' object in the ISession. This means that, if an object is not attached to a Session, and you want to save this object, NHibernate thinks that this object is a new object.
If you have an object which you retrieve from its persistance store, and have to make changes to it, then, I would first 'lock' (using the ISession.Lock method) the object to a Session (if it hasn't been attached to a session yet), make the changes and call the Save method of the Session.
When using NHibernate, I also rely sometimes on the value of my 'Id' property of the business-object to know whether the object is new or not. (For instance, a new object (which doesn't exists in the DB yet), can have an Id property which value is -1).

If you do not use (N)Hibernate or some other O/R mapper which keeps track of state-management, you'll have to implement indeed a flag (or multiple flags) which indicate whether the object is new or modified. Then, I would indeed make those flags properties of my business object. These flags have nothing to do with the 'domain', and are indeed 'administrative' flags, but, imho, this is a pragmatic approach which keeps things simple.