|
Mapping objet-relationnel, Couches d’accès aux données et |
||
Introduction
.............................................................................
Mapping Objet-Relationnel
......................................................
Architecture
Multi-niveaux ..................................................
Séparation
des couches ..................................................
Exemple
d'implémentation ................................................
Correspondances
entre modèles objets et relationnels ...........
Modélisation
UML ..........................................................
Description
des transformations .....................................
Objectifs
d’une couche d’accès aux données ..............................
Data Access Object
(DAO) ..................................................
Présentation
.................................................................
Exemple
d’implémentation ............................................
Limitations
......................................................................
Frameworks de
persistance ....................................................
Fonctionnalités
attendues dans un Framework de persistance ..
Interfaces
de programmation intuitives ...........................
Mapping
objet relationnel ..................................................
Mapping
avancé et comportement .......................................
Lazy
vs. Aggresive Loading ..............................................
Gestion
de différentes bases de données .........................
Concurrence
...................................................................
Transactions
.....................................................................
Gestion
des types de données évolués .................................
Encryptage
des données ....................................................
Cache
objet
......................................................................
Avantages
des Framework de persistance .............................
Conclusion
................................................................................
es premières questions que tous les programmeurs et chefs de projets se posent avant de commencer à développer une
application sont celles concernant le choix de la plateforme de développement, et celui du serveur de bases de données.
Que la réponse soit J2EE/Oracle, .Net/SQL Server, ASP/Access, PHP/MySQL, ou même un autre couple parmi ces possibilités, la tendance actuelle veut que le développement soit fait dans un langage objet et utilise une base de données relationnelle.
Les difficultés de cohabitation entre les mondes objets et relationnels sont résolues grâce au concept de
Mapping objet-relationnel (O/R Mapping), qui est le nom donné aux techniques de transformation des modèles objets en modèles relationnels.
Ce document a pour objectif de vous présenter dans un premier temps les concepts de mapping objet-relationnel, et surtout de vous convaincre
de l'utilité d'une couche logicielle objet pour accéder à vos données. Nous verrons ensuite comment cette couche peut être générée à partir d’un modèle objet, tout en exploitant une base de données relationnelle, quelque soit cette dernière.
Les choix architecturaux d’une application sont décisifs dès lors qu’ils interviennent sur les performances, l’évolutivité, les temps de développement, et bien sûr les coûts. Les sages d’aujourd’hui prônent une séparation en différentes couches des applications, et parlent alors d’applications multi niveaux (n-tier applications).

Ces couches devant être indépendantes, dès que les spécifications fonctionnelles de chacune d’entre elles seront établies, leur développement pourra se faire en parallèle, et la maintenance de l’application n’en sera que plus
aisée.
La couche d’accès aux données (Data Access Layer ou DAL), doit prendre en charge toutes les interactions entre l’application et la base de données. Si une telle couche n’existait pas, on serait
contraint de réécrire les mêmes lignes de code à différents endroits de l’application, par exemple pour lire tous les clients présents dans la base de données.
Au minimum, cette couche se doit de prendre en compte l’ensemble des interactions possibles avec la base de données, c’est-à-dire
la création, la lecteur, la modification ou la suppression des enregistrements dans chacune des tables de la base de données.
Il est commun de créer une classe pour chaque table existante, et de
créer des méthodes possédant autant de paramètres que la table possède de champs.
Considérons une base de données contenant une table Client.

Une telle table pourrait avoir une classe équivalente dans la couche d’accès aux données :

