|
|
|
@ -1 +1,328 @@
|
|
|
|
|
|
|
|
|
|
# ex_042_002_EF_CF_data_annotations_
|
|
|
|
|
*03/01/2020 ⋅ Marc Chevaldonné*
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
Le lien entre une base de données et vos classes à l'aide de **Entity Framework Core** se fait via une classe dérivant de ```DbContext```.
|
|
|
|
|
Cette classe doit contenir des ```DbSet<T>```. Chaque ```DbSet<T>``` correspond à une table et ```T``` correspond à une de vos classes qu'on appelle *entité*.
|
|
|
|
|
Le lien entre les tables et les entités est géré plus ou moins automatiquement par le *framework*.
|
|
|
|
|
**EF Core** permet de lire/écrire les instances d'entité de la base de données ; permet de créer des tables pour les entités via les migrations (pour les bases de données relationnelles) ;
|
|
|
|
|
les types exposés dans les propriétés d'une entité deviennent automatiquement des entités (mais pas forcément des tables)
|
|
|
|
|
|
|
|
|
|
**Entity Framework Core** propose 3 solutions différentes pour relier des entités à des tables de la base de données :
|
|
|
|
|
* **conventions d'écriture** : c'est la solution la plus simple, qui analyse le code de votre classe pour en déduire de quelle manière la relier à la table.
|
|
|
|
|
* **data annotations** : elle se base sur des décorateurs (appelés *data annotations*) que vous placez autour de vos propriétés pour indiquer de quelle manière les relier aux colonnes de votre table. Les *data annotations* écrasent les conventions d'écriture.
|
|
|
|
|
* **Fluent API** : directement dans le code de votre ```DbContext``` (plus précisément, dans la méthode ```OnModelCreating```), vous précisez comment se fait le *mapping* entre votre entité et votre table. La *Fluent API* écrase les conventions d'écriture et les *data annotations*.
|
|
|
|
|
|
|
|
|
|
En d'autres termes, si vous n'écrivez rien, **EF Core** utilise les conventions d'écriture ; si vous n'utilisez que des *data annotations*, elles sont prioritaires sur les convetions d'écriture ; si vous utilisez la *Fluent API*, elle est prioritaire sur les deux autres méthodes.
|
|
|
|
|
Mais on peut faire un mix des différentes méthodes.
|
|
|
|
|
|
|
|
|
|
Cet exemple montre de quelle manière les **data annotations** sont utilisées pour transformer une entité en table.
|
|
|
|
|
Il montre notamment :
|
|
|
|
|
* comment choisir le nom de la table
|
|
|
|
|
* comment ignorer une propriété de l'entité
|
|
|
|
|
* comment le nom et le type d'une colonne de la table peuvent être modifiés
|
|
|
|
|
* comment un identifiant unique est choisi et généré.
|
|
|
|
|
|
|
|
|
|
Cet exemple est répété d'une manière très similaire en utilisant les autres méthodes de *mapping* entre entité et table :
|
|
|
|
|
* **ex_042_001_EF_CF_conventions** : avec les *conventions d'écriture*
|
|
|
|
|
* **ex_042_003_EF_CF_Fluent_API** : avec la *Fluent API*
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Comment est construit cet exemple ?
|
|
|
|
|
* Le projet est de type .NET Core
|
|
|
|
|
* Il contient deux classes :
|
|
|
|
|
* ```Nounours```
|
|
|
|
|
* ```NounoursDBEntities```
|
|
|
|
|
|
|
|
|
|
### La classe ```NounoursDBEntities```
|
|
|
|
|
* Cette classe dérive de ```DbContext```. Elle contient des ```DbSet<T>``` où ```T``` est une entité. Elle contient autant de ```DbSet<T>``` que de tables.
|
|
|
|
|
```csharp
|
|
|
|
|
class NounoursDBEntities : DbContext
|
|
|
|
|
{
|
|
|
|
|
public virtual DbSet<Nounours> NounoursSet { get; set; }
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
ici, on indique donc qu'il y aura une table de ```Nounours```.
|
|
|
|
|
* **EF Core** utilisera cette classe pour faire la création de la base de données et de ses tables. Pour cela, elle utilise la chaîne de connexion donnée dans la méthode ```OnConfiguring```.
|
|
|
|
|
```csharp
|
|
|
|
|
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
|
|
|
|
{
|
|
|
|
|
optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=ex_042_001_EF_CF_conventions.Nounours.mdf;Trusted_Connection=True;");
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
=> cf **ex_041_001_ConnectionStrings** pour en savoir plus sur les chaîne de connexion
|
|
|
|
|
* Dans cet exemple, la table de ```Nounours``` est créée en utilisant les *data annotations* et les conventions d'écriture de la classe ```Nounours```.
|
|
|
|
|
|
|
|
|
|
### La classe ```Nounours```
|
|
|
|
|
* ```Nounours``` est une entité, on parle aussi de classe POCO, i.e. Plain Old CLR Object.
|
|
|
|
|
```csharp
|
|
|
|
|
[Table("TableNounours")]
|
|
|
|
|
public class Nounours
|
|
|
|
|
{
|
|
|
|
|
[Key]
|
|
|
|
|
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
|
|
|
|
public Guid UniqueId
|
|
|
|
|
{
|
|
|
|
|
get; set;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Required]
|
|
|
|
|
[MaxLength(256)]
|
|
|
|
|
//[Column("name", Order=0, TypeName ="varchar(200)")]
|
|
|
|
|
public string Nom
|
|
|
|
|
{
|
|
|
|
|
get;
|
|
|
|
|
set;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Column("Naissance", TypeName = "date")]
|
|
|
|
|
public DateTime DateDeNaissance
|
|
|
|
|
{
|
|
|
|
|
get;
|
|
|
|
|
set;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[NotMapped]
|
|
|
|
|
public int NbPoils
|
|
|
|
|
{
|
|
|
|
|
get;
|
|
|
|
|
set;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public override int GetHashCode()
|
|
|
|
|
{
|
|
|
|
|
return Nom.GetHashCode();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public override bool Equals(object right)
|
|
|
|
|
{
|
|
|
|
|
//check null
|
|
|
|
|
if (object.ReferenceEquals(right, null))
|
|
|
|
|
{
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (object.ReferenceEquals(this, right))
|
|
|
|
|
{
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (this.GetType() != right.GetType())
|
|
|
|
|
{
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return this.Equals(right as Nounours);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public bool Equals(Nounours other)
|
|
|
|
|
{
|
|
|
|
|
return (this.Nom.Equals(other.Nom) && this.DateDeNaissance == other.DateDeNaissance);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public override string ToString()
|
|
|
|
|
{
|
|
|
|
|
return $"{UniqueId}: {Nom} ({DateDeNaissance:dd/MM/yyyy}, {NbPoils} poils)";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
* Elle contient 3 propriétés en lecture/écriture : ```Nom```, ```DateDeNaissance``` et ```NbPoils```. Nous parlerons de la 4ème (```UniqueId```) dans un moment.
|
|
|
|
|
* **Entity Framework Core** va utiliser la classe POCO ```Nounours``` pour créer une table dans la base de données, lorsque le ```DbSet``` va être créé.
|
|
|
|
|
* S'il y a des *data annotations*, elles sont prioritaires sur l'utilisation des conventions d'écriture d'Entity Framework.
|
|
|
|
|
|
|
|
|
|
Un petit tour des annotations utilisées dans l'exemple et quelques indications sur celles manquantes :
|
|
|
|
|
* la classe ```Nounours``` est précédée de l'annotation ```[Table("TableNounours")]``` : la table créée n'aura donc pas le nom de la classe (comme le permettent les conventions d'écriture), mais le nom indiqué dans l'annotation :
|
|
|
|
|
```csharp
|
|
|
|
|
[Table("TableNounours")]
|
|
|
|
|
public class Nounours
|
|
|
|
|
{
|
|
|
|
|
//...
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
* on peut changer le nom, le type ou l'ordre d'une colonne en précédant une propriété de l'annotation ```[Column(...)]```. Dans l'exemple ci-dessous,
|
|
|
|
|
la colonne ne s'appellera pas "DateDeNaissance" (comme le permettraient les conventions d'écriture), mais "Naissance" grâce à l'annotation.
|
|
|
|
|
```csharp
|
|
|
|
|
[Column("Naissance")]
|
|
|
|
|
public DateTime DateDeNaissance
|
|
|
|
|
{
|
|
|
|
|
get;
|
|
|
|
|
set;
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
* On pourrait aller plus loin en indiquant l'ordre et le changement de type, par exemple :
|
|
|
|
|
```csharp
|
|
|
|
|
[Column("name", Order=0, TypeName=varchar(200))]
|
|
|
|
|
public string Nom
|
|
|
|
|
{
|
|
|
|
|
get;
|
|
|
|
|
set;
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
* Dans la classe ```Nounours```, la propriété ```DateDeNaissance``` est annotée de façon à modifier le type par défaut de *mapping* (la partie *time* sera évincée) et le nom de colonne sera modifé en "Naissance".
|
|
|
|
|
```csharp
|
|
|
|
|
[Column("Naissance", TypeName = "date")]
|
|
|
|
|
public DateTime DateDeNaissance
|
|
|
|
|
{
|
|
|
|
|
get;
|
|
|
|
|
set;
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
* L'annotation ```[Required]``` permet de rendre obligatoire une propriété. Une propriété peut être optionnelle seulement si elle peut avoir la valeur ```null```. Dans l'exemple ci-dessous, le nom est obligatoire.
|
|
|
|
|
```csharp
|
|
|
|
|
[Required]
|
|
|
|
|
public string Nom
|
|
|
|
|
{
|
|
|
|
|
get;
|
|
|
|
|
set;
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
* L'annotation ```[MaxLength(256)]``` permet d'imposer une taille fixe à une chaîne de caractères. Dans l'exemple ci-dessous, le nom ne doit pas avoir plus de 256 caractères.
|
|
|
|
|
```csharp
|
|
|
|
|
[MaxLength(256)]
|
|
|
|
|
public string Nom
|
|
|
|
|
{
|
|
|
|
|
get;
|
|
|
|
|
set;
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
* L'annotation ```[NotMapped]``` permet d'indiquer qu'une propriété ne doit pas être mappée en colonne de table. Par exemple, ici, le nombre de poils ne sera pas mappé en colonne.
|
|
|
|
|
```csharp
|
|
|
|
|
[NotMapped]
|
|
|
|
|
public int NbPoils
|
|
|
|
|
{
|
|
|
|
|
get;
|
|
|
|
|
set;
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
* L'annotation ```[Key]``` transforme n'importe quelle propriété en clé primaire.
|
|
|
|
|
Si on ajoute l'annotation ```[DatabaseGenerated(DatabaseGeneratedOption.Identity)]```, on précise par exemple que la clé primaire sera générée par la base, lors de l'insertation dans la table.
|
|
|
|
|
Notez que la clé n'est donc, contrairement aux conventions d'écriture, pas nécessairement un ```int```. Elle peut-être un ```Guid```, un ```string```, etc. Son nom peut-être autre chose que "ID".
|
|
|
|
|
*Note : on peut aussi faire des clés composées, comme je le montrerai dans un autre exemple.*
|
|
|
|
|
Dans l'exemple ci-dessous, un ```Nounours```possédera une clé unique de type ```Guid``` générée par la base et placée dans la colonne "UniqueId".
|
|
|
|
|
```csharp
|
|
|
|
|
[Key]
|
|
|
|
|
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
|
|
|
|
public Guid UniqueId
|
|
|
|
|
{
|
|
|
|
|
get; set;
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**En résumé**, avec les *data annotations* :
|
|
|
|
|
* on peut changer le nom de la table
|
|
|
|
|
* on peut changer le nom, le type et l'ordre d'une colonne
|
|
|
|
|
* on peut rendre une propriété obligatoire ou optionnelle
|
|
|
|
|
* on peut empêcher le *mapping* d'une propriété
|
|
|
|
|
* on peut imposer une taille max pour les chaînes de caractères
|
|
|
|
|
* on peut transformer une propriété en clé primaire et demander à la base de la générée lors de l'insertion.
|
|
|
|
|
|
|
|
|
|
### La classe ```Program```
|
|
|
|
|
Cette classe est le point d'entrée du programme :
|
|
|
|
|
* Elle crée des instances de ```Nounours```
|
|
|
|
|
```csharp
|
|
|
|
|
Nounours chewie = new Nounours { Nom = "Chewbacca", DateDeNaissance = new DateTime(1977, 5, 27), NbPoils = 1234567 };
|
|
|
|
|
Nounours yoda = new Nounours { Nom = "Yoda", DateDeNaissance = new DateTime(1980, 5, 21), NbPoils = 3 };
|
|
|
|
|
Nounours ewok = new Nounours { Nom = "Ewok", DateDeNaissance = new DateTime(1983, 5, 25), NbPoils = 3456789 };
|
|
|
|
|
```
|
|
|
|
|
* Elle démarre une connexion à la base de données
|
|
|
|
|
```csharp
|
|
|
|
|
using (NounoursDBEntities db = new NounoursDBEntities())
|
|
|
|
|
{
|
|
|
|
|
//...
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
* Elle vérifie si la table est vide, et si ce n'est pas le cas, elle la vide.
|
|
|
|
|
Notez l'accès à la table de ```Nounours``` via ```db.NounoursSet```.
|
|
|
|
|
Notez également que tant que ```SaveChanges``` n'est pas appelée, les suppressions ne sont pas effectives dans la base, seulement en local dans le programme.
|
|
|
|
|
_Cette partie de l'exemple ne s'exécutera que si la base existe déjà, par exemple lors d'une deuxième exécution._
|
|
|
|
|
```csharp
|
|
|
|
|
if (db.NounoursSet.Count() > 0)
|
|
|
|
|
{
|
|
|
|
|
WriteLine("La base n'est pas vide !");
|
|
|
|
|
foreach (var n in db.NounoursSet)
|
|
|
|
|
{
|
|
|
|
|
WriteLine($"\t{n}");
|
|
|
|
|
}
|
|
|
|
|
WriteLine("début du nettoyage...");
|
|
|
|
|
|
|
|
|
|
foreach (var n in db.NounoursSet.ToArray())
|
|
|
|
|
{
|
|
|
|
|
WriteLine($"Suppression de {n}");
|
|
|
|
|
db.NounoursSet.Remove(n);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
WriteLine("Base avant sauvegarde des changements :");
|
|
|
|
|
foreach (var n in db.NounoursSet)
|
|
|
|
|
{
|
|
|
|
|
WriteLine($"\t{n}");
|
|
|
|
|
}
|
|
|
|
|
db.SaveChanges();
|
|
|
|
|
WriteLine("Base après sauvegarde des changements :");
|
|
|
|
|
foreach (var n in db.NounoursSet)
|
|
|
|
|
{
|
|
|
|
|
WriteLine($"\t{n}");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
* Elle ajoute ensuite les ```Nounours```et sauvegarde les changements pour que ceux-ci soit effectivement ajoutés à la base.
|
|
|
|
|
```csharp
|
|
|
|
|
db.NounoursSet.AddRange(new Nounours[] { chewie, yoda, ewok });
|
|
|
|
|
|
|
|
|
|
db.SaveChanges();
|
|
|
|
|
WriteLine("Base après ajout des 3 nounours et sauvegarde des changements :");
|
|
|
|
|
foreach (var n in db.NounoursSet)
|
|
|
|
|
{
|
|
|
|
|
WriteLine($"\t{n}");
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Comment exécuter cet exemple ?
|
|
|
|
|
Pour tester cette application, n'oubliez pas les commandes comme présentées dans l'exemple ex_041_001 : pour générer l'exemple, il vous faut d'abord préparer les migrations et les tables.
|
|
|
|
|
* Ouvrez la *Console du Gestionnaire de package*, pour cela, dirigez-vous dans le menu *Outils*, puis *Gestionnaire de package NuGet*, puis *Console du Gestionnaire de package*.
|
|
|
|
|
* Dans la console que vous venez d'ouvrir, déplacez-vous dans le dossier du projet .NET Core, ici :
|
|
|
|
|
```
|
|
|
|
|
cd .\p08_BDD_EntityFramework\ex_042_002_EF_CF_data_annotations
|
|
|
|
|
```
|
|
|
|
|
*Note*:
|
|
|
|
|
si vous n'avez pas installé correctement EntityFrameworkCore, il vous faudra peut-être utiliser également :
|
|
|
|
|
|
|
|
|
|
```dotnet tool install --global dotnet-ef```
|
|
|
|
|
|
|
|
|
|
* Migration :
|
|
|
|
|
```
|
|
|
|
|
dotnet ef migrations add migration_ex_042_002
|
|
|
|
|
```
|
|
|
|
|
* Création de la table :
|
|
|
|
|
```
|
|
|
|
|
dotnet ef database update
|
|
|
|
|
```
|
|
|
|
|
* Génération et exécution
|
|
|
|
|
Vous pouvez maintenant générer et exécuter l'exemple **ex_042_002_EF_CF_data_annotations**.
|
|
|
|
|
|
|
|
|
|
* Comment vérifier le contenu des bases de données SQL Server et SQLite ?
|
|
|
|
|
Vous pouvez vérifier le contenu de votre base en utilisant l'*Explorateur d'objets SQL Server*.
|
|
|
|
|
* Pour cela, allez dans le menu *Affichage* puis *Explorateur d'objets SQL Server*.
|
|
|
|
|
<img src="../ex_041_001_ConnectionStrings/readmefiles/sqlserver_01.png" width="500"/>
|
|
|
|
|
|
|
|
|
|
* Déployez dans l'*Explorateur d'objets SQL Server* :
|
|
|
|
|
* *SQL Server*,
|
|
|
|
|
* puis *(localdb)\MSSQLLocalDB ...*,
|
|
|
|
|
* puis *Bases de données*
|
|
|
|
|
* puis celle portant le nom de votre migration, dans mon cas : *ex_042_002_EF_CF_data_annotations.Nounours.mdf*
|
|
|
|
|
* puis *Tables*
|
|
|
|
|
* Faites un clic droit sur la table *dbo.Nounours* puis choisissez *Afficher les données*
|
|
|
|
|
<img src="../ex_041_001_ConnectionStrings/readmefiles/sqlserver_02.png" width="460"/>
|
|
|
|
|
|
|
|
|
|
* Vous devriez maintenant pouvoir voir les données suivantes dans le tableau :
|
|
|
|
|
|
|
|
|
|
|UniqueId |Nom|Naissance
|
|
|
|
|
|---|---|---
|
|
|
|
|
|fbc3f7c5-a333-4d07-f7f2-08d7907e5437|Chewbacca|27/05/1977
|
|
|
|
|
|63b18e10-a683-4144-f7f3-08d7907e5437|Yoda|21/05/1980
|
|
|
|
|
|c4eab29b-315b-416e-f7f4-08d7907e5437|Ewok|25/05/1983
|
|
|
|
|
|
|
|
|
|
*Notes: les identifiants seront bien sûr différents.*
|
|
|
|
|
*Notez l'absence de la colonne "NbPoils"*
|
|
|
|
|
*Notez le nom de la colonne "Naissance" et le formatage de la date*
|