I. Introduction▲
L'arrivée du framework .NET 4.0 a vu naître la version 4 d'Entity Framework. Cet ORM permet de s'interfacer avec une base de données SQL Server via trois solutions en fonction des besoins :
- Database First où l'on part d'une base existante, ce qui permet de créer le modèle par simple drag and drop ;
- Model First où comme son nom l'indique, un modèle est créé dans le designer, ce dernier assurant la génération de la base de données une fois une connexion spécifiée ;
- et enfin, Code First, dernier né de la version 4.1. Une approche centrée autour du code que je vous propose d'étudier dans ce tutoriel.
II. But du tutoriel▲
Au travers des quelques exercices qui vont suivre, nous allons procéder à la création d'une base de données sur SQL Server à partir de code écrit dans Visual Studio, en utilisant l'approche Code First mise à disposition par Entity Framework.
Commençant par une conception objet très simple que nous étofferons par la suite, nous poursuivrons par une conception plus élaborée, une observation de la mécanique de génération de la base de données, avant de passer à des techniques un peu plus avancées, mais guère plus, promis.
L'ensemble de la solution est basé sur la version 4.2 d'EF (actuellement en version 4.3).
III. Prérequis▲
Ce tutoriel se base sur :
· Visual Studio 2010 ;
· Framework .NET 4.0 ;
· Entity Framework 4.2 ;
· SQL Server 2008 Express.
Dans cet article l'abréviation CF sera utilisée pour Code First.
IV. Architecture utilisée▲
Pour les besoins de ce cours, nous allons utiliser l'architecture suivante.
IHM : Interface Homme Machine.
DAL : Data Access Layer (couche d'accès aux données).
DTO : Data Transfer Object.
Généralement, une BLL (Business Logic Layer) est intercalée entre l'IHM et la DAL, c'est d'ailleurs conseillé afin d'y développer toute la logique métier. Pour plus de détails à ce sujet, je vous recommande l'excellent tutoriel d'immobilis sur l'architecture multicouche : http://immobilis.developpez.com/articles/dotnet/architecture-multicouche-asp-net/.
Nous n'aurons pas de logique métier à implémenter ici, donc afin de simplifier le tutoriel, nous nous contenterons d'une DAL faisant l'interface avec notre BDD, puis d'une IHM. Gardez à l'esprit que pour un « vrai » projet, l'utilisation de la BLL est recommandée.
Concrètement, dans Visual studio, ça donne ceci :
Comme vu sur le schéma, DTO est un projet transverse, donc référencez-le dès à présent dans tous les autres projets de la solution.
Maintenant que nous avons notre base de travail, mettons-nous en route.
V. Mise en situation !▲
- Mendez, mécanicien : « Salut mon ami ! Dis-moi, j'aimerais un programme pour gérer mon garage, connaître mon stock, ce genre de fonctionnalités, pour faire du grand Mendez quoi ! »
Voici notre besoin de départ (J), on va se débrouiller avec ça, on dira que cela sera suffisant dans un premier temps.
Pour faire simple, nous allons commencer en étant un peu feignants. Disons que ce brave mécano gérera dans son garage uniquement des Harleys.
On arrive donc à cette classe :
namespace
CodeFirst.
DTO
{
public
class
EntityHarley
{
public
string
Couleur {
get
;
set
;
}
public
int
Reservoir {
get
;
set
;
}
public
int
NbChevaux {
get
;
set
;
}
public
string
Modele {
get
;
set
;
}
}
}
Effectivement, on a été feignant, la conception objet n'est pas bien maligne pour le coup, mais c'est pour la bonne cause !
Aucun doute sur la question, les classes qui vont enrichir notre projet DTO sont donc bien des POCO (Plain Old CLR Object, un POCO = un DTO).
Ces classes ont pour caractéristiques de ne pas hériter d'une classe en particulier ou de devoir implémenter une interface spécifique. Il n'y a aucune adhérence à un quelconque produit ou DLL permettant de s'interfacer avec un ORM.
De simples propriétés les composent afin de les décrire, il n'y a pas de méthode.
Et ça tombe bien, après tout, nous sommes en train de concevoir de futures tables pour notre base de données.
V-A. Un databaseContext pour les unir tous…▲
Maintenant que nous savons avec quelle entité nous allons travailler, il nous faut encore de quoi la manipuler, une espèce de maître des entités qui les gouvernerait toutes et nous permettrait d'interagir avec elles.
Passons dans la DAL et utilisons l'excellent NuGet pour obtenir la version 4.2 d'EF. (Voir tuto sur NuGet ici : https://rdonfack.developpez.com/tutoriels/dotnet/presentation-gestionnaire-packages-net-nuget/).
Attention à bien préciser la version avec -version, sinon ce sera la dernière version qui sera téléchargée. De même, mentionnez CodeFirst.DAL comme nom de projet, sinon la DLL
sera installée dans le projet principal, la console.
NuGet va télécharger et référencer dans la DAL la DLL version 4.2 d'EF.
Créons une classe GarageContext, qui va hériter de DbContext. Le DbContext, dans les autres modèles d'EF, est plutôt connu sous le nom ObjectContext.
Pour faire simple, le DbContext est un ObjectContext simplifié. Il va notamment nous permettre de manipuler nos tables, donc nos DTO, avec des opérations type CRUD (Create Read Update Delete).
namespace
CodeFirst.
DAL
{
class
GarageContext :
DbContext
{
public
DbSet<
EntityHarley>
Harleys {
get
;
set
;
}
}
}
Avec ceci, nous informons notre contexte de données des entités qui devront être considérées comme des tables et avec lesquelles nous allons donc réaliser nos transactions et nos diverses manipulations, dans le cas présent, une collection d'entités Harley.
On note au passage l'utilisation de DbSet pour marquer l'existence d'une table.
Et maintenant, dernier point, la configuration de la chaîne de connexion à la base de données.
<?xmlversion="1.0"encoding="utf-8"?>
<configuration>
<configSections>
</configSections>
<connectionStrings>
<
addname
=
"GarageContext"
connectionString
=
"DataSource=.\SQLEXPRESS;IntegratedSecurity=True"
providerName
=
"System.Data.SqlClient"
/>
</connectionStrings>
</configuration>
Détail qui a son importance, le nom de la chaîne de connexion est le même que le nom de notre classe de contexte.
Techniquement, on a (presque ;) fini l'implémentation de Code First dans sa version la plus sommaire. Encore un peu de code dans la DAL et nous allons pouvoir commencer à bidouiller.
Prenons la DAL et ajoutons cette classe.
namespace
CodeFirst.
DAL
{
public
class
HarleyProvider
{
public
HarleyProvider
(
)
{
}
public
List<
EntityHarley>
GetAllHarleys
(
)
{
using
(
GarageContext context =
new
GarageContext
(
))
{
return
context.
Harleys.
ToList
(
);
}
}
}
}
Notre solution ressemble maintenant à ça :
Plus qu'à faire un bout de code dans la console :
static
void
Main
(
string
[]
args)
{
AfficheAllVehicule
(
);
Console.
WriteLine
(
"Fin"
);
Console.
ReadLine
(
);
}
static
void
AfficheAllVehicule
(
)
{
HarleyProvider harleyProvider =
new
HarleyProvider
(
);
foreach
(
EntityHarley harley in
harleyProvider.
getAll
(
))
{
Console.
WriteLine
(
harley);
}
}
VI. Premiers essais▲
Pour l'instant, on va se contenter de faire un appel et d'observer ce qu'il se passe. Voici l'état de notre serveur SQL.
On exécute… Paf !
Notre entité ne possède pas de clé, donc erreur pour la génération du modèle. Eh oui, souvenez-vous, que sommes-nous en train de faire ? Nous avons conçu des classes qui vont être de futures tables pour notre base, il leur faut donc des clés primaires.
Rajoutons les propriétés qui vont bien dans chacune des entités.
public
int
HarleyPK {
get
;
set
;
}
2e essai…
Et même erreur !
En fait, l'explication est simple. Comment la génération du modèle est capable de parser les classes pour trouver la clé primaire ? La réponse est dans la question, c'est bien une convention de nommage qui est utilisée et, pour réussir, la clé doit avoir le nom de la classe + ID ou tout simplement ID.
Retentons avec ceci :
public
int
ID{
get
;
set
;}
3e essai…
A priori l'appel, s'est passé correctement. Jetons un coup d'œil dans SQL Server.
Succès : une base de données CodeFirst.DAL.GarageContext a bien été créée et si je regarde mes tables, elles correspondent bien à l'entité que j'ai créée dans mes DTO.
Au passage, on remarque la présence d'une table créée automatiquement par CodeFirst : EdmMetaData. On verra son utilité par la suite.
Maintenant, étoffons un peu ceci avec un peu de CRUD.
Tout d'abord dans le provider, créons ces méthodes (voir code en annexes), mettons à jour la BLL et enfin le Program.cs.
class
Program
{
static
void
Main
(
string
[]
args)
{
AfficheAllVehicule
(
);
AjouterHarleys
(
);
AfficheAllVehicule
(
);
MAJ
(
);
AfficheAllVehicule
(
);
Console.
WriteLine
(
"Fin"
);
Console.
ReadLine
(
);
}
static
void
AfficheAllVehicule
(
)
{
HarleyProvider harleyProvider =
new
HarleyProvider
(
);
foreach
(
EntityHarley harley in
harleyProvider.
getAll
(
))
{
Console.
WriteLine
(
harley);
}
}
static
void
AjouterHarleys
(
)
{
Console.
WriteLine
(
"===== Création d'harley dans le garage ====="
);
HarleyProvider provider =
new
HarleyProvider
(
);
provider.
Create
(
new
EntityHarley {
Couleur =
"Noire"
,
Modele =
"883 Iron"
,
Reservoir =
12
}
);
provider.
Create
(
new
EntityHarley {
Couleur =
"Rouge"
,
Modele =
"1200 Nighster"
,
Reservoir =
12
}
);
provider.
Create
(
new
EntityHarley {
Couleur =
"Grise"
,
Modele =
"1200 Forty height"
,
Reservoir =
12
}
);
provider.
Create
(
new
EntityHarley {
Couleur =
"Noire"
,
Modele =
"Fat boy"
,
Reservoir =
25
}
);
Console.
WriteLine
(
"======================"
);
}
static
void
MAJ
(
)
{
Console.
WriteLine
(
"===== Mise à jour de la couleur ====="
);
HarleyProvider provider =
new
HarleyProvider
(
);
foreach
(
EntityHarley harley in
provider.
getAll
(
))
{
harley.
Couleur =
"Violet"
;
provider.
Update
(
harley);
}
Console.
WriteLine
(
"======================"
);
}
}
Exécutons :
Ce code n'a rien d'exceptionnel, mais il a le mérite de valider certaines choses :
- les opérations de base type CRUD fonctionnent ;
- par rapport à un mapping « classique », le fait que la base soit créée à partir des classes est transparent pour nous, il n'y a rien à rajouter au niveau de la DAL pour opérer.
À ce stade, nous venons d'implémenter CF dans sa version la plus basique.
Maintenant que ces quelques bases sont validées, étoffons un peu tout ça, à commencer par le modèle de données.
Étant donné que ce sont des classes qui vont gérer la création de la base de données, on est en droit de se demander comment se comporte Code First avec l'héritage. Et puisqu'on parle de base de données, on va forcément en venir aux relations, comment les gérer en CF ?
Je vous propose de répondre à ces questions avec la partie “Techniques avancées” !
VII. Techniques avancées▲
VII-A. Renommer la base▲
Avant de procéder à une évolution de la conception objet, nous allons modifier le nom de la base de données.
Pour ce faire, il suffit de modifier le constructeur du GarageContext :
public
GarageContext
(
) :
base
(
"GarageMendez"
)
{
}
On réexécute… Et on observe dans SQL Management Studio.
Voilà notre base renommée (enfin, une autre base est créée avec le bon nom).
Passons maintenant à l'évolution du modèle.
VII-B. Évolution de la conception objet▲
Reprenons donc notre conception objet, à la base très simple, pour la faire évoluer.
Au niveau des DTO, on étoffe pour obtenir ceci :
On obtient donc la solution suivante :
Le Main est quant à lui réduit à ceci :
class
Program
{
static
GarageManager garage =
new
GarageManager
(
);
static
void
Main
(
string
[]
args)
{
AfficheAllVehicule
(
);
Console.
WriteLine
(
"Fin"
);
Console.
ReadLine
(
);
}
static
void
AfficheAllVehicule
(
)
{
HarleyProvider harleyProvider =
new
HarleyProvider
(
);
foreach
(
EntityHarley harley in
harleyProvider.
getAll
(
))
{
Console.
WriteLine
(
harley);
}
}
}
On exécute, et là, on peut observer le message d'erreur suivant :
EF a détecté que notre modèle de données, enfin, nos classes, ont été modifiées depuis la dernière création de la base et nous fait part de ce déphasage.
Avant d'aller plus loin, certains se sont peut-être posé la question. Mais comment EF a-t-il su que les classes ont été modifiées ?
Souvenez-vous de cette table, EdmMetadata, créée automatiquement.
Cette table permet de savoir s’il y a eu un changement dans notre design, on peut d'ailleurs aller la consulter, on y trouve un enregistrement contenant un hachage, « ModelHash », on peut donc en déduire qu'un hash est obtenu à partir des DTO une première fois et que pour les accès suivants, un nouveau hash est calculé, par comparaison EF en déduit si les classes ont été modifiées.
Pour résoudre ce problème, EF nous propose deux solutions, soit manuellement on delete et on met à jour la base, soit on utilise un IDatabaseInitializer…
Étant donné que nous sommes en phase de développement, nous allons prendre la 2e solution, bien plus souple et permettant plus de choses.
VII-C. Stratégie d'initialisation▲
Nous allons créer une nouvelle classe, GarageContextInitializer.
class
GarageContextInitializer :
DropCreateDatabaseIfModelChanges<
GarageContext>
{
protected
override
void
Seed
(
GarageContext context)
{
List<
EntityHarley>
listeHarley =
new
List<
EntityHarley>(
);
listeHarley.
Add
(
new
EntityHarley {
Couleur =
"Noire"
,
Modele =
"Fatbob"
,
Reservoir =
15
}
);
listeHarley.
Add
(
new
EntityHarley {
Couleur =
"Blanche"
,
Modele =
"Road King"
,
Reservoir =
30
}
);
listeHarley.
Add
(
new
EntityHarley {
Couleur =
"Noire"
,
Modele =
"883 Iron"
,
Reservoir =
12
}
);
List<
EntityFerrari>
listeFerrari =
new
List<
EntityFerrari>(
);
listeFerrari.
Add
(
new
EntityFerrari {
Couleur =
"Rouge"
,
Modele =
"Enzo"
}
);
listeFerrari.
Add
(
new
EntityFerrari {
Couleur =
"Bleue"
,
Modele =
"California"
}
);
listeHarley.
ForEach
(
entity =>
context.
Harleys.
Add
(
entity));
listeFerrari.
ForEach
(
entity =>
context.
Ferrari.
Add
(
entity));
}
}
On peut voir qu'elle hérite d'une classe, très explicite, DropCreateDatabaseIfModelChanges.
Cette classe est une implémentation de IDatabaseInitializer, qui détruit la base et la recrée si le modèle change, et éventuellement, réinjecte un jeu de données.
C'est le rôle de la méthode « protected override void Seed(GarageContext context) ».
Et maintenant nous allons modifier le constructeur de GarageContext comme suit :
public
GarageContext
(
) :
base
(
"GarageMendez"
)
{
Database.
SetInitializer<
GarageContext>(
new
GarageContextInitializer
(
));
}
Recompilons et relançons le programme, on obtient :
Tout fonctionne, ma base a été recréée et dans la foulée a été alimentée. Bien sûr, nous n'oublierons pas d'enlever cette feature lors d'une mise en production, ça serait dommage de perdre des données…
Sachez qu'il existe une dernière solution pour éviter d'avoir un message d'erreur suite à une modification des DTO. C'est Julie Lerman qui en parle sur son blog, il suffit de changer les stratégies d'initialisation, je vous laisse voir ça ici :
./fichiers/turning-off-code-first-database-initialization-completelyhttp://thedatafarm.com/blog/data-access/turning-off-code-first-database-initialization-completely/Maintenant que nous avons implémenté de l'héritage, il serait intéressant de voir comment cela s'est traduit en base.
VII-D. Code First et Héritage▲
Voyons ce que nous dit le Management Studio :
On constate que chacune des propriétés propagées par l'héritage a été dupliquée dans les tables Harleys et Ferrari.
Maintenant que nous nous sommes assurés de ce point, établissons la relation entre le garage et les véhicules, en partant du principe qu'un garage peut posséder plusieurs véhicules et qu'un véhicule appartient à un seul garage.
Le moteur de génération de Code First utilise une convention d'écriture. Lors de la génération de la bdd, il va donc examiner vos classes.
La convention utilisée pour symboliser une relation du type « one to many » se fait via les mots clés virtual et ICollection.
Rajoutons les lignes suivantes dans l'entité garage :
public
virtual
ICollection<
EntityVehicule>
Vehicules {
get
;
set
;
}
Et dans l'entité Vehicule nous rajoutons ceci :
public
virtual
EntityGarage Garage {
get
;
set
;
}
Réexécutons le programme et observons les changements dans le Management Studio :
Tout d'abord changement de taille, les tables pour les entités Harleys et Ferraris ont disparu pour laisser place à une nouvelle table pour les entités Vehicule. Bien évidemment, c'est l'héritage mis en place qui est responsable de ceci, nous allons voir pourquoi.
En ce qui concerne notre relation entre un garage et ses véhicules, on constate la présence d'un nouveau champ « Garage_ID », FK sur notre garage.
Si tous les types différents de véhicules sont stockés dans la même table, comment faire la différence ? Eh bien, grâce au nouveau champ créé automatiquement, le discriminant (colonne « Discriminator »). Regardons à quoi il ressemble.
Le discriminant porte le nom de la classe, la FK sur le garage est null car nous ne l'avons pas renseigné, les enregistrements sont conformes à ce qui est dans le Seed.
Alors tout d'abord, pourquoi la création d'une seule supertable, correspondant en fait à la classe mère des entités Vehicules ?
Eh bien, par défaut, lorsque rien n'est précisé, Code First décide de créer la base de données selon le mode Table Per Hierarchy et encore par défaut, si rien n'est précisé c'est le nom de la classe qui fait office de discriminant.
Il est possible de customiser le discriminant avec plusieurs colonnes ou bien encore d'utiliser explicitement le mode Table Per Hierarchy.
Ensuite, sachez qu'il est possible de travailler selon un mode Table Per Type et Table Per Classe, mais cela dépasse un peu le cadre du tutoriel, je vous laisse chercher des infos là-dessus si cela vous intéresse.
Nous allons modifier la classe Harley pour y inclure un booléen indiquant l'existence d'une peinture personnalisée, ainsi que la méthode Seed pour prendre en compte la création d'un garage et relier les véhicules à ce garage.
Réexécutons et observons
Nous avons bien récupéré l'id du garage créé, et on observe que le booléen rajouté est bien spécifique aux entités Harleys, il est initialisé à NULL pour les Ferrari n'ayant pas de sens pour ces entités. C'est peut-être dans ce genre de cas qu'un mode Table Per Type serait préférable).
Nous avons réalisé ici une relation très simple , du type « one to many », entre le garage et les véhicules . Mais on aurait pu faire beaucoup plus poussé grâce à l'API Fluent et à la méthode à surcharger « OnModelCreating » qui permet de modifier le schéma afin d'y apporter une plus grande précision sur les besoins requis.
Continuons ce tutoriel dans les techniques avancées en abordant les règles de validation.
VII-E. Décorer les propriétés avec des attributs▲
Pour l'instant, nous avons été relativement laxistes sur la valeur et le formalisme des données d'entrée. Nous allons mettre un peu plus de rigueur en décorant les propriétés souhaitées avec des attributs adéquats.
On se place dans la classe EntityVehicule, on observe ce que l'on a disposition dans le namespace prévu, à savoir System.ComponentModel.DataAnnotations.
On peut par exemple spécifier qu'une propriété est une clé avec Key, fixer une fourchette avec le Range, indiquer un champ obligatoire avec Required ou bien carrément personnaliser le formalisme avec une expression rationnelle grâce à RegularExpression.
[Range(
1
,
4
, ErrorMessage=
"Le nombre de places est compris entre 1 et 4."
]
public
int
NbPlaces {
get
;
set
;
}
[Required(ErrorMessage=
"Le champ modèle est obligatoire."
)]
public
string
Modele {
get
;
set
;
}
VII-F. DbContext, oui, mais…▲
Précédemment, je vous ai parlé du DbContext en vous expliquant que c'était une version simplifiée de l'objectContext. C'est effectivement le cas et nombre de méthodes sont masquées pour faciliter l'utilisation du dbContext.
Cependant, pour certains cas, il se peut que vous ayez besoin d'avoir recours à des méthodes exposées uniquement dans l'objectContext. Eh bien, sachez qu'à partir de votre dbContext, il est possible d'obtenir l'accès à un ObjectContext, et pour cause, en interne, c'est un ObjectContext qui fait les traitements.
Pour ce faire, on utilise le fait que DbContext implémente l'interface IObjectContextAdapter, on ajoute alors dans notre GarageContext :
public
ObjectContext ObjectContext
(
)
{
return
(
this
as
IObjectContextAdapter).
ObjectContext;
}
Et voilà, nous avons désormais accès à l'ObjectContext interne de notre DbContext !
VIII. Conclusion▲
Notre tutoriel est enfin fini ! Nous sommes donc partis de simples classes décrivant notre modèle de données pour générer une base de données. Ces mêmes classes nous ont également permis d'interagir avec la base et de manipuler les données, tout ça sans action directe avec un outil du type SQL Management Studio, du pur code !
Certains pourraient se dire que cette solution ne fait pas très sérieuse, voire un peu trop fantaisiste par rapport à la précision de leur script SQL. Sachez que cette solution est régulièrement mise en avant dans des démos officielles, comme j'ai pu le voir au Tech Days de Marseille lors de la session architecture, ainsi que dans des tutoriels sur les sites officiels de Microsoft.
Certains sont allergiques au fait de devoir tout écrire côté code. Si ce n'est pas fait là, c'est fait dans un script SQL, alors pour ceux qui comme moi aime l'approche full code, c'est parfait : on a un outil avec une prise en main très simple et si l'on souhaite pousser la mécanique, des fonctions comme celles de l'API Fluent et les DataAnnotations sont là pour répondre au besoin afin de fournir une bdd ciselée au poil.
J'ai envie de vous citer le blog de Julie Lerman sur le sujet : « Code First and DbContext are now ‘The Entity Framework' » http://thedatafarm.com/blog/data-access/code-first-and-dbcontext-are-now-ldquo-the-entity-framework-rdquo/ que l'on pourrait résumer en disant que maintenant, lorsque l'on parle d'EF, on parle de Code First.
De plus, il suffit de regarder les dernières entrées dans le blog de la team ADO.NET pour se rendre compte de l'importance donnée à CF, c'est à mes yeux une solution simple et efficace.
On la privilégiera tout de même pour une nouvelle base de données. En effet, même s'il est possible de faire du CF à partir d'une base existante, cela reste tout de même moins rapide qu'un simple Drag and Drop dans un modèle edmx, surtout si le nombre de tables est important (des add-ons Visual Studio proposent de faire la génération des DTO pour ce cas).
Comme le dit Julie, je vais conclure en vous disant : « Use the best tool for the job […] for *your* job “ »
IX. Liens et références▲
Si je ne devais citer qu'une référence, ce serait le blog de Julie Lerman. Auteur de « Entity Framework », « Entity Framework 2 », « DbContext » parues chez O'Reilly, en étroite relation avec les équipes EF de Microsoft, c'est LA référence dans le domaine et Julie Lerman est une blogueuse très active.
Je compléterai avec le blog de la team ADO.NET, également très active :
http://blogs.msdn.com/b/adonet/.
Voici le zip de la solution complète:
X. Remerciements▲
Je tiens à remercier l'ensemble de l'équipe .NET pour ses corrections, en particulier Tomlev. J'aimerais également remercier Max, Djibril et jacques_jean pour leurs aides et relecture orthographique.