Lorsqu’une telle classe comporte les méthodes nécessaires à la création, la lecture, la modification, et la suppression d’entités de bases de données, on dit qu’elle implémente une interface de type CRUD (Create, Read, Update, Delete). Les avantages et les inconvénients d’une telle classe, plus connue sous le nom de DAO (Data Access Object), seront abordés plus loin.
Voici, en pratique, comment exploiter une base de données sans couche d’accès :
SqlConnection myConnection =
new sqlConnection(StrConn);
myDataRow = myDataSet.Tables["Customers"].Rows[0];
myDataRow["Age"] = 56;
myDataRow["Nom"] = "Henri";
myDataRow["Prenom"] = "Michel";
mySqlDataAdapter.Update(myDataSet, "Customers");
Voici ce que nous pourrions écrire si nous implémentions une couche d’accès aux données :
Client monClient =
new Client();Le programmeur utilisant la couche d’accès aux données n’a plus besoin de connaître le langage SQL pour pouvoir manipuler des données, il lui suffit d’utiliser la méthode adéquate. Ainsi, la couche métier est complètement découplée de la base de données et peut être implémentée pour différents serveurs de bases de données.
On vient de voir qu’une table pourrait facilement être encapsulée par une classe dans notre couche d’accès aux données. En fait, cela n’est rien d’autre que du mapping objet relationnel, et toutes les entités du modèle relationnel ont leur équivalent dans le monde objet, et inversement.
Pour définir l’ensemble de ces correspondances, nous allons nous baser sur le langage
UML (Unified Modeling Language), et plus particulièrement sur le diagramme de classes, qui est le diagramme statique le plus utilisé de la modélisation UML. Un diagramme de classes est un schéma représentant toutes les classes d’un programme, leurs attributs, leurs méthodes, ainsi que les relations entre ces classes.
Etant donné qu’un modèle UML est plus riche qu’un schéma de base de données, je vais décrire les transformations des différentes entités UML en entités relationnelles de bases de données.
L’entité principale du monde objet est la « classe ». Celle-ci, comme nous avons pu le voir, sera transformée en une « table », à laquelle on pourra donner le même nom que la classe. De la même manière qu’une classe est composée de plusieurs « attributs », une table est composée de « champs », tous typés.

Ainsi, une « instance » d’une classe particulière, correspondra à un « enregistrement » de la table correspondante.
Vous pouvez remarquer que la table Customer dans la figure précédente
contient un champ qui n’a aucun équivalent dans le modèle objet. Il s’agit de la
clé primaire : ID. Ce champ est de type GUID (General Unique Identifier), et représente de manière unique un enregistrement dans une table. Si par le passé vous définissiez vos clés primaires comme un ensemble de champs métiers de chaque table, c’était dans l’optique d’insérer une contrainte d’intégrité, et par exemple de confier au serveur de base de données la tâche de vérifier que deux enregistrements représentant la même entité logique ne puissent être insérés. Cette règle est typiquement une règle métier qui pourra être incluse dans la couche d’accès aux données, renvoyant une exception caractéristique par exemple. Dans l’optique de l’utilisation d’une couche d’accès aux données, les clés doivent être uniques,
et n’avoir aucun sens vis-à-vis du métier qui est modélisé. Dans cette optique, les
GUID sont la solution idéale.
Intéressons nous maintenant à un concept propre au modèle objet, l’héritage. Il existe plusieurs manières de représenter l’héritage dans une base de données, chacune étant plus ou moins efficace quant à la simplicité d’exploitation et vis-à-vis des performances de la couche d’accès. La méthode que je vais vous présenter permet d’obtenir de très bonnes performances. Il s’agit de modéliser toute une hiérarchie de classes dans une même table, chaque classe ajoutant ses attributs propres comme de nouveaux champs. Il nous suffit alors d’ajouter un champ contenant le type de l’instance pour pouvoir charger les champs correspondants.

Voici la table une fois remplie avec deux instances de la classe Circle, et une de la classe « Square ».

Remarquons que le champ « Size » a pour valeur NULL si l’enregistrement correspond à une instance de la classe Circle.
Etudions maintenant comment modéliser les relations entre les classes. En UML, il est possible de spécifier la cardinalité des relations. Voici des exemples de relations avec des cardinalités différentes :

La relation la plus simple est la relation 1-1, comme entre les classes Customer et Address de l’exemple ci-dessus. Cela signifie qu’un objet de type Customer ne peut posséder qu’une seule instance de type Address, et inversement qu’un objet de type Address ne peut être possédé que par un seul objet de type Customer.
Pour effectuer la correspondance en base de données, il suffit d’ajouter à la table Customer, un champ de type GUID, qui contiendra la valeur du champ ID d’un enregistrement de la table Address. Il s’agit donc d’une clé étrangère.
Voici le résultat sur la table « Customer » :

La relation entre Customer et Order est une relation 1-*, c'est-à-dire qu’une instance de la classe Customer pourra posséder une collection d’objets de type Order, et que chaque objet Order ne peut être possédé que par un seul objet Customer.
Tout comme pour la relation 1-1, il nous faut ajouter une clé étrangère. Mais cette fois, ce n’est plus la table Customer qui va référencer les enregistrements de la table Order, mais l’inverse. La table Order possède donc autant de champs que la classe Order possède d’attributs plus un champ ID et la clé étrangère pour référencer l’objet de type Customer qui le possède.
Voici le résultat sur la table « Order » :
Le dernier type de relation qui nous reste à étudier est la relation *-* entre les classes Order et Product. Cette relation représente le fait qu’un objet Order peut posséder des références vers plusieurs objets Product, et que chaque objet Product peut être référencé par plusieurs objets Order.
Dans un schéma de base de données, une telle structure nécessite une table supplémentaire contenant l’ensemble des relations entre chaque entité. Une telle table est appelée « table d’index ».
Cette table d’index est composée d’une clé primaire et de deux clés étrangères, qui référencent les deux objets en relation.

