Patrón de almacenamiento en caché de objetos de dominio para .NET

Autor: Iqbal Kan

Resumen

El almacenamiento en caché mejora en gran medida el rendimiento de la aplicación porque reduce los costosos viajes a la base de datos. Pero, si desea utilizar el almacenamiento en caché en su aplicación, debe decidir qué almacenar en caché y dónde colocar su código de almacenamiento en caché. La respuesta es simple. Guarde en caché los objetos de su dominio y coloque el código de almacenamiento en caché dentro de sus clases de persistencia. Los objetos de dominio son fundamentales para cualquier aplicación y representan sus datos principales y las reglas de validación comercial. Y, aunque los objetos de dominio pueden conservar algunos datos de solo lectura, la mayoría de los datos son transaccionales y cambian con frecuencia. Por lo tanto, no puede simplemente mantener los objetos de dominio como "variables globales" para la totalidad de su aplicación porque los datos cambiarán en la base de datos y sus objetos de dominio se volverán obsoletos, lo que causará problemas de integridad de datos. Tendrá que usar una solución de almacenamiento en caché adecuada para esto. Y sus opciones son ASP.NET Cache, Caching Application Block en Microsoft Enterprise Library o alguna solución comercial como NCache en Alachisoft . Personalmente, desaconsejaría el uso de ASP.NET Cache, ya que lo obliga a almacenar en caché desde la capa de presentación (páginas ASP.NET), lo cual es malo. El mejor lugar para incrustar el almacenamiento en caché en su aplicación son las clases de persistencia de objetos de su dominio. En este artículo, extiendo un patrón de diseño anterior que escribí llamado Patrón de persistencia de objetos de dominio para .NET. Le mostraré cómo puede incorporar el almacenamiento en caché inteligente en su aplicación para aumentar su rendimiento y qué consideraciones debe tener en cuenta al hacerlo.

El patrón de almacenamiento en caché de objetos de dominio intenta proporcionar una solución para el almacenamiento en caché de objetos de dominio. Los objetos de dominio en este patrón desconocen las clases que los conservan o si se almacenan en caché o no, porque la dependencia es solo unidireccional. Esto hace que el diseño del objeto de dominio sea mucho más simple y fácil de entender. También oculta el código de persistencia y almacenamiento en caché de otros subsistemas que utilizan los objetos de dominio. Esto también funciona en sistemas distribuidos donde solo se pasan los objetos de dominio.

Lo que hacemos

Objetos de dominio, almacenamiento en caché de objetos de dominio.

Definición del problema

Los objetos de dominio forman la columna vertebral de cualquier aplicación. Capturan el modelo de datos de la base de datos y también las reglas comerciales que se aplican a estos datos. Es muy típico que la mayoría de los subsistemas de una aplicación se basen en estos objetos de dominio común. Y, por lo general, las aplicaciones pasan la mayor parte de su tiempo cargando o guardando estos objetos de dominio en la base de datos. El "tiempo de procesamiento" real de estos objetos es muy pequeño, especialmente para aplicaciones N-Tier donde cada "solicitud de usuario" es muy breve.

Esto significa que el rendimiento de la aplicación depende en gran medida de la rapidez con que estos objetos de dominio estén disponibles para la aplicación. Si la aplicación tiene que realizar numerosos viajes a la base de datos, el rendimiento suele ser malo. Pero, si la aplicación almacena en caché estos objetos cerca, el rendimiento mejora considerablemente.

Al mismo tiempo, es muy importante que el código de almacenamiento en caché de objetos de dominio se mantenga en un lugar tan central que no importa quién cargue o guarde los objetos de dominio, la aplicación interactúa automáticamente con el caché. Además, debemos ocultar el código de almacenamiento en caché del resto de la aplicación para que podamos sacarlo fácilmente si es necesario.

Solución

Como se describió anteriormente, la solución es una extensión de un patrón de diseño existente denominado Patrón de persistencia de objetos de dominio para .NET. Ese patrón ya logra el objetivo de separar los objetos de dominio del código de persistencia y también del resto de la aplicación. Este doble desacoplamiento proporciona una gran flexibilidad en el diseño. Los objetos de dominio y el resto de la aplicación no se ven afectados en absoluto, ya sea que los datos provengan de una base de datos relacional o de cualquier otra fuente (por ejemplo, XML, archivos planos o Active Directory/LDAP).

