Modèle de mise en cache des objets de domaine pour .NET

Auteur : Iqbal Khan

Abstract

La mise en cache améliore considérablement les performances des applications car elle réduit les déplacements coûteux vers la base de données. Mais, si vous souhaitez utiliser la mise en cache dans votre application, vous devez décider quoi mettre en cache et où placer votre code de mise en cache. La réponse est simple. Mettez en cache vos objets de domaine et placez le code de mise en cache dans vos classes de persistance. Les objets de domaine sont au cœur de toute application et représentent ses données de base et ses règles de validation métier. Et, alors que les objets de domaine peuvent conserver certaines données en lecture seule, la plupart des données sont transactionnelles et changent fréquemment. Par conséquent, vous ne pouvez pas simplement conserver les objets de domaine en tant que "variables globales" pour l'intégralité de votre application car les données changeront dans la base de données et vos objets de domaine deviendront obsolètes, ce qui entraînera des problèmes d'intégrité des données. Vous devrez utiliser une solution de mise en cache appropriée pour cela. Et, vos options sont ASP.NET Cache, Caching Application Block dans Microsoft Enterprise Library ou une solution commerciale comme NCache de Alachisoft . Personnellement, je déconseille d'utiliser ASP.NET Cache car cela vous oblige à mettre en cache à partir de la couche de présentation (pages ASP.NET), ce qui est mauvais. Le meilleur endroit pour intégrer la mise en cache dans votre application est vos classes de persistance des objets de domaine. Dans cet article, j'étends un modèle de conception antérieur que j'ai écrit appelé Domain Objects Persistence Pattern for .NET. Je vais vous montrer comment vous pouvez intégrer la mise en cache intelligente dans votre application pour améliorer ses performances et quelles considérations vous devez garder à l'esprit lors de cette opération.

Le modèle de mise en cache des objets de domaine tente de fournir une solution pour la mise en cache des objets de domaine. Les objets de domaine de ce modèle ne connaissent pas les classes qui les conservent ou s'ils sont mis en cache ou non, car la dépendance n'est qu'à sens unique. Cela rend la conception de l'objet de domaine beaucoup plus simple et plus facile à comprendre. Il masque également le code de mise en cache et de persistance des autres sous-systèmes qui utilisent les objets du domaine. Cela fonctionne également dans les systèmes distribués où seuls les objets du domaine sont transmis.

Domaine

Objets de domaine, mise en cache des objets de domaine.

Définition du problème

Les objets de domaine constituent l'épine dorsale de toute application. Ils capturent le modèle de données de la base de données ainsi que les règles métier qui s'appliquent à ces données. Il est très courant que la plupart des sous-systèmes d'une application s'appuient sur ces objets de domaine communs. De plus, les applications passent généralement la majeure partie de leur temps à charger ou à enregistrer ces objets de domaine dans la base de données. Le "temps de traitement" réel de ces objets est très faible, spécialement pour les applications N-Tier où chaque "requête utilisateur" est très courte.

Cela signifie que les performances de l'application dépendent grandement de la rapidité avec laquelle ces objets de domaine peuvent être mis à la disposition de l'application. Si l'application doit effectuer de nombreux déplacements dans la base de données, les performances sont généralement mauvaises. Mais, si l'application met ces objets en cache à proximité, les performances s'améliorent considérablement.

Dans le même temps, il est très important que le code de mise en cache des objets de domaine soit conservé dans un endroit si central que, peu importe qui charge ou enregistre les objets de domaine, l'application interagit automatiquement avec le cache. De plus, nous devons masquer le code de mise en cache du reste de l'application afin de pouvoir le retirer facilement si nécessaire.

Solution

Comme décrit ci-dessus, la solution est une extension d'un modèle de conception existant appelé Domain Objects Persistence Pattern for .NET. Ce modèle atteint déjà l'objectif de séparer les objets de domaine du code de persistance et du reste de l'application également. Ce double découplage offre une grande souplesse de conception. Les objets de domaine et le reste de l'application ne sont absolument pas affectés, que les données proviennent d'une base de données relationnelle ou de toute autre source (par exemple XML, fichiers plats ou Active Directory/LDAP).

