Pattern di memorizzazione nella cache degli oggetti di dominio per .NET

Autore: Iqbal Khan

Astratto

La memorizzazione nella cache migliora notevolmente le prestazioni dell'applicazione perché riduce i viaggi costosi al database. Tuttavia, se desideri utilizzare la memorizzazione nella cache nella tua applicazione, devi decidere cosa memorizzare nella cache e dove inserire il codice di memorizzazione nella cache. La risposta è semplice. Memorizza nella cache i tuoi oggetti di dominio e inserisci il codice di memorizzazione nella cache all'interno delle tue classi di persistenza. Gli oggetti di dominio sono fondamentali per qualsiasi applicazione e rappresentano i dati di base e le regole di convalida aziendale. E, mentre gli oggetti di dominio possono conservare alcuni dati di sola lettura, la maggior parte dei dati è transazionale e cambia frequentemente. Pertanto, non puoi semplicemente mantenere gli oggetti di dominio come "variabili globali" per l'intera applicazione perché i dati cambieranno nel database e gli oggetti di dominio diventeranno obsoleti, causando così problemi di integrità dei dati. Dovrai utilizzare una soluzione di memorizzazione nella cache adeguata per questo. E le tue opzioni sono ASP.NET Cache, Caching Application Block in Microsoft Enterprise Library o qualche soluzione commerciale come NCache da Alachisoft . Personalmente, consiglierei di non utilizzare ASP.NET Cache poiché ti costringe a memorizzare nella cache dal livello di presentazione (pagine ASP.NET), il che è negativo. Il posto migliore per incorporare la memorizzazione nella cache nella tua applicazione sono le classi di persistenza degli oggetti di dominio. In questo articolo, sto estendendo un modello di progettazione precedente che ho scritto chiamato Domain Objects Persistence Pattern per .NET. Ti mostrerò come incorporare la memorizzazione nella cache intelligente nella tua applicazione per aumentarne le prestazioni e quali considerazioni dovresti tenere a mente mentre lo fai.

Domain Objects Caching Pattern tenta di fornire una soluzione per la memorizzazione nella cache degli oggetti di dominio. Gli oggetti di dominio in questo modello non sono a conoscenza delle classi che li persistono o se vengono memorizzati nella cache o meno, perché la dipendenza è solo unidirezionale. Ciò rende la progettazione degli oggetti di dominio molto più semplice e facile da capire. Nasconde inoltre la memorizzazione nella cache e il codice di persistenza da altri sottosistemi che utilizzano gli oggetti di dominio. Funziona anche nei sistemi distribuiti in cui vengono passati solo gli oggetti di dominio.

Obbiettivo

Oggetti di dominio, memorizzazione nella cache di oggetti di dominio.

Definizione del problema

Gli oggetti di dominio costituiscono la spina dorsale di qualsiasi applicazione. Acquisiscono il modello di dati dal database e anche le regole aziendali che si applicano a questi dati. È molto tipico per la maggior parte dei sottosistemi di un'applicazione fare affidamento su questi oggetti di dominio comuni. E, di solito, le applicazioni trascorrono la maggior parte del loro tempo a caricare o salvare questi oggetti di dominio nel database. Il "tempo di elaborazione" effettivo di questi oggetti è molto ridotto, specialmente per le applicazioni di livello N in cui ogni "richiesta dell'utente" è molto breve.

Ciò significa che le prestazioni dell'applicazione dipendono molto dalla velocità con cui questi oggetti di dominio possono essere resi disponibili all'applicazione. Se l'applicazione deve effettuare numerosi viaggi nel database, le prestazioni sono generalmente scadenti. Tuttavia, se l'applicazione memorizza nella cache questi oggetti nelle vicinanze, le prestazioni migliorano notevolmente.

Allo stesso tempo, è molto importante che il codice di memorizzazione nella cache degli oggetti di dominio sia mantenuto in una posizione così centrale che, indipendentemente da chi carica o salva gli oggetti di dominio, l'applicazione interagisce automaticamente con la cache. Inoltre, dobbiamo nascondere il codice di memorizzazione nella cache dal resto dell'applicazione in modo da poterlo rimuovere facilmente se necessario.

Soluzione

Come descritto sopra, la soluzione è un'estensione di un modello di progettazione esistente chiamato Domain Objects Persistence Pattern per .NET. Questo modello raggiunge già l'obiettivo di separare gli oggetti di dominio dal codice di persistenza e anche dal resto dell'applicazione. Questo doppio disaccoppiamento offre una grande flessibilità nella progettazione. Gli oggetti di dominio e il resto dell'applicazione sono totalmente inalterati indipendentemente dal fatto che i dati provengano da un database relazionale o da qualsiasi altra fonte (ad esempio XML, file flat o Active Directory/LDAP).

