NHibernate is loading same object multiple times - please help!

966 views Asked by At

I've just been reading the trace for one of my ASP.NET pages and I've noticed that the page user is being loaded from the database each time the user is required. Since each ISession is supposed to cache objects, I'm really mystified about this.

Logically, the problem must surely be one of the following two things:

  1. The ISession's cache isn't working properly
  2. Each time the user is requested, it's being loaded using a different ISession

I assume that the problem is number 2). I'm using Castle Windsor to manage object lifecycles so I've posted some of the code I'm using in case someone can help spot the problem. The classes being managed by Castle Windsor are:

  1. MooseUserRepository - a repository class for managing MooseUser instances (i.e. the page user in this case)
  2. KctcUnitOfWork - a wrapper for the ISession

MooseUserRepository has a constructor dependency on KctcUnitOfWork like this:

public MooseUserRepository(IUnitOfWork unitOfWork)
    {

    }

The config file looks like this:

<component id="KctcUnitOfWork" service="Kctc.BusinessLayer.Kctc.IUnitOfWork,Kctc.BusinessLayer" type="Kctc.NHibernate.Kctc.UnitOfWork,Kctc.NHibernate" lifestyle="PerWebRequest"/>
<component id="MooseUserRepository" service="Kctc.BusinessLayer.Kctc.Repositories.IMooseUserRepository,Kctc.BusinessLayer" type="Kctc.NHibernate.Kctc.Repositories.MooseUserRepository,Kctc.NHibernate" lifestyle="PerWebRequest"/>

Note the PerWebRequest lifestyles.

The Castle Windsor container is simply a static property of a sort of utility class called Moose.Application so it's always there:

private static IWindsorContainer _windsorContainer;

    public static IWindsorContainer WindsorContainer
    {
      get
      {
        if (_windsorContainer == null)
        {
          _windsorContainer = new WindsorContainer(new XmlInterpreter(HttpContext.Current.Server.MapPath("~/CastleWindsorConfiguration.xml")));
        }
        return _windsorContainer;
      }
    }

The page itself has a IMooseUserRepository instance like this:

private IMooseUserRepository _mooseUserRepository;
private IMooseUserRepository MooseUserRepository
  {
    get
    {
      if (_mooseUserRepository == null)
      {
        _mooseUserRepository = Moose.Application.WindsorContainer.Resolve<IMooseUserRepository>();
      }
      return _mooseUserRepository;
    }
  }

The user of the page is accessed by a property which looks like this:

private MooseUser PageUser
  {
    get { return MooseUserRepository.Load(ApplicationSettings.UsernameFromWeb); }}

It appears that these subsequent calls to PageUser are causing the duplicate SQL commands:

txtSubject.Enabled = PageUser.CanHandleLegalWorks;
    ddlDue.Enabled = PageUser.CanHandleLegalWorks;

Now obviously I can work around this problem by storing the loaded MooseUser object in a private variable, but my understanding is that the ISession is supposed to do this for me.

Can anyone hazard a guess as to what's going wrong?

4

There are 4 answers

2
David On BEST ANSWER

I've worked out what the problem is and it is quite a subtle one.

I'm retrieving the user with the following code:

private MooseUser PageUser
  {
    get { return MooseUserRepository.Load(ApplicationSettings.UsernameFromWeb); }
}

ApplicationSettings.UsernameFromWeb retrieves the username of the current user as far as ASP.NET is concerned. The username of the user is a natural key for the Users table, but it's not the primary key! As far as I know, the first level cache only works for objects retrieved by primary key.

Edit: I solved this problem by creating a property which stuffs the loaded user in HttpContext.Current.Items and checks there first before loading as per this article.

2
ema On

Like you have noticed in your question you are using a PerWebRequest lifestyle for the repository, so the ISession (used by repository) will be recreated on every request. I find this behavior the right one, the ISession should be created on every request and every operation on NH should be transacted.

If you wanna use one ISession as singleton you should declare the repositories lifestyle as singleton.

I think you should have some sort of SessionProvider o SessionFactory in your app, maybe you could work on that for the singleton session.

HTH

2
DanB On

You state the following:

Logically, the problem must surely be one of the following two things:

  1. The ISession's cache isn't working properly
  2. Each time the user is requested, it's being loaded using a
    different ISession

I think you may be confusing Nhibernate's First (Session) Level Cache with the Second Level Cache.

Sessions are cheap to manufacture and throw away. In a Web app, you would typically use one session per request. Once you get or load an entity for the first time it is placed into the first level cache which is scoped to the lifetime of the session. When the Session is closed and disposed of at the end of the request you will no longer have access to objects in the session-level cache. Indeed, each User is being loaded by different sessions- this is perfectly normal.

The Second-Level cache is scoped to the lifetime of the Session Factory. If 2nd-level cache is enabled, once you load an entity by its primary key, it is stored in the 2nd-level cache and can be accessed by ALL sessions without hitting the database again until it is removed from the cache. You will need to explicitly enable caching on a per-entity basis. This is the behaviour you are looking for.

Further reading:

edit

You'll need to pick a Cache Provider from the NHContrib project. You probably want SysCache2 which uses the Asp.Net cache but you could go with MemCached or Velocity or a couple of others if you wanted to. I also recommmend you give Nhibernate Profiler a try. I've found it invaluable in poking under the hood and finding out what Nhibernate's getting upto.

1
Jamie Ide On

You can use natural-id (NaturalId in Fluent NH) in your mapping to bypass second level cache expiration for just this situation.