Par conséquent, le meilleur endroit pour intégrer le code de mise en cache est dans les classes de persistance. Cela garantit que, quelle que soit la partie de l'application émettant l'appel load ou save vers les objets du domaine, la mise en cache est correctement référencée en premier. Cela masque également tout le code de mise en cache du reste de l'application et vous permet de le remplacer par autre chose si vous le souhaitez.

Classes de domaine et de persistance

Dans cet exemple, nous examinerons une classe Employee de la base de données Northwind mappée à la table "Employees" de la base de données.

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

Exemple d'application

Vous trouverez ci-dessous un exemple de la manière dont une application cliente utilisera ce 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");
        }
    }

Le code ci-dessus vous montre la structure globale de vos classes pour gérer la persistance et la mise en cache des objets de domaine. Comme vous pouvez le constater, il existe une séparation nette entre les classes de domaine et de persistance. De plus, il existe une classe FactoryProvider supplémentaire qui vous permet de masquer l'implémentation de la persistance du reste de l'application. Cependant, les objets de domaine (Employé dans ce cas) se déplacent dans l'application.

Création de clés de cache

La plupart des systèmes de cache vous fournissent une clé basée sur une chaîne. Dans le même temps, les données que vous cachez sont constituées de différentes classes ("Clients", "Employés", "Commandes", etc.). Dans cette situation, un EmployeeId de 1000 peut entrer en conflit avec un OrderId de 1000 si vos clés ne contiennent aucune information de type. Par conséquent, vous devez également stocker certaines informations de type dans le cadre de la clé. Vous trouverez ci-dessous quelques structures clés suggérées. Vous pouvez créer le vôtre sur la base des mêmes principes.

  1. Touches pour les objets individuels : Si vous ne stockez que des objets individuels, vous pouvez créer vos clés comme suit :
    1. "Clients :KP :1000". Cela signifie que les clients ont un objet avec une clé primaire de 1000.
  2. Clés pour les objets associés : Pour chaque objet individuel, vous pouvez également conserver les objets associés afin de pouvoir les retrouver facilement. Voici les clés pour cela :
    1. "Clients:PK:1000:REL:Commandes". Cela signifie une collection de commandes pour le client avec une clé primaire de 1000
  3. Clés pour les résultats de la requête : Parfois, vous exécutez des requêtes qui renvoient une collection d'objets. De plus, ces requêtes peuvent également prendre des paramètres d'exécution différents à chaque fois. Vous souhaitez stocker ces résultats de requête afin que la prochaine fois que vous n'ayez pas à exécuter la requête. Voici les clés pour cela. Veuillez noter que ces clés incluent également les valeurs des paramètres d'exécution :
    1. "Employés:REQ:RechercherParTitreEtAge:Manager:40". Cela représente une requête dans la classe "Employés" appelée "FindByTitleAndAge" qui prend deux paramètres d'exécution. Le premier paramètre est "Titre" et le second est "Âge". Et, leurs valeurs de paramètres d'exécution sont spécifiées.

Mise en cache dans les opérations transactionnelles

La plupart des données transactionnelles contiennent des opérations sur une seule ligne (charger, insérer, mettre à jour et supprimer). Ces méthodes sont toutes basées sur les valeurs de clé primaire de l'objet et sont l'endroit idéal pour commencer à mettre du code en cache. Voici comment gérer chaque méthode :

  1. Méthode de chargement : Vérifiez d'abord le cache. Si des données sont trouvées, récupérez-les à partir de là. Sinon, chargez depuis la base de données puis mettez-le dans le cache.
  2. Méthode d'insertion: Après avoir ajouté avec succès une ligne dans la base de données, ajoutez également son objet au cache.
  3. Méthode de mise à jour: Après avoir mis à jour avec succès une ligne dans la base de données, mettez également à jour son objet dans le cache.
  4. Méthode de suppression: Après avoir réussi à supprimer une ligne de la base de données, supprimez également son objet du cache.

Vous trouverez ci-dessous un exemple de méthode Load avec une logique de mise en cache incluse. N'oubliez pas que vous ne chargez qu'un seul objet (une seule ligne) à partir de la base de données.

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