Pertanto, il posto migliore per incorporare il codice di memorizzazione nella cache è nelle classi di persistenza. Ciò garantisce che, indipendentemente da quale parte dell'applicazione emetta la chiamata di caricamento o salvataggio agli oggetti di dominio, la memorizzazione nella cache venga prima opportunamente referenziata. Questo nasconde anche tutto il codice di memorizzazione nella cache dal resto dell'applicazione e ti consente di sostituirlo con qualcos'altro se scegli di farlo.

Classi di dominio e di persistenza

In questo esempio, esamineremo una classe Employee del database Northwind mappata alla tabella "Employees" nel 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 (); }
}

Applicazione di esempio

Di seguito è riportato un esempio di come un'applicazione client utilizzerà questo codice.

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

Il codice sopra mostra la struttura generale delle tue classi per la gestione della persistenza e della memorizzazione nella cache degli oggetti di dominio. Come puoi vedere, c'è una netta separazione tra il dominio e le classi di persistenza. Inoltre, è disponibile una classe FactoryProvider aggiuntiva che consente di nascondere l'implementazione della persistenza dal resto dell'applicazione. Tuttavia, gli oggetti di dominio (in questo caso Dipendente) si spostano all'interno dell'applicazione.

Creazione di chiavi cache

La maggior parte dei sistemi di cache fornisce una chiave basata su stringhe. Allo stesso tempo, i dati memorizzati nella cache sono costituiti da diverse classi ("Clienti", "Dipendenti", "Ordini", ecc.). In questa situazione, un EmployeeId di 1000 potrebbe entrare in conflitto con un OrderId di 1000 se le chiavi non contengono informazioni sul tipo. Pertanto, è necessario memorizzare anche alcune informazioni sul tipo come parte della chiave. Di seguito sono riportate alcune strutture chiave suggerite. Puoi inventare il tuo in base agli stessi principi.

  1. Chiavi per singoli oggetti: Se stai memorizzando solo singoli oggetti, puoi creare le tue chiavi come segue:
    1. "Clienti:PK:1000". Ciò significa che i clienti sono oggetto di una chiave primaria di 1000.
  2. Chiavi per oggetti correlati: Per ogni singolo oggetto, potresti anche voler conservare gli oggetti correlati in modo da poterli trovare facilmente. Ecco le chiavi per questo:
    1. "Clienti:PK:1000:REL:Ordini". Ciò significa una raccolta di Ordini per il Cliente con chiave primaria di 1000
  3. Chiavi per i risultati della query: A volte, esegui query che restituiscono una raccolta di oggetti. Inoltre, queste query possono anche richiedere parametri di runtime diversi ogni volta. Si desidera archiviare questi risultati della query in modo che la prossima volta non sia necessario eseguire la query. Ecco le chiavi per questo. Tieni presente che queste chiavi includono anche i valori dei parametri di runtime:
    1. "Dipendenti:QRY:FindByTitleAndEge:Manager:40". Ciò rappresenta una query nella classe "Employees" denominata "FindByTitleAndAge" che accetta due parametri di runtime. Il primo parametro è "Titolo" e il secondo è "Età". Inoltre, vengono specificati i valori dei parametri di runtime.

Memorizzazione nella cache nelle operazioni transazionali

La maggior parte dei dati transazionali contiene operazioni su riga singola (caricamento, inserimento, aggiornamento ed eliminazione). Questi metodi sono tutti basati sui valori della chiave primaria dell'oggetto e sono il luogo ideale per iniziare a memorizzare il codice nella cache. Ecco come gestire ogni metodo:

  1. Metodo di caricamento: Per prima cosa controlla la cache. Se vengono trovati dati, recuperali da lì. In caso contrario, caricare dal database e quindi inserire nella cache.
  2. Metodo di inserimento: Dopo aver aggiunto correttamente una riga nel database, aggiungi anche il suo oggetto alla cache.
  3. Metodo di aggiornamento: Dopo aver aggiornato correttamente una riga nel database, aggiorna anche il suo oggetto nella cache.
  4. Elimina metodo: Dopo aver rimosso correttamente una riga dal database, rimuovere anche il relativo oggetto dalla cache.

Di seguito è riportato un esempio di metodo di caricamento con logica di memorizzazione nella cache inclusa. Ricorda, stai caricando solo un singolo oggetto (singola riga) dal 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: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
        }
    }

