Autor: Iqbal Khan
O armazenamento em cache melhora muito o desempenho do aplicativo porque reduz as viagens caras ao banco de dados. Mas, se você quiser usar o cache em seu aplicativo, você deve decidir o que armazenar em cache e onde colocar seu código de cache. A resposta é simples. Armazene em cache seus objetos de domínio e coloque o código de armazenamento em cache dentro de suas classes de persistência. Os objetos de domínio são centrais para qualquer aplicativo e representam seus dados principais e regras de validação de negócios. E, embora os objetos de domínio possam manter alguns dados somente leitura, a maioria dos dados é transacional e muda com frequência. Portanto, você não pode simplesmente manter objetos de domínio como "variáveis globais" para todo o seu aplicativo porque os dados serão alterados no banco de dados e seus objetos de domínio ficarão obsoletos, causando problemas de integridade de dados. Você terá que usar uma solução de cache adequada para isso. E suas opções são ASP.NET Cache, Caching Application Block in Microsoft Enterprise Library ou alguma solução comercial como NCache da Alachisoft . Pessoalmente, eu desaconselharia o uso do ASP.NET Cache, pois ele força você a fazer o cache da camada de apresentação (páginas ASP.NET), o que é ruim. O melhor lugar para incorporar o cache em seu aplicativo são suas classes de persistência de objetos de domínio. Neste artigo, estou estendendo um padrão de design anterior que escrevi chamado Domain Objects Persistence Pattern para .NET. Vou mostrar como você pode incorporar o cache inteligente em seu aplicativo para aumentar seu desempenho e quais considerações você deve ter em mente ao fazer isso.
O Padrão de Cache de Objetos de Domínio tenta fornecer uma solução para o cache de objetos de domínio. Os objetos de domínio nesse padrão não têm conhecimento das classes que os persistem ou se estão sendo armazenados em cache ou não, porque a dependência é apenas unidirecional. Isso torna o design do objeto de domínio muito mais simples e fácil de entender. Ele também oculta o código de armazenamento em cache e de persistência de outros subsistemas que estão usando os objetos de domínio. Isso também funciona em sistemas distribuídos onde apenas os objetos de domínio são passados.
Objetos de Domínio, Cache de Objetos de Domínio.
Os objetos de domínio formam a espinha dorsal de qualquer aplicativo. Eles capturam o modelo de dados do banco de dados e também as regras de negócios que se aplicam a esses dados. É muito comum que a maioria dos subsistemas de um aplicativo dependa desses objetos de domínio comuns. E, geralmente, os aplicativos passam a maior parte do tempo carregando ou salvando esses objetos de domínio no banco de dados. O "tempo de processamento" real desses objetos é muito pequeno, especialmente para aplicativos N-Tier, onde cada "solicitação do usuário" é muito curto.
Isso significa que o desempenho do aplicativo depende muito da rapidez com que esses objetos de domínio podem ser disponibilizados para o aplicativo. Se o aplicativo tiver que fazer várias viagens ao banco de dados, o desempenho geralmente é ruim. Mas, se o aplicativo armazenar em cache esses objetos próximos, o desempenho melhorará muito.
Ao mesmo tempo, é muito importante que o código de cache de objeto de domínio seja mantido em um local tão central que, independentemente de quem carregue ou salve os objetos de domínio, o aplicativo interaja automaticamente com o cache. Além disso, devemos ocultar o código de cache do restante do aplicativo para que possamos removê-lo facilmente, se necessário.
Conforme descrito acima, a solução é uma extensão de um padrão de design existente chamado Domain Objects Persistence Pattern para .NET. Esse padrão já atinge o objetivo de separar objetos de domínio do código de persistência e também do restante do aplicativo. Este duplo desacoplamento proporciona uma grande flexibilidade no projeto. Os objetos de domínio e o restante do aplicativo não são afetados, independentemente de os dados serem provenientes de um banco de dados relacional ou de qualquer outra fonte (por exemplo, XML, arquivos simples ou Active Directory/LDAP).
Portanto, o melhor lugar para incorporar o código de cache é nas classes de persistência. Isso garante que, não importa qual parte do aplicativo emita a chamada load ou save para objetos de domínio, o cache seja referenciado adequadamente primeiro. Isso também oculta todo o código de cache do restante do aplicativo e permite substituí-lo por outra coisa, caso você decida fazê-lo.
Neste exemplo, examinaremos uma classe Employee do banco de dados Northwind mapeada para a tabela "Employees" no banco de dados.
// 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 (); }
}
Abaixo está um exemplo de como um aplicativo cliente usará esse código.
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");
}
}
O código acima mostra a estrutura geral de suas classes para lidar com persistência e armazenamento em cache de objetos de domínio. Como você pode ver, há uma separação clara entre as classes de domínio e persistência. E há uma classe FactoryProvider adicional que permite ocultar a implementação de persistência do restante do aplicativo. No entanto, os objetos de domínio (Employee, neste caso) se movem por todo o aplicativo.
A maioria dos sistemas de cache fornece uma chave baseada em string. Ao mesmo tempo, os dados que você armazena em cache consistem em várias classes diferentes ("Clientes", "Funcionários", "Pedidos", etc.). Nessa situação, um EmployeeId de 1000 pode entrar em conflito com um OrderId de 1000 se suas chaves não contiverem nenhuma informação de tipo. Portanto, você também precisa armazenar algumas informações de tipo como parte da chave. Abaixo estão algumas estruturas-chave sugeridas. Você pode criar o seu próprio com base nos mesmos princípios.
A maioria dos dados transacionais contém operações de linha única (carregar, inserir, atualizar e excluir). Esses métodos são todos baseados em valores de chave primária do objeto e são o local ideal para começar a colocar o código em cache. Veja como lidar com cada método:
Abaixo está um exemplo de método Load com lógica de cache incluída. Lembre-se, você está carregando apenas um único objeto (única linha) do banco de dados.
// 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:1000stringobjKey = CacheUtil.GetObjectKey("Employee", emp.EmployeeId.ToString());
objectobj = 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 elseArrayListdependencyKeys = null;
CacheItemRemovedCallbackonRemoved = null;
CacheUtil.Store(objKey, emp, dependencyKeys, Cache.NoAbsoluteExpiration,
Cache.NoSlidingExpiration, CacheItemPriority.Default, onRemoved );
// Now, load all its related subordinatesLoadSubordinates(emp);
}
else {
emp.Copy((Employee)obj);
}
}
catch(Exception e)
{
// Handle exceptions here
}
}
Observe algumas coisas aqui.
Objetos de domínio geralmente representam dados relacionais provenientes de um banco de dados relacional. Portanto, ao armazená-los em cache, você deve ter em mente seus relacionamentos e armazenar em cache os objetos relacionados também. E você também precisa criar "dependência" entre o objeto e todos os seus objetos relacionados. A razão é que se você remover o objeto do cache, você também deve remover todos os seus objetos relacionados para que não haja problemas de integridade de dados. Abaixo está um exemplo de código de como especificar relacionamentos no 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
}
}
No exemplo acima, você notará que uma coleção está sendo retornada do banco de dados e cada objeto dentro da coleção é armazenado individualmente no cache. Em seguida, a coleção está sendo armazenada em cache como um único item, mas com uma dependência de cache em todos os objetos individuais da coleção. Isso significa que, se algum objeto individual for atualizado ou removido do cache, a coleção será removida automaticamente pelo cache. Isso permite que você mantenha a integridade dos dados em coleções de cache.
Você também notará no exemplo acima que a coleção tem uma dependência de cache no "objeto primário" cujos objetos relacionados a coleção contém. Essa dependência também significa que se o objeto primário for removido ou atualizado no cache, a coleção será removida para manter a integridade dos dados.
Um método de consulta retorna uma coleção de objetos com base nos critérios de pesquisa especificados nele. Pode ou não receber parâmetros de tempo de execução. Em nosso exemplo, temos um FindByTitle que recebe "title" como parâmetro. Abaixo está um exemplo de como o cache é incorporado em um método de consulta.
// 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
}
}
No exemplo acima, assim como no método de relacionamento, você notará que uma coleção está sendo retornada do banco de dados e cada objeto dentro da coleção é armazenado individualmente no cache. Em seguida, a coleção está sendo armazenada em cache como um único item, mas com uma dependência de cache em todos os objetos individuais da coleção. Isso significa que, se algum objeto individual for atualizado ou removido do cache, a coleção será removida automaticamente pelo cache. Isso permite que você mantenha a integridade dos dados em coleções de cache.
O padrão acima funciona para ambientes de implantação de servidor único ou de farm de servidores. A única coisa que deve mudar é a solução de cache subjacente. A maioria das soluções de cache são para ambientes de servidor único (por exemplo, ASP.NET Cache e Caching Application Block). Mas, existem algumas soluções comerciais como Alachisoft NCache (http: // www.alachisoft.com) que fornecem um cache distribuído que funciona em uma configuração de farm de servidores. Dessa forma, seu aplicativo pode usar um cache de qualquer servidor no farm e todas as atualizações de cache são propagadas imediatamente para todo o farm de servidores.
Usando o padrão de armazenamento em cache de objetos de domínio, demonstramos como você deve incorporar o código de armazenamento em cache em suas classes de persistência. Além disso, cobrimos as situações mais usadas de Carga, Consultas e Relacionamentos com relação ao armazenamento em cache. Isso deve fornecer um bom ponto de partida para determinar como você deve usar o cache em seu aplicativo.
Autor: Iqbal Khan trabalha para Alachisoft, uma empresa de software líder que fornece soluções de cache distribuído .NET e Java, mapeamento O/R e otimização de armazenamento do SharePoint. Você pode alcançá-lo em iqbal@alachisoft.com.