Posted by: Iqbal M. Khan;
Alachisoft; www.alachisoft.com
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.
- Keys for individual objects:
If you're only storing individual objects,
you can make up your keys as following:
- "Customers:PK:1000".
This means Customers object with primary
key of 1000.
- 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:
- "Customers:PK:1000:REL:Orders".
This means an Orders collection for
Customer with primary key of 1000
- 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:
- "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:
- Load Method: First check the
cache. If data found, get it from there.
Otherwise, load from the database and
then put in the cache.
- Insert Method: After successfully
adding a row in the database, add its
object to the cache as well.
- Update Method: After successfully
updating a row in the database, update
its object in the cache as well.
- 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.
- RemovedItemCallback: This is
a delegate that allows your application
to be notified asynchronously when the
given item is removed from the cache.
- 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.