Primed Cache

Summary

Purpose

Explicitly populates a cache with a predicted set of data. A primed cache is useful for data that is read frequently and predictably.

Scenario

Primed Cache alleviates the problem of slow, incremental cache population exhibited by the Demand Cache pattern. The primary difference is that a primed cache explicitly populates the cache with an anticipated subset of data before making any database requests. A single comprehensive query primes the cache with a single, relevant set of data that the application is likely to reference. Most of the time this single query is much faster than the sum of the smaller queries requires to populate the cache on demand.

Primed cache refines the concepts that the Cache Accessor describes and imposes more structure for its cache keys. Primed cache uses partial keys and specific keys. A partial key usually corresponds to a discrete set of data with common characteristics whereas a specific key corresponds to an exact data item in the cache. Primed cache uses a partial key to read the entire set of matching data from the database and stores it in the cache, whereas callers uses specific keys when reading data from the cache. The caching logic must be able to infer whether the supplied key is a partial specification of another key in order to determine whether referenced data has been primed.

For example, an application to manage a web site can determine which pages users are allowed to visit by storing authorization data in the database. When a user signs in, the application reads all authorization information for that user and stores it in the user's cache (each user will have his/her cache). A user name and page make up a specific key while the partial form of this key contains only the user name without the page name. 

Defining key relationships in broad forms allows you to implement priming cache logic with a single uniform framework that works with multiple data types and database entities.

Structure & UML

The following figure illustrates the static structure for the Primed Cache :

This structure is similar to the Data Accessor. The CacheAccessor class defines the entry point for all the client's data access operations. It manages both cache and database interactions. Cache implements the cache storage mechanism and IDataAccessor is responsible for accessing the database. 

CacheAccessor maintains a list of partial keys that have been pre-populated (recall: partial keys are used to read from the database - for example, UserName). This list of pre-populated partial keys is used to avoid repeated data-access operations that involve the same partial keys. CacheAccessor also uses this list to determine if a specific key should be in the cache or not (recall: specific keys are used to read specific data items from the cache - for example, UserName+PageName). If a client requests a specific key that is not in the cache, but corresponds to one of the partial keys in this list, the CacheAccessor can infer that there is no matching database data. The IKeyFactory interface defines the primary customization point for this pattern. CacheAccessor uses a ConcreteKeyFactory to assign specific keys to individual data items that it reads during a priming operation.

The following figure illustrates the sequence diagram for the Primed Cache:

A client uses a CacheAccessor object to pre-populate cache entries based on a partial key. The CacheAccessor object first checks its list of pre-populated partial keys to ensure that data for that partial key has not been already populated. If data has not been pre-populated, the CacheAccessor issues a database read operation that selects all data that match that key. It then sends the data items to a ConcreteKeyFactory which generates a specific key for each data item (the CacheAccessor uses these keys when it stores data in the cache). Finally, the CacheAccessor adds the partial key to its list of pre-populated partial keys .

Example

// Represents partial and specific keys such as (UserName = 'Yazan') && (PageIndicator = "Support")
internal class Key
{
    // Data members
   
private Hashtable m_htKeys = new Hashtable();

    // Properties
   
public Hashtable Keys { get { return m_htKeys;} }

    // Data members
   
public void AddKey( string strAttribute, string strValue )
    {
        if (! m_htKeys.Contains( strAttribute ) )
            m_htKeys.Add( strAttribute, strValue );
    }

    // Determines if this instance of the key is a partial specification of the given key
    // For example, if oSpecificKey contains (UserName='YAZAN', Region='UK', Page='Support')
    // and this key contains (UserName='YAZAN', Region='UK), then this key is partial of
    // oSpecificKey
   
public bool IsPartialOf( Key oSpecificKey )
    {
        // Loop over all fields of this key
        foreach (DictionaryEntry de in m_htKeys)
        {
            string strAttribute = (string)de.Key;

            // Does the specific key contain this attribute?
            if (!oSpecificKey.Keys.Contains(strAttribute))
                return false;

            // Does the specific key contain the same value for this attribute
            if (oSpecificKey.Keys[strAttribute] != m_htKeys[strAttribute])
                return false;
        }
        return true;
    }

    // Construct a SQL where clause from the key values
    public string ToFilter()
    {
        StringBuilder sb = new StringBuilder();
        foreach (DictionaryEntry de in m_htKeys)
        {
            if (sb.Length != 0) sb.Append( "," );     // If more than one attribute
            sb.AppendFormat( "{0} = {1}", (string)de.Key, (string)de.Value );
        }
        return sb.ToString();
    }
}

internal interface IKeyFactory
{
    Key NewSpecificKey( object obDomainObject );
}

internal class WebAuthorizationData
{
    // Data members
   
private string m_strUserName;
    private string m_strPageIdentifier;