Voici le schéma de base de données correspondant au modèle UML définit au début du document.

Vous connaissez maintenant les règles fondamentales
du mapping objet relationnel, même si le spectre des entités UML na pas
été présenté. Je pense par exemple aux interfaces, aux énumération,
aux classes abstraites, aux singletons, à l’agrégation ou la
composition, …
Si vous voulez en savoir un peu plus à ce sujet, vous pouvez toujours consulter les articles disponibles sur le site de Scott Ambler ici : http://www.ambysoft.com/.
Notez aussi que la plupart des outils de modélisation UML permettent en général de générer un script SQL correspondant au modèle dessiné.
A ce niveau du document, vous devriez avoir compris que l’on peut modéliser n’importe quel système de données avec un diagramme de classes UML. Ce modèle sera d’autant plus utile qu’il reflètera à 90% l’ensemble des classes que vous allez devoir manipuler lors de l’écriture de votre application. Ainsi il sera très facilement compréhensible par toute l’équipe de développement. Vous disposerez de plus de souplesse de modélisation grâce aux différentes entités UML et relations entre les entités plus explicites.
Nous allons maintenant étudier les solutions qui se présentent à nous pour créer une couche d’accès aux données, et quelles sont leurs limitations.
Quel que soit le système sur lequel vous développez, vous pourrez toujours accéder à vos données relationnelles. Vous utiliserez alors des API telles que JDBC, ADO, ADO.Net, et pourrez alors exécuter des requêtes SQL, ou obtenir des objets représentant des tables et leurs champs, …
Ces outils sont suffisants mais ne permettrons jamais à eux seuls une vraie abstraction
de la base de données sous-jacente. Par exemple vous ne pourrez pas obtenir toutes les commandes d’un client donné avec une seule ligne de code. De plus, chaque fois que vous voudrez supprimer un client, vous devrez supprimer manuellement son adresse, alors que cela pourrait être fait automatiquement.
Tous ces problèmes doivent être gérés par une couche d’accès aux données chargée de communiquer avec le serveur de bases de données, et de renvoyer des objets métiers au programmeur ; Ce denier n’aurait alors plus besoin de taper des requêtes SQL, comprendre les relations entre les tables, ou connaître tous les paramètres des procédures stockées, ou encore d’autres éléments.
Lorsqu'on parle de séparation des couches par responsabilité, il y a va
également de la séparation des compétences de chaque
développeurs.
C’est à cela que servent les DAO. Ils sont créés pour simplifier au maximum le travail du programmeur avec le système de données en encapsulant les accès à la base via les frameworks tels que ADO.Net. Le programmeurs utilisent par exemple une méthode Save() pour sauvegarder une entité. Cette méthode créera une requête SQL d’insertion ou de mise à jour si nécessaire. On peut
également imaginer que les DAO nous permettent d’accéder à des entités qui sont en relation, effectuant les jointures en base de données automatiquement.
Voici un aperçu d’une classe qui implémente en C# une couche d’accès aux données pour une base de données SQL Server, générée à partir d’une table mappée sur le modèle suivant :
Voici la table résultant du mapping objet relationnel pour ce modèle :
Méthode Update() de la classe :
private
int Update()Voici un exemple d’accesseur à implémenter :
public
int XOn voit bien que l’on en arrive très rapidement à une masse énorme de lignes de codes à écrire, et ceci pour chaque table de la base de données, sans pour autant que l’on ai implémenté des fonctionnalités particulières. Heureusement, beaucoup d’outils existent pour générer l’ensemble de ce code à notre place.
Les DAO n’ont pas pour objectif de gérer des concepts plus complexes tels que les transactions, la concurrence ou encore le cache. De ce fait, la réalisation de la couche métier restera quand même plus ou moins complexe à réaliser suivant le modèle de base de données. Nous verrons plus tard comment remédier à ce problème.
Vis-à-vis du programmeur des classes métiers, le découplage avec la base de données est réalisé. Cependant, d’autres programmeurs devront se charger du codage de l’ensemble de ces classes, et la moindre modification de la base de données obligera à revoir l’implémentation des DAO correspondants.
Même s’il existe aujourd’hui de nombreux générateurs de classes DAO qui utilisent des bases de données existantes, leur utilisation pour des projets volumineux s’avère limitée, car nécessitant des fonctionnalités plus avancées.
Les frameworks de persistance sont plus que de simples wrappers de
tables proposant l'accès aux champs de celles-ci. Il s’agit en général d’outils permettant de modéliser un système de données, en UML ou en XML par exemple, et de générer tous les DAO ainsi que les objets métiers permettant d’exploiter au maximum un système de données.
De plus, ils prennent en charge des concepts avancés comme le cache des objets, la concurrence, les transactions distribuées, le chargement des données via des requêtes objets, et peuvent
également offrir une totale indépendance vis-à-vis du serveur de bases de données.
Ils créent des systèmes complètement autonomes de gestion du système d’information, autorisent un fort découplage avec la base de données, améliorent les performances et apportent une grande simplicité d’utilisation.
Le seul inconvénient est qu’il s’agit souvent de solutions commerciales plus ou moins coûteuses suivant les
fonctionnalités ou la plateforme d’utilisation.
Il faut avant tout que la couche d’accès soit très facilement manipulable, et que le programmeur puisse exploiter un système de données de façon très intuitive.
Concrètement, il faut que des méthodes CRUD soient implémentées, si possibles des patterns
Factory permettant de charger des objets de différentes manières. Il faut aussi que chaque attribut ou relation du modèle de classe soit interprété en tant qu’accesseurs sur les champs et les entités correspondants, chargeant de manière transparente les entités en question.
Prenons l’exemple simple de deux classes avec une relation 1-*.
Un exemple simple de code parfaitement exploitable à partir de ce modèle est le suivant :
// Chargement de tous les clients en base
Cet exemple lit tous les clients présents en base de données, les parcours et affiche toutes les commandes qui lui appartiennent.
Voici un autre exemple permettant de charger tous les clients ayant un nom donné, et de leur ajouter une nouvelle commande :
string
OPathQuery = "Customer[FirstName = ‘Eric’]";
La plupart du temps, ce type de fonctionnalité est encapsulé dans la couche de services, via l’utilisation de méthodes telles que
GetCustomerByOrderID(), qui doivent être codées indépendamment de la couche d’accès aux données, et qui obligent le programmeur à connaître toutes les possibilités de chargement de chaque entité, suivant les entités en relation.
Ces deux exemples permettent de voir la simplicité d’exploitation d’un système de données lorsque l’on utilise un
Framework de persistance, et la facilité de navigation à travers les données, pour récupérer de façon intuitive, uniquement en regardant le modèle objet, l’ensemble des données
correspondantes.
On peut aussi très bien imaginer que d’autres fonctionnalités très utiles comme les tris ou des méthodes pour faire du « paging » (chargement d’une partie seulement des résultats), soient implémentées.
Il doit être possible de modéliser une couche de données avec un modèle entièrement objet, gérant les concepts d’héritage, de relations complexes, et autres. De manière générale, on pourra par exemple utiliser un éditeur UML du marché, ou créer un fichier XML représentant ce modèle.
Cette méthodologie permet une plus grande souplesse de modification du modèle et de la base de
données, les modifications du modèle étant répercutées automatiquement sur la base de
données de façon transparente pour le programmeur.
De plus, le code généré doit refléter à l’identique l’architecture modélisée. Chaque classe du modèle trouvera son équivalent dans la couche générée, grâce à une classe métier persistante, et si une classe hérite d’une autre, les objets de la couche d’accès devront eux-mêmes hériter l’un de l’autre.
Exemple :
Pour ce modèle, on peut imaginer pouvoir taper ceci :
Dessin grandDessin =
new Dessin(10, 15) ;Remarquez la possibilité d’exploiter l’héritage qui a été modélisé, à l’intérieur même de la couche d’accès générée.
La base de données, quant à elle, sera générée avec les règles de mapping énoncées plus haut, et comportera alors ces enregistrements :

