现在的位置: 首页 > 综合 > 正文

Domain Objects Caching Pattern for .NET

2012年10月13日 ⁄ 综合 ⁄ 共 15965字 ⁄ 字号 评论关闭

Domain Objects Caching Pattern for .NET

4033 Users read it.

(April 26, 2006)

Rating: 1 users 
 5 out of 5

Properties

Author(s) 
Iqbal Khan
User Level 
Advanced
Language 
C#, VB.NET
Platform 
Windows
Product(s) 
VS.NET, SQL Server 2000

Caching greatly improves application performance
because it reduces expensive trips to the database. But, if you want to
use caching in your application, you must decide what to cache and
where to put your caching code. The answer is simple. Cache your domain
objects and put caching code inside your persistence classes.

Abstract

Caching
greatly improves application performance because it reduces expensive
trips to the database. But, if you want to use caching in your
application, you must decide what to cache and where to put your
caching code. The answer is simple. Cache your domain objects and put
caching code inside your persistence classes.

Domain objects are central to any
application and represent its core data and business validation rules.
And, while domain objects may keep some read-only data, most of the
data is transactional and changes frequently. Therefore, you cannot
simply keep domain objects as "global variables" for the entirety of
your application because the data will change in the database and your
domain objects will become stale, thereby causing data integrity
problems. You'll have to use a proper caching solution for this. And,
your options are ASP.NET Cache, Caching Application Block in Microsoft
Enterprise Library, or some commercial solution like NCache from
Alachisoft. Personally, I would advise against using ASP.NET Cache
since it forces you to cache from presentation layer (ASP.NET pages)
which is bad.

The best place to embed caching in
your application is your domain objects persistence classes. In this
article, I am extending an earlier design pattern I wrote called Domain Objects Persistence Pattern for .NET.
I am going to show you how you can incorporate intelligent caching into
your application to boost its performance and what considerations you
should keep in mind while doing that.

Domain Objects Caching Pattern
attempts to provide a solution for domain object caching. The domain
objects in this pattern are unaware of the classes that persist them or
whether they're being cached or not, because the dependency is only
one-way. This makes the domain object design much simpler and easier to
understand. It also hides the caching and persistence code from other
subsystems that are using the domain objects. This also works in
distributed systems where only the domain objects are passed around.

Scope

Domain Objects, Domain Objects Caching.

Problem Definition
Domain
objects form the backbone of any application. They capture data model
from the database and also the business rules that apply to this data.
It is very typical for most subsystems of an application to rely on
these common domain objects. And, usually applications spend most of
their time in either loading or saving these domain objects to the
database. The actual "processing time" of these objects is very small
specially for N-Tier applications where each "user request" is very
short.

This means that performance of the
application depends greatly on how quickly these domain objects can be
made available to the application. If the application has to make
numerous database trips, the performance is usually bad. But, if the
application caches these objects close-by, the performance improves
greatly.

At the same time, it is very important
that domain object caching code is kept in such a central place that no
matter who loads or saves the domain objects, the application
automatically interacts with the cache. Additionally, we must hide the
caching code from the rest of application so we can take it out easily
if needed.

Solution

As
described above, the solution is an extension of an existing design
pattern called Domain Objects Persistence Pattern for .NET. That
pattern already achieves the goal of separating domain objects from
persistence code and from the rest of the application as well. This
double-decoupling provides a great deal of flexibility in the design.
The domain objects and the rest of the application is totally
unaffected whether the data is coming from a relational database or any
other source (e.g. XML, flat files, or Active Directory/LDAP).

Therefore, the best place to embed
caching code is in the persistence classes. This ensures that no matter
which part of the application issues the load or save call to domain
objects, caching is appropriately referenced first. This also hides all
the caching code from rest of the application and lets you replace it
with something else should you choose to do so.

Domain and Persistence Classes

In this sample, we will look at an Employee class from Northwind database mapped to the "Employees" table in the database.

