Entity Framework

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 :

                                     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.