The conception, birth, and first steps of an application named Charlie

Subscribe: Atom or RSS

A Word on Lazy Loading

by Alister Jones (SomeNewKid)

In my previous weblog entry, I described my fundamental misunderstanding of how the ASP.NET Cache works. But while I finally understood that the cache works like a shadow, I needed Charlie’s cache to work like a box. The reason I needed a box is that Charlie’s business objects use lazy loading. And what, you ask, is lazy loading?

One of the fundamental business objects in Charlie is the Document entity, which represents a document on the internet. The Document entity includes a Containers property, which exposes a ContainerCollection. For example, the Homepage document might include a Header container, a Menu container, a Content container, and a Footer container. Another object can “get” these containers through the Document.Containers property. So, a fully-loaded Document object would look like this:

What you will see from the above diagram is that the Document object contains to the Containers object. You will also notice that the Containers object is a much bigger object than the Document object itself. (If you took away the Containers object, the Document object would collapse to a much smaller size.) The Document object just includes simple properties such as its URL and its Title. The Containers however include Models, Views, Controllers, and much more.

If a particular Document object is being presented to the user, then we do want these Containers. The Containers will hold the header, content, footer, and everything else in the Document. So to display the Document, we need its Containers.

However, if we are presenting a site map page or similar, then we do not need these Containers. All we want is the Document with its URL and its Title. Loading up all of those Containers for every Document on the site map page will incur a major database hit, when we won’t be using the Containers anyway.

Lazy loading is a technique by which we load up just the main business object, and not all of its child business objects. In this example, we would load up the Document object, but leave the Containers property as null. Here is how the code looks, followed by an illustration of the half-loaded Document business object.

public class Document
{
    public ContainerCollection Containers
    {
        get
        {
            // We'll come back to this
        }
    }
    private ContainerCollection containers = null;
    
    // rest of class
}

You will see that, to start with, the private containers field is null. A given Document’s containers are not loaded when the Document is retrieved from the database. This means that any Document used is nice and light, and does not carry a heavy Containers collection. Again, if we are working with a site map or similar page, the Containers will not be used, so it is a waste to load them. However, if another class requests the Containers from a given Document, then that is the point at which the Document will have to go and fetch its containers from the database. Here is how that looks in code, and how you might envisage the process.

public class Document
{
    public ContainerCollection Containers
    {
        get
        {
            if (this.containers == null)
            {
                this.containers = 
                    ContainerManager.GetCollectionByDocumentId(this.Id);
            }
            return this.containers;
        }
    }
    private ContainerCollection containers = null;
    
    // rest of class
}

This process of deferred loading of child business objects is known as lazy loading. I have said that lazy loading means that Charlie wants the cache to work like a box, and not like a shadow. Why?

Let’s look at how a Document might be loaded. (This is not how it looks in Charlie. This is just a simple example.)

public class DocumentManager
{
    public Document GetDocumentById(Int32 id)
    {
        String cacheKey = "Charlie.Document." + id.ToString();
        
        // If we have the document in cache, return it
        Object cached = HttpContext.Current.Cache[cacheKey];
        if (cached != null && cached is Document)
        {
            return cached as Document;
        }
        
        // Get the document from the database
        Document document = DataAccessLayer.LoadDocumentById(id);
            
        // Store the document in cache
        HttpContext.Current.Cache.Insert(cacheKey, document);
        
        // Return the newly-loaded document
        return document;
    }
    
    // rest of class
}

To put the code into words, the DocumentManager first checks whether the requested Document has been loaded before and has been placed into the cache. If so, that cached version will be returned. If the requested Document has not been loaded recently, then it will be fetched from the database. That newly-fetched Document will be stored into cache, so that the next request for the same Document can be served from the cache rather than incurring another database hit.

Let us presume that the DocumentManager receives a request for a Document that has not been loaded recently. It requests the document from the Data Access layer. Keeping in mind that the Document uses lazy loading, it has a whopping hole where its Containers would be. Here then is a diagram of this newly-loaded Document.

The DocumentManager will store this new Document in the cache.

The original document is returned to the application. The application is presenting a single full page, and not presenting a site map or similar page. So, it will call the Document.Containers property in order to get the containers it needs to present the Document. Because the Containers property is lazy loaded, this will force the Containers to be fetched separately, and used to “fill out” the original Document. What is important to note is that because the cache works like a shadow, the cached version of the document is also filled out.

The document will be returned to Peter, who is a visitor to the website. Now, Samantha requests the same document. This time, the DocumentManager will see that it does have a cached version of the same Document, so it will return the Document from cache, rather than loading it from the database. However, because the cached version shadowed changes to the last-requested original version, then the cache will return the filled-out version of the Document, and not the half-empty, lazy-loaded version of the same Document.

This is no good. We don’t want the DocumentManager to sometimes return a lazy-loaded version, and sometimes return a fully-loaded version. Peter and Samantha may have different authorization roles, or may have different user preferences, so the Document needs slightly different Containers for each user. (Or Peter may be an administrator, and has changed the Containers or their content.) To allow Peter and Samantha to view slightly different versions of the same Document, we need the DocumentManager to always return a lazy-loaded Document, and never return a fully-loaded Document. Moreover, the class that requests a Document from the DocumentManager should never have to worry about the possibility that the returned Document may be an old, filled-out version. That calling class should be able to presume that the returned Document is always a new, lazy-loaded version. For this to be true, we need the cache to work like a box, and not like a shadow.

Before Charlie, I had never used lazy-loaded business objects. Before Charlie, I had never used localised business objects. Before Charlie, I had never used fine-grained security on business objects. So, when my inexperience with secured, personalized, localised, lazy-loaded business objects was combined with my misunderstanding of the cache, all hell broke loose in Charlie. The ultimate solution was to clone an object going into the cache, and then clone an object coming out of the cache.

public class DocumentManager
{
    public Document GetDocumentById(Int32 id)
    {
        String cacheKey = "Charlie.Document." + id.ToString();
        
        // If we have the document in cache, get it
        Object cached = HttpContext.Current.Cache[cacheKey];
        if (cached != null && cached is Document)
        {
            Document original = (Document)cached;

            // So that changes are not shadowed, 
            // return a clone, not the cached original.
            Document clone = original.Clone();
            return clone;
        }
        
        // Get the document from the database
        Document original = DataAccessLayer.LoadDocumentById(id);
        
        // Clone the original document
        Document clone = original.Clone();
            
        // Store the clone in cache
        HttpContext.Current.Cache.Insert(cacheKey, clone);
        
        // Return the newly-loaded, original document
        return original;
    }
    
    // rest of class
}

By cloning an object before it is placed in the cache, and then cloning that object as it comes out of the cache, the cache works like a box and not like a shadow. And by having the cache work like a box and not like a shadow, the integrity of lazy-loaded business objects is preserved.

One final note is that cloning a business object is not as easy as calling the Clone method, since by default there is no such method. The Clone method in Charlie’s business objects is a custom method of the underlying Entity System. This custom method is derived from Expert C# Business Objects.

Wow, what a long weblog entry. It is my hope that it saves someone else from experiencing the same problems that I experienced with caching lazy-loaded business objects.

by Alister Jones | Next up: The EntityCache Object

0 comments

----