// Domain object "Employee" that holds your data
public class Employee {
    // Some of the private data members
    // ...
    public Employee() {}

    // Properties for Employee object
    public String    EmployeeId { get {return _employeeId;} set {_employeeId = value;}}
    public String    Title { get {return _title;} set {_title = value;}}
    public ArrayList Subordinates { get {return _subordinates;} set {_subordinates = value;}}
}

// Interface for the Employee persistence
public interface IEmployeeFactory 
{    // Standard transactional methods for single-row operations
    void Load(Employee emp);   
    void Insert(Employee emp); 
    void Update(Employee emp); 
    void Delete(Employee emp); 

    // Load the related Employees (Subordinates) for this Employee
    void LoadSubordinates(Employee emp);

    // Query method to return a collection of Employee objects
    ArrayList FindByTitle(String title);
}
// Implementation of Employee persistence
public class EmployeeFactory : IEmployeeFactory
{    // all methods described in interface above are implemented here
}
// A FactoryProvider to hide persistence implementation
public class FactoryProvider 
{    // To abstract away the actual factory implementation
    public static IEmployeeFactory GetEmployeeFactory() { return new EmployeeFactory(); }
}

Sample Application

Below is an example of how a client application will use this code.

public class NorthwindApp
{
    static void Main (string[] args) {
        Employee emp = new Employee();
        IEmployeeFactory iEmpFactory = FactoryProvider.
GetEmployeeFactory();

        // Let's load an employee from Northwind database.
        emp.EmployeeId = 2;
        iEmpFactory.load(emp);

        // Pass on the Employee object
        HandleEmployee(emp);
        HandleSubordinates(emp.
Subordinates);

        // empList is a collection of Employee objects             
        ArrayList empList = iEmpFactory.
FindByTitle("Manager");
     }
}

The code above shows
you the overall structure of your classes for handling domain objects
persistence and caching. As you can see, there is clear-cut separation
between the domain and persistence classes. And, there is an additional
FactoryProvider class that lets you hide the persistence implementation
from rest of the application. However, the domain objects (Employee in
this case) moves around throughout the application.

Creating Cache Keys

Most cache systems
provide you with a string-based key. At the same time, the data that
you cache consists of various different classes ("Customers",
"Employees", "Orders", etc.). In this situation, an EmployeeId of 1000
may conflict with an OrderId of 1000 if you keys do not contain any 
type information. Therefore, you need to store some type information as
part of the key as well. Below are some suggested key structures. You
can make up your own based on the same principles.

  1. Keys for individual objects: If you're only storing individual objects, you can make up your keys as following:
    1. "Customers:PK:1000". This means Customers object with primary key of 1000.
  2. Keys for related objects: For
    each individual object, you may also want to keep related objects so
    you can easily find them. Here are keys for that:

    1. "Customers:PK:1000:REL:Orders". This means an Orders collection for Customer with primary key of 1000
  3. Keys for query results:
    Sometime, you run queries that return a collection of objects. And,
    these queries may also take different run-time parameters each time.
    You want to store these query results so the next time you don't have
    to run the query. Here are the keys for that. Please note that these
    keys also include run-time parameter values:

    1. "Employees:QRY:FindByTitleAndAge:Manager:40".
      This represents a query in "Employees" class called "FindByTitleAndAge"
      which takes two run-time parameters. The first parameter is "Title" and
      second is "Age". And, their runtime parameter values are specified.

Caching in Transactional Operations

Most transactional data
contains single-row operations (load, insert, update, and delete).
These methods are all based on primary key values of the object and are
the ideal place to start putting caching code. Here is how to handle
each method:

  1. Load Method: First
    check the cache. If data found, get it from there. Otherwise, load from
    the database and then put in the cache.
  2. Insert Method: After successfully adding a row in the database, add its object to the cache as well.
  3. Update Method: After successfully updating a row in the database, update its object in the cache as well.
  4. Delete Method: After successfully removing a row from the database, remove its object from the cache as well.

