Entity Framework
La bibliothèque de classes Entity Framework propose un mécanisme de mapping entre un modèle relationnel et un modèle objet. Le framework utilise le langage Linq pour interroger les données présentes dans les classes. Ce Framework est disponible avec VS 2008 et son service pack 1 (VS 2008 + SP1 VS 2008) ; ceci correspond au framework dotnet 3.5.
1) Etude d'un exemple : école de conduite
On dispose d'un modèle de données simple de gestion de cours de conduite, auto ou moto:
- Une leçon concerne un véhicule, auto ou moto
(si c'est une voiture, on connait son modèle, sinon la cylindrée
de la moto)
- Lorsqu'une leçon est éffectuée, le crédit horaire
de l'élève concerné est décrémenté.
La base de donnée est sous SQL Server et dispose d'une
procédure stockée pour l'insertion d'un nouvel élève.
Récupérer la base à restaurer.
2) Prise en main en mode console
Créons un nouveau projet, de type console. Ajoutons à
ce projet un nouvel élément, de type Entity Data Model -EDM-
:
C'est ce modèle, généré par Visual
Studio, qui va piloter le mapping. Créons une nouvelle connexion :
Sélectionnons la base de données (ecoleConduite)
Indiquons que nous importons les tables et procédures stockées :
Après avoir terminé la configuration de la connexion
on peut observer le modèle de classe généré :
Remarque : si le modeleur ajoute une classe dtProperties, supprimez-la du modèle visuel, il s'agit d'on bogue référencé.
2.1 Code généré
Le diagramme est de "type" uml (champ, cardinalités) ; observons les classes générées :
- Une classe principale, dérivant d'ObjectContext:
C'est à partir de cette classe que les différents traitements
de mapping seront effectués.
- Une classe par table, dérivant d'EntityObject
:
Le modèle de mapping est de type 1-1 (une table =>
une classe), la construction des instances propose une méthode static
CreateEleve, mais attention seuls les champs Not Null de la base
sont présents dans les arguments ici !
La génération automatique distingue la notion de champ -privé-,
par exemple _id, de celle de propriété -publique-, par
exemple id.
La déclaration partial permettra de surcharger la classe sans
intervenir dans le fichier généré.
Remarque : le fait que la classe Eleve hérite d'une classe technique témoigne d'une forte adhérence entre les "classes métiers" et les classes techniques ; on pourrait néanmoins s'affranchir de cette dépendance en implémentant les (nombreuses...) interfaces dont dépend EntityObject.
- Des propriétés de navigation dans les
deux sens.
Par exemple la classe LECON contient une référence sur un Elève
(de nom ELEVE) et sur un véhicule (de nom VEHICULE). La classe ELEVE
contient une collection de leçons :
public global::System.Data.Objects.DataClasses.EntityCollection<LECON>
LECON
Il est possible de modifier le nom des propriétés de navigation -cf plus loin-
2.2 Exemples d'utilisation des classes
2.2.a Opérations ajout/modification/suppression
2.2.a.1 Ajout
Nous allons commencer par ajouter un nouvel élève
:
La ligne 1 crée une instance de la classe centrale (ObjectContext)
du FrameWork
La ligne 2 utilise le "constructeur statique"
La ligne 3 ajoute l'instance au contexte
La ligne 4 sauve en base le modèle.
Si nous ouvrons l'explorateur de serveur et la table elève,
on vérifie l'insertion :
Remarque : on vérifie ici qu'Entity Framework se distingue bien du modèle déconnecté d'ADO qui conservait en mémoire les copies des tables et mettait à jour qu'à la demande.
2.1.a.2 Modification
Modifions le crédit horaire du premier élève
:
L'affichage indique le crédit courant :
L'ouverture de la table confirme la mise à jour :
2.2.a.3 Suppression
Pour supprimer la première leçon :
Attention : la suppression entraîne la suppression en cascade des objets enfants.
2.2.b Chargement des données en mémoire.
Pour charger des données du contexte, on peut,
soit utiliser le langage Linq (ce que nous ferons couramment) soit utiliser
les expressions lambda :
Remarque : la définition de la requête req n'entraîne pas son exécution, seule l'accès par une méthode spécifique (ici First) charge en mémoire le résultat de la requête.
Si une requête retourne plusieurs occurrences, on utilise
la méthode ToList de la requête et on itère sur
le résultat:
Remarque : c'est ici la méthode ToList qui appelle l'exécution de la requête mais on pourrait aussi demander l'itération directement sur l'objet req car req est de type IQueryable itérable avec foreach.
Le langage Linq permet ainsi de nous affranchir de jointures
chères à SQL :
Mais, pas de miracle néanmoins, le code suivant ne peut fonctionner (même si le compilateur ne signale pas d'erreur):
En effet, la requête Linq est traduite en SQL ; foreach ne peut itérer sur des données inexistantes (ici les informations du véhicule) !!
Pour s'en convaincre, voici la requête exécutée
:
2.2.c Chargement des objets connexes
Pour charger les données connexes (celles qui sont atteingnables grâce aux propriétés de navigation) il fait explicitement l'indiquer dans la reqête l'insertion souhaitée, ainsi :
C'est la méthode Include qui demande l'insertion
des leçons de l'élève (rappel : LECON est le nom par
défaut de la propriété de navigation élève
=> leçon). Nous pourrions "plonger" plus loin dans l'insertion
en réutilisant la méthode Include : from e in monModele.ELEVE.Include("LECON").Include(...
Après avoir récupéré l'élève,
on peut parcourir ses leçons grâce à foreach sur les leçons
de l'élève ; le résultat est sans surprise :
Voici une seconde version, un peu différente dans le
chargement des données, qui utilise la méthode Find :
Ici, toutes les lignes de la table ELEVE sont chargées en mémoire (ainsi que les leçons associées) ; la méthode Find sur la liste obtenue permet d'extraire, par une expression lambda, l'élève voulu. La méthode Find s'utilise à partie de la clé de la table -id ==128-
On pourrait également utiliser une jointure pour charger
explicitement des données connexes :
Remarque : noter l'emploi des classe anonyme pour récupérer
les champs (select new {...)
Voici la jointure générée :
et le résultat obtenu :
Remarque : pour visualiser les requêtes SQL exécutées, on écrit et utilise ici une méthode d'extension de l'interface IQueryable:
3) Quelques commentaires.
Les différents framework de mapping utilisent des stratégies différentes de chargement des données (base=>mémoire). Le problème concerne les objets "connexes" aux données appelées. Jusqu'à quelle profondeur ce chargement se fait-il ? Par exemple, si l'on demande toutes les leçons, le framework charge-t-il la voiture associée à chaque leçon ? Le choix délibéré d'Entity Framework est de ne charger (par défaut) que les données explicitement demandées : on parle de chargement explicite. Il n'en pas de même pour tous les frameworks de mapping. Ce fonctionnement par défaut garantit de ne solliciter la base de données que selon des besoins clairement explimés par le développeur. Nous avons vu plus haut que le chargements d'objets connexes se faisait grâce à la méthode include sur la requête.
Le modeleur EDM (Entity Data Model) permettant, à l'aide
de l'assistant -cf plus haut-, de construire la modélisation graphique
utilise en fait trois fichiers XML qui décrivent la structure des classes,
des relations et du mapping ; ces 3 fichiers sont compilés comme ressource
dans le projet :
4) Quelques manipulations
4.a Modifications sur le modèle
On peut modifier le nom des champs des classes ; ainsi, transformons id de Vehicule par un numImma moins connotée base de données.
A partir du modeleur de mapping, on peut voir la modification
de mapping :
On peut aussi modifier les proprietés de navigation :
4.b Modification par le code
On peut ajouter de nouveaux attributs ou méthodes dans les classes "métiers". L'architecture basée ici sur des classes partial encourage fortement à ne pas intervenir sur le code généré. Le plus cohérent (et simple) est donc d'ajouter une classe partial, de même nom que la classe à enrichir et ceci dans un fichier distinct :
Remarques : nous avons ajouté un constructeur "classique" ; comme le code généré dans le contexte utilise un constructeur par défaut, il est nécessaire d'ajouter un constructeur par défaut explicite. Nous pouvions aussi surcharger le "constructeur statique". L'ajout d'un attribut "commentaire" n'offre que très peu d'intérêt puisqu'il n'y aura pas de mapping...
L'appel du constructeur de LECON peut prendre la forme suivante
:
Remarque : nous avons utilisé des expressions lambda, on pouvait, bien sûr, faire appel à Linq
4.c
Mise en oeuvre de l'héritage
La classe vehicule regroupe voitures (modèle) et motos (cylindrée) ; il serait utile de faire dériver deux classes (auto et moto) d'une même classe vehicule.
Dans la classe vehicule, conservons uniquement le numéro
d'immatriculation :
Dans le modeleur, à partir de la boite à outils,
ajoutons une nouvelle entity, classe MOTO, ainsi que la relation d'héritage
; supprimons la propriété id générée:
Ajoutons une propriété laCylindree (de
type int32).
A partir des détails de mapping sur la classe
MOTO, indiquons le mappage à VEHICULE :
Indiquons le mapping de la propriété :
Définissons la condition de filtre :
Procédons de même pour la classe AUTO :
Testons maintenant :
La base de donnée a été mise à jour
:
Le champ voitureO/N est bien passé aussi à false.
On peut afficher les motos ainsi :
Remarque : on peut regréter que le contexte ne connaisse pas directement le type MOTO ...
Ce qui produit :
Si l'on observe la requête générée,
on constate sans surprise le filtre sur le champ voitureO/N :
3.d Utilisation de procédure stockée
La base contient une procédure stockée permettant
l'insertion d'un nouvel élève ; pour l'appeler, il suffit de
l'ajouter dans le modeleur :
Remarque : étrangement, il faut déclarer un type de retour, alors que la procédure stockée n'en a pas !!
L'appel se fait à partir du contexte:
5) Le binding
Le binding ou liaison de données permet de créer
un lien bidirectionnel entre un composant graphique et une source de données
au sens large (Table, ArrayList, objet....).
Le mécanisme est très proche de celui mis en oeuvre avec une
source de données ADO. Nous allons juste montrer ici quelques exemples.
5.1 Binding simple.
Nous allons gérer les véhicule à l'aide
d'un DataGridView.
Construisons un formulaire avec deux boutons, l'un pour charger, l'autre pour
sauvegarder. Ajoutons un dataGridView :
Indiquons la source de données pour le DataGridView,
la classe VEHICULE ; paramétrons le dataGridView et ajoutons une nouvelle
source de données, de type objet :
Sélectionnons la classe VEHICULE :
La source de données a été ajoutée
au projet et un composant de Binding ajouté au formulaire :
Après avoir chargé le modèle de ses véhicules
avec une requête Linq, il ne reste plus qu'à lier le composant
de binding à la source et le DataGridView au composant de binding :
On pouvait ajouter un BindingNavigator pour un parcours par occurrence.
5.2 Binding lié à deux composants
Nous désirons visualiser (et éventuellement) modifier les leçons d'un élève sélectionné :
Le premier DataGridView sera bindé aux élèves, le second à la relation entre l'élève et ses leçons (comme pour binding associé à des tables).
5.2.a Le DatagridView associé aux élèves
La manipulation est identique à celle décrite pour le binding source ; mais on peut déclarer au préalable la source de données objet :
A partir du menu Données/Ajouter une nouvelle source
de données, ajoutons la source pointant sur les élèves,
cette source apparaît dans la fenêtre :
Ajoutons un composant de binding au formulaire, dont la source est la source créée ci-dessus :
Ajoutons un DataGridView dont la source de données
est ce composant de binding, ne conservons que certaines données :
5.2.b
Le DataGridView associé aux leçons
Procédons de la même manière (création
de la source de données, paramétrage du composant de binding,
paramétrage du DataGridView) :
En toutes rigueur, la liaison à la source "LECON" n'est pas nécessaire ; elle est seulement utile pour paramétrer simplement (modification des colonnes) le DataGridView.
5.2.c Chargement des données
Ceci se fera (par exemple) à partir d'un bouton sprécifique dont le code de l'événement click est le suivant :
Notez l'appel à Include des leçons connexes
à chaque élève.
La sauvegarde des données appelle la méthode SaveChanges.
5.3 Binding lié à trois composants
Terminons en ajoutant chaque véhicule associé à une leçon :
Deux versions sont proposées, l'une avec un DataGridView et l'autre
une zone de texte ; les paramétrages sont identiques à ceux
vus plus haut, le code de chargement diffère légèrement
:
Notez l'appel à Include qui charge les leçons et les véhicules associés
Pour lier la zone de texte, ceci peut se faire en mode conception.