Por lo tanto, el mejor lugar para incrustar el código de almacenamiento en caché es en las clases de persistencia. Esto garantiza que, independientemente de la parte de la aplicación que emita la llamada de carga o de guardado a los objetos de dominio, primero se haga la referencia adecuada al almacenamiento en caché. Esto también oculta todo el código de almacenamiento en caché del resto de la aplicación y le permite reemplazarlo con algo más si decide hacerlo.

Clases de dominio y persistencia

En este ejemplo, veremos una clase de empleado de la base de datos Northwind asignada a la tabla "Empleados" en la base de datos.

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

Aplicación de ejemplo

A continuación se muestra un ejemplo de cómo una aplicación cliente utilizará este 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");
        }
    }

El código anterior le muestra la estructura general de sus clases para manejar la persistencia y el almacenamiento en caché de los objetos de dominio. Como puede ver, existe una separación clara entre el dominio y las clases de persistencia. Y hay una clase FactoryProvider adicional que le permite ocultar la implementación de persistencia del resto de la aplicación. Sin embargo, los objetos de dominio (Empleado en este caso) se mueven por toda la aplicación.

Creación de claves de caché

La mayoría de los sistemas de caché le proporcionan una clave basada en cadenas. Al mismo tiempo, los datos que almacena en caché se componen de varias clases diferentes ("Clientes", "Empleados", "Pedidos", etc.). En esta situación, un EmployeeId de 1000 puede entrar en conflicto con un OrderId de 1000 si sus claves no contienen ningún tipo de información. Por lo tanto, también debe almacenar cierta información de tipo como parte de la clave. A continuación se sugieren algunas estructuras clave. Puedes inventarte los tuyos basados ​​en los mismos principios.

  1. Teclas para objetos individuales: Si solo está almacenando objetos individuales, puede crear sus claves de la siguiente manera:
    1. "Clientes:PK:1000". Esto significa que el objeto Clientes tiene una clave principal de 1000.
  2. Claves para objetos relacionados: Para cada objeto individual, es posible que también desee mantener objetos relacionados para que pueda encontrarlos fácilmente. Aquí hay claves para eso:
    1. "Clientes:PK:1000:REL:Pedidos". Esto significa una colección de pedidos para el cliente con clave principal de 1000
  3. Claves para los resultados de la consulta: En algún momento, ejecuta consultas que devuelven una colección de objetos. Y estas consultas también pueden tomar diferentes parámetros de tiempo de ejecución cada vez. Desea almacenar estos resultados de consulta para que la próxima vez no tenga que ejecutar la consulta. Aquí están las claves para eso. Tenga en cuenta que estas claves también incluyen valores de parámetros de tiempo de ejecución:
    1. "Empleados:QRY:FindByTitleAndAge:Gerente:40". Esto representa una consulta en la clase "Empleados" llamada "FindByTitleAndAge" que toma dos parámetros de tiempo de ejecución. El primer parámetro es "Título" y el segundo es "Edad". Y se especifican sus valores de parámetros de tiempo de ejecución.

Almacenamiento en caché en operaciones transaccionales

La mayoría de los datos transaccionales contienen operaciones de una sola fila (cargar, insertar, actualizar y eliminar). Todos estos métodos se basan en valores de clave principal del objeto y son el lugar ideal para comenzar a colocar el código de almacenamiento en caché. Aquí está cómo manejar cada método:

  1. Método de carga: Primero revisa el caché. Si se encuentran datos, obténgalos desde allí. De lo contrario, cárguelo desde la base de datos y luego colóquelo en el caché.
  2. Método de inserción: Después de agregar con éxito una fila en la base de datos, agregue también su objeto al caché.
  3. Método de actualización Después de actualizar con éxito una fila en la base de datos, actualice también su objeto en el caché.
  4. Eliminar método: Después de eliminar con éxito una fila de la base de datos, elimine también su objeto del caché.

A continuación se muestra un método de carga de muestra con lógica de almacenamiento en caché incluida. Recuerde, solo está cargando un solo objeto (una sola fila) de la base de datos.

// 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
        }
    }