Below is a sample Load
method with caching logic included. Remember, you're only loading a
single object (single row) from the database.

// Check the cache before going to the database
void Load(Employee emp)
{     try
    {
        // Construct a cache-key to lookup in the cache first
        // The cache-key for the object will be like this: Employees:PK:1000
   
    string objKey = CacheUtil.GetObjectKey("Employee", emp.EmployeeId.ToString());

        object obj = CacheUtil.Load(objKey);
        if (obj == null)
        {
            // item not found in the cache. Load from database and then store in the cache
            _LoadFromDb(emp);

            // For simplicity, let's assume this object does not depend on anything else
            ArrayList dependencyKeys = null;
            CacheItemRemovedCallback onRemoved = null;

            CacheUtil.Store(objKey, emp, dependencyKeys, Cache.NoAbsoluteExpiration,
                            Cache.NoSlidingExpiration, CacheItemPriority.Default, onRemoved );

            // Now, load all its related subordinates
            LoadSubordinates(emp);
        }
        else
        {
            emp.Copy((Employee)obj);
        }
    }
    catch (Exception e)
    {
        // Handle exceptions here
    }
}

Please note a few things here.

  1. RemovedItemCallback:
    This is a delegate that allows your application to be notified
    asynchronously when the given item is removed from the cache.
  2. Expiration: You can specify an
    absolute time or idle time expiration. Although, we did not specify any
    expiration above, you could have specified two types of expirations.
    One is a fixed-time expiration (e.g. 10 minutes from now) and the
    second is an idle-time expiration (e.g. if item is idle for 2 minutes).

Caching Relationships

Domain objects usually
represent relational data coming from a relational database. Therefore,
when you cache them, you have to keep in mind their relationships and
cache the related objects as well. And, you also have to create
"dependency" between the object and all its related objects. The reason
being that if you remove the object from the cache, you should also
remove all its related objects so there is not data integrity problems.
Below is a code example of how to specify relationships in the cache.

// LoadSubordinates method
void LoadSubordinates(Employee emp)
{     try
    {
        // Construct a cache-key to lookup related items in the cache first
        // The cache-key for related collection will be like this: Employees:PK:1000:REL:Subordinates
   
    string relKey = CacheUtil.GetRelationKey("Employees", "Subordinates", emp.EmployeeId.ToString());
        string employeeKey = CacheUtil.GetObjectKey("Employee", emp.EmployeeId.ToString());

        object obj = CacheUtil.Load(relKey);
        if (obj == null)
        {
            // Subordinates not found in the cache. Load from database and then store in the cache
            _LoadSubordinatesFromDb(emp);

            ArrayList subordList = emp.Subordinates;

            // Result is a collection of Employee. Let's store each Employee separately in
            // the cache and then store the collection also but with a dependency on all the
            // individual Employee objects. Then, if any Employee is removed, the collection will also be
            // Count + 1 is so we can also put a dependency on the Supervisor
            ArrayList dependencyKeys = new ArrayList(subordList.Count + 1);
            for (int index = 0; index , subordList.Count; index++)
            {
                string objkey=CacheUtil.GetObjectKey("Employee",subordList[index].EmployeeId.ToString());
                CacheUtil.Store(objKey, subordList[index], null, Cache.NoAbsoluteExpiration,
                                        Cache.NoSlidingExpiration, CacheItemPriority.Default, null );
                dependencyKeys[index] = objKey;
            }
            dependencyKeys[subordList.Count] = employeeKey;

            CacheItemRemovedCallback onRemoved = null;
            CacheUtil.Store(relKey, subordinateList, dependencyKeys, Cache.NoAbsoluteExpiration,
                            Cache.NoSlidingExpiration, CacheItemPriority.Default, onRemoved );
        }
        else
        {
            // Subordinates already in the cache. Let's get them
            emp.Subordinates = (ArrayList)obj;
        }
    }
    catch (Exception e)
    {
        // Handle exceptions here
    }
}

