Padrão de cache de objetos de domínio para .NET

Autor: Iqbal Khan

Sumário

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.

Objetivo

Objetos de Domínio, Cache de Objetos de Domínio.

Definição de problema

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.

Solução

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.

Classes de domínio e persistência

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 (); }
}

Aplicativo de amostra

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.

Criando chaves de cache

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.

  1. Chaves para objetos individuais: Se você estiver armazenando apenas objetos individuais, poderá criar suas chaves da seguinte maneira:
    1. "Clientes:PK:1000". Isso significa que o objeto Customers com chave primária de 1000.
  2. Chaves para objetos relacionados: Para cada objeto individual, você também pode querer manter objetos relacionados para que possa encontrá-los facilmente. Aqui estão as chaves para isso:
    1. "Clientes:PK:1000:REL:Pedidos". Isso significa uma coleção de Pedidos para Cliente com chave primária de 1000
  3. Chaves para resultados da consulta: Às vezes, você executa consultas que retornam uma coleção de objetos. E essas consultas também podem ter parâmetros de tempo de execução diferentes a cada vez. Você deseja armazenar esses resultados da consulta para que da próxima vez não seja necessário executar a consulta. Aqui estão as chaves para isso. Observe que essas chaves também incluem valores de parâmetro de tempo de execução:
    1. "Funcionários:QRY:FindByTitleAndAge:Manager:40". Isso representa uma consulta na classe "Employees" chamada "FindByTitleAndAge" que usa dois parâmetros de tempo de execução. O primeiro parâmetro é "Título" e o segundo é "Idade". E seus valores de parâmetro de tempo de execução são especificados.

Cache em Operações Transacionais

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:

  1. Método de carregamento: Primeiro verifique o cache. Se os dados forem encontrados, obtenha-os de lá. Caso contrário, carregue do banco de dados e coloque no cache.
  2. Método de inserção: Após adicionar com sucesso uma linha no banco de dados, adicione seu objeto ao cache também.
  3. Método de atualização: Após atualizar com sucesso uma linha no banco de dados, atualize seu objeto no cache também.
  4. Excluir método: Após remover com sucesso uma linha do banco de dados, remova seu objeto do cache também.

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.

  1. RemovidoItemCallback: Este é um delegado que permite que seu aplicativo seja notificado de forma assíncrona quando um determinado item for removido do cache.
  2. Expiração: Você pode especificar uma expiração de tempo absoluto ou tempo ocioso. Embora não tenhamos especificado nenhuma expiração acima, você poderia ter especificado dois tipos de expirações. Uma é uma expiração de tempo fixo (por exemplo, daqui a 10 minutos) e a segunda é uma expiração de tempo ocioso (por exemplo, se o item estiver ocioso por 2 minutos).

Relacionamentos de cache

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.

Cache em métodos de consulta

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.

Aplicativos em farms de servidores

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.

Conclusão

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.

© Copyright Alachisoft 2002 - . Todos os direitos reservados. NCache é uma marca registrada da Diyatech Corp.