Vous pourrez notez que la base de données a bien pris en compte le fait que l’objet petitDessin contient une forme Cercle, et qu’il est lui-même une des formes de l’objet grandDessin.
Lors de la modélisation objet d’un système de données,
il arrive parfois qu'on soit contraint d'utiliser des concepts n'existant pas
obligatoirement au niveau relationnel car la modélisation objet est plus riche qu’une modélisation relationnelle.
Le stéréotype « singleton » en fait partie. Le singleton est un pattern permettant de ne travailler qu’avec une seule instance d’une classe tout au long de l’utilisation d’un programme. Cette classe pourra par exemple contenir les paramètres spécifiques à l’application, comme une couleur de fond, un mot de passe administrateur, ou toute autre valeur générale au contexte.
En base de données, on devra créer une table avec autant de champs que cette classe contient d’attributs, mais sa spécificité tiendra dans le fait qu’elle ne contiendra qu’un seul et unique champ. Ainsi la classe correspondante dans la couche d’accès ne possèdera qu’une méthode
GetObject(), qui renverra l’instance correspondant au seul enregistrement de la table. Le comportement sera
donc celui d’un fichier de configuration, global à l’application.
// Incrémentation du nombre de visiteurs du site
De la même manière, une classe d’association devra aussi être traitée de façon à ce que l’on puisse accéder à chacune des entités, dès lors que l’on aura identifié les deux autres. Pour cela, la couche doit posséder des méthodes particulières pour le chargement de ce type de données.
Exemple d’utilisation d’une classe d’association :
Entreprise corpCie =
new Entreprise() ;Tout ceci nous permet de générer des comportements différents, via des méthodes spécifiques aux types d’entités modélisées.
Lors de la lecture de l’attribut d’une classe de notre couche d’accès aux données, nous avons le choix entre charger tous les attributs en même temps, charger seulement l’attribut en question, ou charger tous les enregistrements ainsi que ceux en relation avec celui-ci. Ceci peut être réalisé via la requête qui est générée, et qui pourra contenir une ou plusieurs clauses SELECT de sorte à charger différentes table en un
seul accès à la base.
On appelle « Lazy Loading » la technique qui consiste à ne charger que les éléments correspondants à la demande spécifique du programmeur. A l’inverse, on parlera d’ « aggressive loading » si la couche d’accès aux données charge aussi tous les enregistrements qui ont une relation avec celui demandé, provenant de tables différentes.
On comprendra facilement que le lazy loading (cf. figure
ci-dessous) permet d’optimiser le code, et ne surcharge pas le serveur de bases de données inutilement. Mais il faut bien comprendre qu’il peut arriver que cette technique
crée par moment plus de requêtes au serveur que nécessaire. Elle reste tout de même la technique la plus utilisée, car en général plus rapide, et moins gourmande en ressources que
l’aggressive loading.
Lazy
Loading
L’abstraction vis-à-vis de la base de données est très importante pour le programmeur. Mais l’abstraction vis-à-vis du serveur de bases de données est peut être encore plus importante.
En théorie, une couche d’accès aux données peut être construite pour être indépendante du serveur de bases de données. En pratique il existe deux méthodologies possibles : utiliser un langage de communication standard (SQL), ou exploiter une API telle que ADO.Net et les différents fournisseurs de données. On instancie alors les fournisseurs adéquats au moment voulu, en utilisant par exemple le pattern
abstract factory.
Par expérience, la première possibilité est à écartée puisque tous les serveurs de bases de données adoptent leurs propres spécificités SQL, ou utilisent des types de données particuliers. Aussi, même si cette technique fonctionnait, on ne pourrait pas utiliser le serveur de bases de données de façon optimale, puisque des concessions devraient être faites pour permettre une meilleure portabilité du code SQL.
La deuxième possibilité est réalisable, en théorie. Mais voilà, par expérience je peux vous assurer qu’il est extrêmement compliqué (en fait impossible) de la mettre en pratique, encore une fois à cause des types de données. Seulement cette fois-ci cela est dû à l’implémentation même de
l'API ADO.Net.
Heureusement, il existe une solution. Il suffit de se rappeler que la couche d’accès aux données est une « couche », et oui ! Ce principe nous permet de l’ôter, et de la remplacer par une autre. La seule condition est que chacune des couches implémente les mêmes services. On peut alors imaginer créer une couche dédiée à chaque serveur de bases de données. Celle-ci implémentera les caractéristiques de chacun d’entre eux, tout en gardant une certaine cohérence au niveau de l’application grâce à la spécification préalable des services implémentés.
L’implémentation du PetShop DNG repose sur ce principe, et permet même de mettre en place différents types de couches d’accès aux données, grâce à une couche de services intermédiaire faisant le lien entre la logique métier et la couche d’accès aux données.
La gestion de la concurrence est une fonctionnalité primordiale pour une application client serveur, à partir de laquelle plusieurs utilisateurs peuvent modifier les mêmes objets simultanément.
Il existe principalement deux manières de gérer la concurrence : la manière dite « pessimiste », et la manière « optimiste ».
La manière pessimiste est basée sur le principe que deux utilisateurs vont quoi qu’il advienne tenter de modifier le même objet au même moment. Pour éviter que cela n’arrive, la première personne tentant d’accéder à l’objet va le verrouiller, pour l’ouvrir en mode exclusif. En pratique, cela peut être fait en ajoutant un champ dans la table, pour signaler que l’enregistrement est verrouillé. Cette méthode radicale a le désavantage
d'être coûteuse en termes de performances, car elle verrouille l’accès à l’objet tant qu’il n’a pas été complètement libéré, même si aucune modification n’est effective.
La manière optimiste, quant à elle, autorise plusieurs utilisateurs à ouvrir en écriture le même objet. Ce n’est qu’au moment de sa sauvegarde que la couche d’accès vérifiera si l’objet a été modifié entre temps. Le seul désavantage est qu’il faut
obligatoirement conserver une copie des valeurs avant chaque modifications. Cette fonctionnalité est permise avec ADO.Net, via les objets
DataRow modélisant les enregistrements d’une table. Cela permet au programmeur de revenir aux valeurs
précédentes avant toute modification et charge aux objets
DataAdapter de les prendre en compte pour générer des requêtes SQL utilisant les valeurs initiales de l’enregistrement. Si la couche de données détecte que quelqu’un a modifié l’enregistrement entre la lecture et la demande d’écriture, une exception est levée, et le programme peut en avertir l’utilisateur.
Une bonne couche d’accès aux données doit rendre ce fonctionnement transparent pour le programmeur, et lever automatiquement une exception typée lui permettant d’effectuer les traitements adéquats.
Pour gérer l’atomicité de plusieurs modifications sur la base de données au sein d’une même méthode, comme au travers de plusieurs, la couche d’accès doit implémenter des services permettant de définir le début et la fin d’une transaction, et proposer un mécanisme d’interception des erreurs pour annuler cette dernière.
Exemple :
DataBase.GetInstance().BeginTransaction();
Il existe principalement deux technique en C# pour effectuer ces opérations : utiliser les fonctionnalités transactionnelles du serveur de bases de données, ou hériter des classes de l’espace de nom
System.EnterpriseServices, et laisser MTS se charger de ça. La deuxième solution à beau offrir plus de fonctionnalités, elle n’en reste pas moins plus complexe à implémenter dans une couche d’accès puisqu’elle oblige toutes nos classes à hériter de classes internes de la FCL (Foundation Class Library), limitant ainsi notre flexibilité.
La notion de transaction est primordiale, et sera utilisée principalement par la couche de service qui devra s’occuper de l’intégrité des cas d’utilisation de l’application.
La gestion des types de données est primordiale lors de l’adaptation du modèle objet en modèle relationnel, car chacun des systèmes implémente des types différents. Par exemple, le type csharp System.Drawing.Image n’a pas d’équivalent dans tous les serveurs de bases de données, et il faut alors effectuer des adaptations sur le type de données, et alors transformer ces objets en tableaux d’octets (byte []) pour pouvoir les enregistrer dans des champs de type BLOB (Oracle) ou Image (MS SQL). Cette question revient souvent sur les forums de discussion, et il existe différentes alternatives en fonction de chaque langage et serveur de bases de données.
Vous comprendrez que ce traitement puisse être fait sans mal par notre couche d’accès aux données. Cette technique est appelée « marshalling / unmarshalling ». Voici un exemple de transformation d’une image, grâce aux méthodes de sérialisation binaires fournies avec la FCL .Net.
Marshalling :
System.Drawing.Bitmap myPicture;
BinaryFormatter bf =
Unmarshalling :
System.Drawing.Bitmap myPicture;
BinaryFormatter bf = new BinaryFormatter();
MemoryStream ms = new MemoryStream();
ms.Write((byte[])mDataRow["Picture"],
0, ((byte[])mDataRow["Picture"]).Length);
ms.Position = 0;
myPicture = bf.Deserialize(ms) as
System.Drawing.Bitmap;
ms.Close();
Cette technique s’avère très portable pour n’importe quel type de donnée, et peut être étendue pour gérer le type Object, et ainsi permettre toutes les sérialisations possibles dans une base de données relationnelle. Le seul inconvénient réside dans le format binaire de l'objet qui s'avère très difficile à modifier manuellement en opérant directement sur les enregistrements de la base de données. Par ailleurs, cela rend très difficile la compréhension d'un tel champ.
Qui n’a jamais créé de base de données contenant des couples login/password ? Par contre, combien ne prennent pas la peine d’encrypter ces données dans la base de données, de sorte que même un DBA (Database Administrator) ne puisse récupérer ces mots de passe et puisse se faire passer pour un utilisateur du système ?
Il existe des techniques d’encryptage reconnues pour ce type d’utilisation, comme pour les autres types de données tels que les numéros de comptes en banque par exemple.
Pour les mots de passes, on utilise souvent une technique dite du one-way encryption, qui permet d’encrypter une donnée, mais pas de la décrypter. Ainsi pour vérifier si un mot de passe fourni est correct, on devra l’encrypter et comparer cette valeur avec le mot de passe original qui est aussi stocké en encrypté dans la base de données. Le nom « one-way » vient du fait qu’il n’existe pas d’algorithme de décryptage. L’algorithme le plus connu est le MD5.
Concernant les autres types de données pour lesquels on a besoin de pouvoir lire la valeur décryptée, il faut utiliser une méthode dite « two-way encryption ». Celle-ci peut restaurer la valeur originelle si on connaît la clé (une chaîne de caractères par exemple) qui a permis de l’encrypter. Les algorithmes connus sont par exemple RDA, DES, Triple-DES.
Pourquoi ne pas imaginer que notre couche d’accès aux données puisse encapsuler ces algorithmes, et proposer au programmeur d’encrypter une donnée avec une clé qu’il pourrait spécifier, et que lors de la lecture des entités, la couche se charge de les décrypter, rendant ce travail complètement transparent. Surtout que .Net met à notre disposition toutes les classes nécessaires dans le package System.Security.Cryptography.
Ce principe peut s’avérer très utile dans la plupart de applications, tout en restant d’une simplicité extrême.
Typiquement voici comment utiliser un champ codé en MD5, sachant que nous ne pouvons pas récupérer la donnée originelle, mais seulement tester si une valeur est la bonne :
// Charge l’utilisateur qui tente de se loguer
La méthode CheckMD5Password sera chargée d’encrypter la chaîne de caractères passée en paramètre, et de comparer le résultat avec la valeur qui se trouve dans la base de données.
J’ai volontairement gardé cette partie pour la fin, car c’est sans doute la plus complexe de toutes. Derrière ce concept se cachent énormément de problèmes, ainsi que des techniques très intéressantes à mettre en place pour les résoudre.
Lorsque l’on cherche l’utilité d’un cache objet, on pense tout d’abord à la notion de performance. Dans le cas d’une couche d’accès aux données, cette partie est surtout nécessaire pour conserver la cohérence des instances générées.
Un Framework de persistance a beau implémenter un cache, il faut surtout voir ce dont il se charge, car entre une simple pile d’objets et un cache qui distingue les accès en lecture/écriture, qui automatise le recyclage des instances à l’aide d’un ramasse miette (garbage collector) et qui
gère les accès concurrentiels, la différence peut vite être énorme.
Imaginons que lors de l’appel d’une méthode de la couche de services, un enregistrement soit chargé deux fois. Une couche d’accès très simple renverra deux instances distinctes, alors qu’elles représentent le même enregistrement en base de données. Si l’utilisateur modifie les deux instances, la cohérence de l’objet sera perdue. Pour cette raison il nous faut une liste de références vers les objets, identifiés par un numéro unique correspondant à l’enregistrement qu’ils représentent (le fameux GUID). Ainsi, à chaque instanciation d’un objet, on va d’abord vérifier si on ne peut pas le récupérer de notre pile d’objets, résolvant ainsi le problème.
Imaginez maintenant que vous utilisiez votre couche d’accès dans une application ASP.Net, et que votre cache soit statique. Votre cache est alors partagé par tous les utilisateurs et il se peut très bien que plusieurs d’entre eux tentent de charger le même objet, et de le modifier. Pour ne pas avoir à verrouiller tous les autres processus durant de telles utilisations (mot clé lock en C#), il va falloir proposer un cache pour chacun des utilisateurs. Mais ce problème n’apparaît que si on désire modifier un objet et on peut donc imaginer pouvoir créer un cache ne contenant que des objets chargé en lecture, qui serait partagé par tous les utilisateurs, et invalidé (éligible au rechargement), dès qu’un utilisateur charge cet objet en écriture et le sauvegarde.
Cependant, il va bien falloir libérer ces instances un moment ou un autre, car sinon les objets vont être chargés jusqu’à ce qu’ils soient tous en mémoire. On atteindra vite la saturation de la machine si la base de données est importante. Nous devons donc imaginer un moyen pour libérer les instances.
On pourrait se dire que le ramasse miette du Framework .Net va s’en charger, mais il ne le peut pas, car notre cache possède une référence vers les objets et le ramasse miette ne passera que lorsque toutes ces références seront perdues. La solution qui vient à l’esprit est de programmer un timer pour que les références soient libérées au bout d’un certain temps. Cette solution n’est pas viable, car rien ne nous dit que durant ce laps de temps, la mémoire n’arrivera pas déjà à saturation alors que les objets ne sont plus utilisés. La meilleure solution est d’implémenter le pattern « Smart Pointer ».
Le pattern « Smart Pointer » est connu des programmeurs C++ car ce langage ne possède pas de ramasse miette. Ils doivent alors libérer manuellement la mémoire occupée. C’est ce pattern qui est utilisé pour créer des ramasses miettes. On l’utilise pour compter le nombre de références vers une même instance, permettant d’effectuer un traitement particulier lorsque ce nombre atteint zéro. Si vous voulez comprendre plus précisément le fonctionnement de ce pattern, référez-vous à cet article
http://ootips.org/yonat/4dev/smart-pointers.html
Une fois les références perdues, on peut supprimer l’objet de notre cache et c’est alors que le ramasse miette du framework .Net passe pour libérer la mémoire.
Indépendance
On n’a plus à se soucier des problèmes liés aux bases de données telles que le mapping, ou les méthodologies d’accès aux différents serveurs de bases de données.
Performances
L’utilisation d’un cache objet performant permet d’améliorer sensiblement les caractéristiques de montée en charge des applications.
Modélisation objet
Elle est d’autant plus facile qu’elle est objet, permettant une meilleure compréhension du système de données grâce à l’UML, et une réutilisation d’autant plus facile de modèles préexistants.
Savoir-faire
En informatique, personne n’est spécialiste dans tous les domaines, et l’exploitation des bases de données est un domaine vaste. Les frameworks de persistance permettent à n’importe qui d’exploiter des méthodologies d’accès avancées, avec une simplicité d’utilisation déconcertante.
Temps de développement
Les temps de développement sont énormément réduits, encore plus si il y a peu de logique métier à votre application. Pour vous donner un ordre de grandeur, je vais prendre l’exemple d’une application que j’ai été amené à développer : sur une application financière professionnelle développée en C# comportant énormément de règles métiers, sur 850 000 lignes de codes, 300 000 ont pu être générées par un outil de persistance. Je pense que celle-ci n’aurait jamais vu le jour sans de tels outils.
Je rencontre énormément de programmeurs et de chefs de projets qui n’ont pas connaissance des outils de persistance, soit parce qu’ils ne pensent pas en avoir besoin, soit par manque de temps pour s’y intéresser et prendre ainsi conscience des avantages que cela pourrait leur apporter.
Il existe énormément de produits de ce type dans le monde Java, et de récents travaux ont aboutis à la définition d’un standard de services à apporter (JDO), permettant ainsi une plus grande abstraction vis-à-vis des produits commerciaux.
Dans l’univers .Net, ce type de produits est encore rare, et le niveau des fonctionnalités très disparates, laissant souvent les utilisateurs sur leur faim. Microsoft annonce depuis
un certain temps son produit
ObjectSpaces qui devrait être disponible cette année, et fonctionner uniquement avec SQL Server. Le marché pour ce type de
produit est très prometteur, et beaucoup de sociétés s’y engouffrent. J’espère que ce document vous permettra de mieux appréhender ceux-ci, pour faire le choix qui correspond le mieux à vos attentes.
Sachez tout de même que la société Evaluant http://www.evaluant.com dont je suis le directeur technique, a développé le seul logiciel pour le
Framework .Net implémentant tous les concepts que je viens d'énoncer à
travers le
Data Tier Modeler. Cette application génère également une application WinForms et une application WebForms vous permettant d’administrer la base de données générée. Vous pouvez télécharger et utiliser librement une version complètement fonctionnelle de ce logiciel sur notre site web.
N’hésitez pas à me faire part de vos remarques ou de vos questions si vous voulez en savoir plus sur ce domaine.
Auteur : Sébastien Ros (Directeur technique de la société Evaluant)
Copyright © Février 2003 - La copie ou reproduction de ce document est strictement interdite sans l'accord préalable de son auteur
DotNetGuru remercie Sébastien Ros et la société Evaluant pour ce document.
Ressources
Site de la société Evaluant : www.evaluant.com
Le site de Scott Ambler : http://www.ambysoft.com
Documentation sur les smart Pointers : http://ootips.org/yonat/4dev/smart-pointers.html