In the above example,
you'll notice that a collection is being returned from the database and
each object inside the collection is stored individually in the cache.
Then, the collection is being cached as a single-item but with a cache
dependency on all the individual objects in the collection. This means
that if any the individual objects is updated or removed in the cache,
the collection is automatically removed by the cache. This allows you
to maintain data integrity in caching collections.

You'll also notice in
the above example that the collection has a cache dependency on the
"primary object" whose related objects the collection contains. This
dependency also means that if the primary object is removed or updated
in the cache, the collection will be removed in order to maintain data
integrity.

Caching in Query Methods

A query method returns
a collection of objects based on the search criteria specified in it.
It may or may not take any runtime parameters. In our example, we have
a FindByTitle that takes "title" as a parameter. Below is an example of
how caching is embedded inside a query method.

// Query method to return a collection
ArrayList FindByTitle(String title)
{     try
    {
        // Construct a cache-key to lookup items in the cache first
        // The cache-key for the query will be like this: Employees:PK:1000:QRY:FindByTitle:Manager
   
    string queryKey = CacheUtil.GetQueryKey("Employees", "Query", title);

        object obj = CacheUtil.Load(queryKey);
        if (obj == null)
        {
           // No items found in the cache. Load from database and then store in the cache
           ArrayList empList = _FindByTitleFromDb(title);
               
           // Result is a collection of Employee. Let's store each Employee separately in
           // the cache and then store the collection also but with a dependency on all the
           // individual Employee objects. Then, if any Employee is removed, the collection will also be
           ArrayList dependencyKeys = new ArrayList(empList.Count);
           for (int index = 0; index , empList.Count; index++)
           {
               string objKey = CacheUtil.GetObjectKey("Employee", empList[index].EmployeeId.ToString());

               CacheUtil
.Store(objKey, empList[index], null, Cache.NoAbsoluteExpiration,
                                       Cache.NoSlidingExpiration, CacheItemPriority.Default, null );
               dependencyKeys[index] = objKey;
           }

           CacheItemRemovedCallback onRemoved = null;
           CacheUtil.Store(queryKey, empList, dependencyKeys, Cache.NoAbsoluteExpiration,
                                Cache.NoSlidingExpiration, CacheItemPriority.Default, onRemoved );
        }
        else
        {
           // Query results already in the cache. Let's get them
           return (ArrayList) obj;
        }
    }
    catch (Exception e)
    {
        // Handle exceptions here
    }
}

In the above example,
just like the relationship method, you'll notice that a collection is
being returned from the database and each object inside the collection
is stored individually in the cache. Then, the collection is being
cached as a single-item but with a cache dependency on all the
individual objects in the collection. This means that if any the
individual objects is updated or removed in the cache, the collection
is automatically removed by the cache. This allows you to maintain data
integrity in caching collections.

Applications in Server Farms

The above pattern works
for both single-server or server-farm deployment environments. The only
thing that must change is the underlying caching solution. Most caching
solutions are for single-server environments (e.g. ASP.NET Cache and
Caching Application Block). But, there are some commercial solutions
like Alachisoft NCache (http://www.alachisoft.com)
that provide you a distributed cache that works in a server farm
configuration. This way, your application can use a cache from any
server in the farm and all cache updates are immediately propagated to
the entire server farm.

Conclusion

Using
the Domain Objects Caching Pattern, we have demonstrated how you should
embed caching code into your persistence classes. And, we've covered
the most commonly used situations of Load, Queries, and Relationships
with respect to caching. This should give you a good starting point to
determine how you should use caching in your application.

Author: Iqbal M.
Khan works for Alachisoft, a leading software company providing O/R
Mapping and Clustered Object Caching solutions for .NET. You can reach
him at iqbal@alachisoft.com or visit Alachisoft at www.alachisoft.com.

抱歉!评论已关闭.