Tenga en cuenta algunas cosas aquí.

  1. Elemento eliminadoDevolución de llamada: Este es un delegado que permite que su aplicación sea notificada de forma asincrónica cuando el elemento dado se elimine del caché.
  2. Vencimiento: Puede especificar un tiempo absoluto o una expiración del tiempo de inactividad. Aunque no especificamos ningún vencimiento arriba, podría haber especificado dos tipos de vencimientos. Uno es un vencimiento de tiempo fijo (por ejemplo, dentro de 10 minutos) y el segundo es un vencimiento de tiempo de inactividad (por ejemplo, si el artículo está inactivo durante 2 minutos).

Relaciones de almacenamiento en caché

Los objetos de dominio generalmente representan datos relacionales provenientes de una base de datos relacional. Por lo tanto, cuando los almacene en caché, debe tener en cuenta sus relaciones y también almacenar en caché los objetos relacionados. Y también debe crear una "dependencia" entre el objeto y todos sus objetos relacionados. La razón es que si elimina el objeto del caché, también debe eliminar todos sus objetos relacionados para que no haya problemas de integridad de datos. A continuación se muestra un ejemplo de código de cómo especificar relaciones en la memoria caché.

// 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 
    }
}

En el ejemplo anterior, notará que se devuelve una colección desde la base de datos y cada objeto dentro de la colección se almacena individualmente en el caché. Luego, la colección se almacena en caché como un elemento único pero con una dependencia de caché en todos los objetos individuales de la colección. Esto significa que si alguno de los objetos individuales se actualiza o elimina en la memoria caché, la memoria caché elimina automáticamente la colección. Esto le permite mantener la integridad de los datos en las colecciones de almacenamiento en caché.

También notará en el ejemplo anterior que la colección tiene una dependencia de caché en el "objeto principal" cuyos objetos relacionados contiene la colección. Esta dependencia también significa que si el objeto principal se elimina o actualiza en la memoria caché, la colección se eliminará para mantener la integridad de los datos.

Almacenamiento en caché en métodos de consulta

Un método de consulta devuelve una colección de objetos en función de los criterios de búsqueda especificados en él. Puede o no tomar ningún parámetro de tiempo de ejecución. En nuestro ejemplo, tenemos un FindByTitle que toma "título" como parámetro. A continuación se muestra un ejemplo de cómo se incrusta el almacenamiento en caché dentro de un 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 
    }
}

En el ejemplo anterior, al igual que el método de relación, notará que la base de datos devuelve una colección y cada objeto dentro de la colección se almacena individualmente en la memoria caché. Luego, la colección se almacena en caché como un elemento único pero con una dependencia de caché en todos los objetos individuales de la colección. Esto significa que si alguno de los objetos individuales se actualiza o elimina en la memoria caché, la memoria caché elimina automáticamente la colección. Esto le permite mantener la integridad de los datos en las colecciones de almacenamiento en caché.

Aplicaciones en Granjas de Servidores

El patrón anterior funciona tanto para entornos de implementación de un solo servidor como de granja de servidores. Lo único que debe cambiar es la solución de almacenamiento en caché subyacente. La mayoría de las soluciones de almacenamiento en caché son para entornos de un solo servidor (por ejemplo, ASP.NET Cache y Caching Application Block). Pero, hay algunas soluciones comerciales como Alachisoft NCache (http: // www.alachisoft.com) que le proporcionan una caché distribuida que funciona en una configuración de granja de servidores. De esta manera, su aplicación puede usar un caché de cualquier servidor en la granja y todas las actualizaciones de caché se propagan inmediatamente a toda la granja de servidores.

Conclusión

Usando el patrón de almacenamiento en caché de objetos de dominio, hemos demostrado cómo debe incrustar el código de almacenamiento en caché en sus clases de persistencia. Y hemos cubierto las situaciones más utilizadas de carga, consultas y relaciones con respecto al almacenamiento en caché. Esto debería brindarle un buen punto de partida para determinar cómo debe usar el almacenamiento en caché en su aplicación.


Escrito por: Iqbal Khan trabaja para Alachisoft, una empresa de software líder que ofrece soluciones de almacenamiento en caché distribuido .NET y Java, mapeo O/R y optimización de almacenamiento de SharePoint. Puedes localizarlo en iqbal@alachisoft.com.

© Copyright Alachisoft 2002 - Todos los derechos reservados. NCache es una marca registrada de Diyatech Corp.