Cache Accessor

Summary

Purpose

Decouples caching access logic from the underlying data model and data access details.

Scenario

In general, application and middleware caching logic rarely relates directly to any data access mechanism. Caching logic primarily focuses on how and when to access cached data and physical data. As you implement a caching solution you must determine which data is worth caching. For example, data that is read only once is not a good cache candidate as it consumes unnecessary storage overhead. Data that is read often (whether predictably or unpredictably) is a good cache candidate as it saves the application from having to issue identical database read operations.

It is common for developers to mix caching logic with data access logic making it harder to debug and maintain an overall implementation. The Cache Accessor logic decouples cache-access code from data-access code through a generic structure that promotes consistent and reusable cache utilization for all data types and database entities. As was the case with the Data Accessor, incorporating cache-access logic into a single component always you to incorporate sophisticated optimizations, collection, replications and administrative features that apply to multiple physical data access implementations or data types.

Structure & UML

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

As usual, we start with the entry point to the client and this is the CacheAccessor class.  The CacheAccessor manages both cache and database operations. Class Cache implements the actual cache storage mechanism. Its operations refer to cached data using unique keys (unique keys uniquely identify each data item in the storage). If data is stored in an ADO.NET DataTable then the unique key should already be present if the underlying database table has a key. For domain object, keys are represented as identity objects.

CacheAccessor delegates all its database operations to IDataAccessor which can be an ADO.NET provider, a Data Accessor implementation, or a domain object mapping. Note that the closer your define CacheAccessor operations to match those of IDataAccessor, the more transparent the CacheAccessor will be to your client code.

The following figure illustrates the sequence diagram when a client uses Cache Accessor to read cached data:

The CacheAccessor forms the cache key based on the Read operation's parameters. It finds the data in the cache and returns it without any database interaction. CacheAccessor class does not dictate when or how to populate the cache - all it does is access the cache. Demand Cache and Primed Cache are some of the patterns that can be used to populate a cache.

The following sequence diagram shows a client issuing a Write operation to the CacheAccessor. In addition to writing data to the cache, the CacheAccessor must also update the database to ensure that the database and the cache are synchronized:

Example

public class CacheAccessor
{
    // Data members
    DataTable m_dtCache = null; 

    // Constructors
   
public CacheAccessor(DataTable dt)
    {
        m_dtCache = dt;
    }

    // Reads a cached entry by first verifying that the primary keys are valid, and then finding data
    // based on the supplied keys

    public DataRow Read( Hashtable keys )
    {
        DataRow row = null;

        // Check that primary keys of the cache match those supplied by aPrimaryKeys (should match
        // in number and in column names)
        DataColumn[] dcPrimaryKeys = m_dtCache.PrimaryKey;
        if (dcPrimaryKeys.Length == keys.Count)
        {
            foreach (DataColumn dc in dcPrimaryKeys)
                if (!keys.Contains(dc.ColumnName))
                    throw new InvalidOperationException( "Invalid primary key names given" );
        }
        else
        {
            throw new InvalidOperationException( "Invalid number of primary keys given" );
        }

        // Copy the columns names into an array than be used within DataTable.Contains()
        object[] aPK = new object[ keys.Count ];
        keys.Keys.CopyTo( aPK, 0 );

        // Does the DataTable contain a record whose primacy keys are described by aPrimaryKeys?
        // If so, return the matching row
       
if (m_dtCache.Rows.Contains( aPK ))
            row = m_dtCache.Rows.Find( aPK );

        return row;
    }
}

// CLIENT

// Get data from database
DataTable dt = ExecuteSQL( "select TradeID, SecID, BrokerID, Cash, EntryDate from T" );

// Cache data in the accessor
CacheAccessor accessor = new CacheAccessor( dt );

// Now read data from the datatable
Hashtable htKeys = new Hashtable();
htKeys.Add( "TradeID", "12QWA" );
htKeys.Add( "SecID", "12333DF345" );
DataRow row = accessor.Read( htKeys );

...

Applicability

Use this pattern when:

Strategies / Variants

Consider these strategies when designing a cache accessor

Benefits

Liabilities

Related Patterns