S'il vous plaît noter quelques choses ici.

  1. Rappel de l'élément supprimé : Il s'agit d'un délégué qui permet à votre application d'être avertie de manière asynchrone lorsque l'élément donné est supprimé du cache.
  2. Expiration: Vous pouvez spécifier une durée absolue ou une expiration du temps d'inactivité. Bien que nous n'ayons pas spécifié d'expiration ci-dessus, vous auriez pu spécifier deux types d'expiration. L'un est une expiration à durée fixe (par exemple, dans 10 minutes) et le second est une expiration de temps d'inactivité (par exemple, si l'élément est inactif pendant 2 minutes).

Mise en cache des relations

Les objets de domaine représentent généralement des données relationnelles provenant d'une base de données relationnelle. Par conséquent, lorsque vous les mettez en cache, vous devez garder à l'esprit leurs relations et mettre également en cache les objets associés. Et, vous devez également créer une "dépendance" entre l'objet et tous ses objets associés. La raison en est que si vous supprimez l'objet du cache, vous devez également supprimer tous ses objets associés afin qu'il n'y ait pas de problèmes d'intégrité des données. Vous trouverez ci-dessous un exemple de code indiquant comment spécifier des relations dans le 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 
    }
}

Dans l'exemple ci-dessus, vous remarquerez qu'une collection est renvoyée de la base de données et que chaque objet de la collection est stocké individuellement dans le cache. Ensuite, la collection est mise en cache en tant qu'élément unique mais avec une dépendance de cache sur tous les objets individuels de la collection. Cela signifie que si l'un des objets individuels est mis à jour ou supprimé dans le cache, la collection est automatiquement supprimée par le cache. Cela vous permet de maintenir l'intégrité des données dans les collections de mise en cache.

Vous remarquerez également dans l'exemple ci-dessus que la collection a une dépendance de cache sur "l'objet principal" dont les objets associés la collection contient. Cette dépendance signifie également que si l'objet principal est supprimé ou mis à jour dans le cache, la collection sera supprimée afin de maintenir l'intégrité des données.

Mise en cache dans les méthodes de requête

Une méthode de requête renvoie une collection d'objets en fonction des critères de recherche qui y sont spécifiés. Il peut ou non prendre des paramètres d'exécution. Dans notre exemple, nous avons un FindByTitle qui prend "title" comme paramètre. Vous trouverez ci-dessous un exemple de la façon dont la mise en cache est intégrée dans une méthode de requête.

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

Dans l'exemple ci-dessus, tout comme la méthode de relation, vous remarquerez qu'une collection est renvoyée de la base de données et que chaque objet à l'intérieur de la collection est stocké individuellement dans le cache. Ensuite, la collection est mise en cache en tant qu'élément unique mais avec une dépendance de cache sur tous les objets individuels de la collection. Cela signifie que si l'un des objets individuels est mis à jour ou supprimé dans le cache, la collection est automatiquement supprimée par le cache. Cela vous permet de maintenir l'intégrité des données dans les collections de mise en cache.

Applications dans les fermes de serveurs

Le modèle ci-dessus fonctionne pour les environnements de déploiement à serveur unique ou à batterie de serveurs. La seule chose qui doit changer est la solution de mise en cache sous-jacente. La plupart des solutions de mise en cache sont destinées aux environnements à serveur unique (par exemple, ASP.NET Cache et Caching Application Block). Mais, il existe des solutions commerciales comme Alachisoft NCache (http: // www.alachisoft.com) qui vous fournissent un cache distribué qui fonctionne dans une configuration de batterie de serveurs. De cette façon, votre application peut utiliser un cache à partir de n'importe quel serveur de la batterie et toutes les mises à jour du cache sont immédiatement propagées à l'ensemble de la batterie de serveurs.

Conclusion

À l'aide du modèle de mise en cache des objets de domaine, nous avons montré comment intégrer le code de mise en cache dans vos classes de persistance. Et, nous avons couvert les situations les plus couramment utilisées de chargement, de requêtes et de relations en ce qui concerne la mise en cache. Cela devrait vous donner un bon point de départ pour déterminer comment utiliser la mise en cache dans votre application.


Auteur : Iqbal Khan travaille pour Alachisoft, un éditeur de logiciels de premier plan fournissant des solutions de mise en cache distribuée .NET et Java, de mappage O/R et d'optimisation du stockage SharePoint. Vous pouvez le joindre au iqbal@alachisoft.com.

© Copyright Alachisoft 2002 - . Tous droits réservés. NCache est une marque déposée de Diyatech Corp.