    // Properties
   
public string UserName
    {
        get { return m_strUserName; }
        set { m_strUserName = value; }
    }
    public string PageIdentifier
    {
        get { return m_strPageIdentifier; }
        set { m_strPageIdentifier = value; }
    }
}

internal class AuthorizationKeyFactory : IKeyFactory
{
    public Key NewSpecificKey( object obDomainObject )
    {
        // Get the authorization object
        WebAuthorizationData obAuthData = (WebAuthorizationData)obDomainObject;

        Key obSpecificKey = new Key();
        obSpecificKey.AddKey( "UserName", obAuthData.UserName );
        obSpecificKey.AddKey( "PageIdentifier", obAuthData.PageIdentifier );

        return obSpecificKey;
    }
}

internal class CacheAccessor
{
    // Data members
    private Hashtable m_htCache;
    private ArrayList m_alPrimedPartialKeys;
    private IKeyFactory m_obKeyFactory;

    public CacheAccessor(Hashtable cache, ArrayList alKeys, IKeyFactory factory)
    {
        m_htCache = cache;
        m_alPrimedPartialKeys = alKeys;
        m_obKeyFactory = factory;
    }

    // Pre-populates the cache based on a partial key (UserName = "...")
    public void PrePopulate( Key obPartialKey )
    {
        // Do not read data for a partial key if key has already been primed
        if (m_alPrimedPartialKeys.Contains( obPartialKey )) return;

        // This partial key has not been primed. Read data from the database
        DataTable dt = GetDataFromSomewhere();

        // Now iterate over each row in dt assigning a specific key for each row
        foreach (DataRow row in dt.Rows)
        {
            // Get a domain object corresponding to the current row
            object obDomain = GetDomainObjectForRow( row );

            // Get a specific key for this row
            Key key = m_obKeyFactory.NewSpecificKey( obDomain );

            // Now cache the domain object if not already cached
            if (!m_htCache.Contains( key ))
                m_htCache.Add( key, obDomain );
        }

        // Finally, indicate that this partial key has been primed
        m_alPrimedPartialKeys.Add( obPartialKey );
    }

    public object Read( Key obSpecificKey )
    {
        if (m_htCache.Contains( obSpecificKey ))
            return m_htCache[obSpecificKey];
        else
            return null;
    }
}

public class ClientWrapper
{
    private Hashtable         m_htMap = new Hashtable();
    private ArrayList         m_alPrimedPartialKeys = new ArrayList();
    private IKeyFactory       m_obKeyFactory = null;
    private CacheAccessor     m_obCacheAccessor = null;

    public ClientWrapper()
    {
        IKeyFactory m_obKeyFactory = new AuthorizationKeyFactory();
        m_obCacheAccessor = new CacheAccessor( m_htMap, m_alPrimedPartialKeys, m_obKeyFactory );
    }

    public void PrimeAuthorizationCache( string strUserName )
    {
        Key obPartialKey = new Key();
        obPartialKey.AddKey( "UserName", strUserName );
        m_obCacheAccessor.PrePopulate( obPartialKey );
    }

    public void ReadCacheAuthoData( string strUserName, string strPageIdentifier )
    {
        Key obSpecificKey = new Key();
        obSpecificKey.AddKey( "UserName", strUserName );
        obSpecificKey.AddKey( "PageIdentifier", strPageIdentifier );
        object obDomain = m_obCacheAccessor.Read( obSpecificKey );
    }
}

Applicability

Use this pattern when:

Strategies / Variants

Depending on application functionality, it might be perfectly valid to explicitly request data that does not exist in the database. For example, in the web site management application, a missing entry means that the user has no authorization to view a specific page. You can indicate this fact by storing a special placeholder entry in the cache that represents the notion that no data exists in the table. This placeholder entry prevents CacheAccessor from repeatedly scanning the primed key list, each time finding no matched data.

If you combine Primed Cache with Demand Cache, then there is one additional case to address. When a client primes a partial key, the CacheAccessor stores all the corresponding data in the cache. If the client later attempts to read using a specific key that does not match any cache entries, then you have two options:

  1. Issue a database read operation like Demand Cache described.
  2. You can iterate through the primed partial key list asking each primed partial key if it is a partial representation of the specific key. This requires you to define an additional IKey operation called IsPartialOf, which strictly defines the relation between partial and specific keys. If you find a primed partial key (i.e., UserName) that is a partial representation of a specific key (i.e., UserName+PageNumber), then you know that there is physical data in the database that corresponds to this specific key. At this point, you can add a placeholder cache entry for that specific key to indicate that there is no matching data. Obviously, this placeholder indicates that there is no need to access the database because data simple does not exist.

Benefits

Liabilities

Related Patterns