Si prega di notare alcune cose qui.

  1. Richiamata oggetto rimosso: Questo è un delegato che consente all'applicazione di ricevere una notifica in modo asincrono quando l'elemento specificato viene rimosso dalla cache.
  2. Scadenza: È possibile specificare un tempo assoluto o una scadenza del tempo di inattività. Sebbene non abbiamo specificato alcuna scadenza sopra, avresti potuto specificare due tipi di scadenze. Uno è una scadenza a tempo fisso (ad es. tra 10 minuti da adesso) e il secondo è una scadenza di inattività (ad es. se l'articolo è inattivo per 2 minuti).

Relazioni di memorizzazione nella cache

Gli oggetti di dominio di solito rappresentano dati relazionali provenienti da un database relazionale. Pertanto, quando li metti nella cache, devi tenere a mente le loro relazioni e memorizzare nella cache anche gli oggetti correlati. E devi anche creare "dipendenza" tra l'oggetto e tutti i suoi oggetti correlati. Il motivo è che se si rimuove l'oggetto dalla cache, è necessario rimuovere anche tutti i relativi oggetti in modo che non si verifichino problemi di integrità dei dati. Di seguito è riportato un esempio di codice su come specificare le relazioni nella 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 
    }
}

Nell'esempio precedente, noterai che una raccolta viene restituita dal database e ogni oggetto all'interno della raccolta viene archiviato singolarmente nella cache. Quindi, la raccolta viene memorizzata nella cache come un singolo elemento ma con una dipendenza della cache da tutti i singoli oggetti nella raccolta. Ciò significa che se uno qualsiasi dei singoli oggetti viene aggiornato o rimosso nella cache, la raccolta viene automaticamente rimossa dalla cache. Ciò consente di mantenere l'integrità dei dati nelle raccolte di memorizzazione nella cache.

Noterai anche nell'esempio precedente che la raccolta ha una dipendenza della cache dall '"oggetto primario" i cui oggetti correlati contiene la raccolta. Questa dipendenza significa anche che se l'oggetto primario viene rimosso o aggiornato nella cache, la raccolta verrà rimossa per mantenere l'integrità dei dati.

Memorizzazione nella cache nei metodi di query

Un metodo di query restituisce una raccolta di oggetti in base ai criteri di ricerca in esso specificati. Potrebbe o meno richiedere parametri di runtime. Nel nostro esempio, abbiamo un FindByTitle che accetta "titolo" come parametro. Di seguito è riportato un esempio di come la memorizzazione nella cache è incorporata all'interno di un metodo di query.

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

Nell'esempio precedente, proprio come il metodo di relazione, noterai che una raccolta viene restituita dal database e ogni oggetto all'interno della raccolta viene archiviato individualmente nella cache. Quindi, la raccolta viene memorizzata nella cache come un singolo elemento ma con una dipendenza della cache da tutti i singoli oggetti nella raccolta. Ciò significa che se uno qualsiasi dei singoli oggetti viene aggiornato o rimosso nella cache, la raccolta viene automaticamente rimossa dalla cache. Ciò consente di mantenere l'integrità dei dati nelle raccolte di memorizzazione nella cache.

Applicazioni in server farm

Il modello precedente funziona sia per ambienti di distribuzione a server singolo che per server farm. L'unica cosa che deve cambiare è la soluzione di memorizzazione nella cache sottostante. La maggior parte delle soluzioni di memorizzazione nella cache sono per ambienti a server singolo (ad es. ASP.NET Cache e Caching Application Block). Ma ci sono alcune soluzioni commerciali come Alachisoft NCache (http: // www.alachisoft.com) che forniscono una cache distribuita che funziona in una configurazione di server farm. In questo modo, l'applicazione può utilizzare una cache da qualsiasi server della farm e tutti gli aggiornamenti della cache vengono immediatamente propagati all'intera server farm.

Conclusione

Utilizzando il pattern di memorizzazione nella cache degli oggetti di dominio, abbiamo dimostrato come incorporare il codice di memorizzazione nella cache nelle classi di persistenza. Inoltre, abbiamo trattato le situazioni più comunemente utilizzate di carico, query e relazioni rispetto alla memorizzazione nella cache. Questo dovrebbe darti un buon punto di partenza per determinare come utilizzare la memorizzazione nella cache nella tua applicazione.


Autore: Iqbal Khan lavora per Alachisoft, azienda di software leader nella fornitura di soluzioni di caching distribuito .NET e Java, mappatura O/R e ottimizzazione dello storage di SharePoint. Puoi raggiungerlo a iqbal@alachisoft.com.

© Copyright Alachisoft 2002 - . Tutti i diritti riservati. NCache è un marchio registrato di Diyatech Corp.