Castle Demo AppLazy Loading and Scopes
First, a bug correction, in an earlier post I said that the user's OpenBugs property should be initalized exactly the same as AssignedBugs, this is a mistake, it should be initialized like this:
_openBugs =
new EntitySet<Bug>();The reason for this is that addition/removal to the OpenBugs shouldn't affect the AssignedBugs collection, which is what I accidently did.
Okay, so now we have Users and Bugs (they do seem to go hand in hand together, don't they? :-) ), but we have a potential problem in our hands that we need to pay attention to:
What happens when we load a user for autentication? We will also load all its bugs, and that can be quite a large number of bugs. It's wasteful in memory, time and network bandwidth. So, what do we do? I don't to give up the connection from the user to its bugs, it'll make my code much simpler, but it has the potential of seriously impacting the performance of my application. What would be best is if I could load the user's bugs on demand, so I would still get the ease of use, but without the performance.
It turns out that there is such a way, and it's called Lazy Loading. What this means is that Active Record will wait until you access the bugs collection of a user before it will load it. Here is how we define it:
[HasMany(typeof(Bug), ColumnKey = "AssignedTo", RelationType = RelationType.Set,
CustomAccess = "NHibernate.Generics.GenericAccessor, NHibernate.Generics",
Lazy=true)]
public ICollection<Bug> AssignedBugs
{
get { return _assignedBugs; }
}
[HasMany(typeof(Bug), ColumnKey = "AssignedTo", RelationType = RelationType.Set,
CustomAccess = "NHibernate.Generics.GenericAccessor, NHibernate.Generics",
Where = "Status = 1",Lazy = true)]
public ICollection<Bug> OpenBugs
{
get { return _openBugs; }
}
I bolded the part we changed. It is a fairly simple change, and it has the potential of saving us quite a bit in performance. So, I run the tests again, to see if this tiny change borke something, and get an error on this test:
[Test]
public void GetUsersBugs()
{
User user = new User("Foo", "Bar", "Foo@bar.com");
user.Save();
Bug nastyBug = new Bug("Nasty", "Figure it out!", user);
nastyBug.Save();
Bug minorBug = new Bug("Minor", "Spelling Mistake in the logs", user);
minorBug.Status = BugStatus.Fixed;
minorBug.Save();
User userDb = User.Find(user.Id);
Assert.AreEqual(2, userDb.AssignedBugs.Count); //Failed to lazily initialize a collection - no session
Assert.AreEqual(1, userDb.OpenBugs.Count);
}
Hm, that is a problem, what is this session that the exception talks about? This requires a little bit of detail with regard to how Active Record works:
When you issue a command that needs to touch the database, a session (which is parallel to a database connection, but with quite a bit more added to it) is created and destroyed on the fly. What happened here is that the session that was associated with the userDb variable, but it was disposed after loading it from the database, when we tried to access the AssignedBugs collection, Active Record tried to use the session, but that has already been disposed.
So, we have a problem, but there is also a solution, we need to tell Active Record to keep the session alive, and we can do that with a SessionScope. Now the following code will work correctly:
using (new SessionScope())
{
User userDb = User.Find(user.Id);
Assert.AreEqual(2, userDb.AssignedBugs.Count);
Assert.AreEqual(1, userDb.OpenBugs.Count);
}
The session is kept open until it is disposed, so this means that we can access lazy loaded collection at will.
A couple of notes about this post:
- You need to think about what parts of your objects you would lazy load. While it has the potential to increase performance, it also has the potential of creating a large number of queries against the database. (Load all users, load user 1's bug, load user 2's bug.... load user 1000's bugs, etc). There are solutions to those problems, but they will be much easier if you think of them ahead of time. One nice thing that Active Record allows is to tell it to usually load a collection in a lazy fashion, but in spesific cases to load all the data in one database trip.
- As you saw, even such a little change as adding a Lazy = true to the attribute can break the application. This is even more so with cascades (which I may or may not talk about). Those two are both issues that need consideration at the beginning of the application. Getting it right isn't that hard, but getting them wrong means that you'll have some work to do.
- On general, I don't recommend using SessionScope like in the example above, it's usually preferred to use the Session Per Request, which I'll show when we get to the MonoRail part.
More posts in "Castle Demo App" series:
- (03 Mar 2006) ViewComponents, Security, Filters and Friends
- (01 Mar 2006) Code Update
- (01 Mar 2006) Queries and Foreign Keys
- (28 Feb 2006) Complex Interactions
- (25 Feb 2006) CRUD Operations on Projects
- (22 Feb 2006) Let There Be A New User
- (22 Feb 2006) Updating our Users
- (20 Feb 2006) Getting serious, the first real page
- (20 Feb 2006) MonoRail At A Glance
- (20 Feb 2006) The First MonoRail Page
- (19 Feb 2006) Many To Many Relations
- (19 Feb 2006) Lazy Loading and Scopes
- (17 Feb 2006) Active Record Relations
- (17 Feb 2006) Getting Started With Active Record
Comments
Comment preview