I use Batch Programs, aka Batch Jobs, all the time for tasks that aren’t necessarily time-critical. In most organizations, there are many of these things running on servers all over the place. I recently had to write a batch job that did a bunch of database updates using NHibernate. The job updates two tables in a foreach loop. Within each loop, an NH transaction is started, committed or rolled back and disposed of. I usually don’t write all my code into the default program.cs file, but opt for writing some service to do the heavy lifting. I recently wrote one that had ran into some problems.
I wrote the service as I usually would. I setup my constructor to take everything it would need and made a StructureMap registry to bootstrap all my dependencies so I could work with the existing components.
Here’s my original batch service constructor:
public ProfileUpdateService(
ISession session, //for managing transactions
IProfileRepository repo //for getting/saving
)
{
//set private vars
}
My StructureMap registry was pretty basic. It takes an ISessionFactory instance in the constructor and uses it to setup the ISession resolution. The repository happens with a scanner’s default conventions:
For<ISession>() .Use(() => _sessionFactory.OpenSession()); //use the lambda to ensure my factory is configured when this is actually accessed
Here’s what my original code looked like (sort of):
var records = _repo.GetAll();
if(!records .Any())
return;
try
{
foreach (var record in records)
{
try
{
_transaction = _session.BeginTransaction();
var otherRecord = _repo.GetOtherThing(record.Something);
//do some processing first then save
_repo.DoFirstThing(record);
//do some processing first, then save
_repo.DoSecondThing(otherRecord);
_transaction.Commit();
}
catch(Exception ex)
{
//log it
//rollback
_transaction.Rollback();
}
finally
{
if(_transaction != null)
_transaction.Dispose();
}
}
}
finally
{
_session.Dispose();
}
During testing, some edge cases emerged that caused an exception while committing my changes. Since my use of NHibernate has followed the Session/Request pattern in web and WCF environments, I never really had to deal with exceptions and their impact on the NHibernate Session. But this case was different. There was no single Request to contain my Unit of Work and I soon found out that the exception being thrown was hosing up my Session. RTFM, right Corey?
Here’s what the NH reference doc has to say about it:
If the ISession throws an exception you should immediately rollback the transaction, call ISession.Close()
and discard the ISession instance. Certain methods of ISession will not leave the session in a consistent state.
I had to make some obvious changes. My batch service needs to be able to create an instance of an ISession and since my repository class takes an ISession in its constructor, I need to be able to create a new IProfileRepository instance as well. Since I wanted to maintain testability, I used the mighty Func<> as a factory.
Here’s what my constructor looks like now:
public ProfileUpdateService(
ISessionFactory sessionFactory,
Func<ISession,IProfileRepository> repoFactory
)
{
//set private vars
}
Now I had to change StructureMap to know how to resolve those dependencies. Using the Lambda in the Use() method gives me deferred execution, so I don’t have to worry about premature invocation of my ISessionFactory.
For<ISessionFactory>()
.Use(() => _factory);
For<Func<ISession, IProfileRepository>>()
.Use((session) => new ProfileRepository(session));
Finally, I had to restructure the code to better handle the exceptions. When one happens, I want to close and dispose of the ISession instance being used. I would also want to use a new Repository object since the current one will be using the damaged ISession object. To handle this, I relied on properties and lazy initialization. (note: this code may not be the most efficient, but it’s a batch job after all, so give me a break)
//use the lazy initialized repo
var records = ProfileRepository.GetAll();
if(!records .Any())
return;
try
{
foreach (var record in records)
{
try
{
_transaction = Session.BeginTransaction();
var otherRecord = ProfileRepository.GetOtherThing(record.Something);
//do some processing first then save
ProfileRepository.DoFirstThing(record);
//do some processing first, then save
ProfileRepository.DoSecondThing(otherRecord);
_transaction.Commit();
}
catch(Exception ex)
{
//log it
//rollback
_transaction.Rollback();
_session.Dispose();
_session = null;
_profileRepository = null;
}
finally
{
if(_transaction != null)
_transaction.Dispose();
}
}
}
finally
{
_session.Dispose();
}
//Using lazy initialization so I don't have to worry about these being destroyed
IProfileRepository ProfileRepository
{
get
{
if (_profileRepository == null)
_profileRepository = _profileRepositoryFactory(Session);
return _profileRepository;
}
}
ISession Session
{
get
{
if (_session == null)
_session = _sessionFactory.OpenSession();
return _session;
}
}
This really isn’t rocket science, but it’s a different way of thinking for those of us using NHibernate from the Web.