You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
278 lines
15 KiB
278 lines
15 KiB
# ex_042_013_OneToOne_FluentAPI
|
|
|
|
*20/01/2020 ⋅ Marc Chevaldonné*
|
|
|
|
---
|
|
|
|
Cet exemple montre comment réaliser une relation *One To One* entre deux entités avec *Entity Framework Core* et la *Fluent API*.
|
|
Une version équivalente réalisée avec les *annotations de données* est disponible dans l'exemple
|
|
[**ex_042_012 : One To One with data annotations**](../ex_042_012_OneToOne_conventions)
|
|
|
|
---
|
|
|
|
## Comment est construit cet exemple ?
|
|
* Le projet est de type .NET Core
|
|
* Il contient quatre classes :
|
|
* ```Nounours```
|
|
* ```CarnetDeSante```
|
|
* ```NounoursDBEntities```
|
|
* ```StubbedContext```
|
|
|
|
### Les classes entités : ```Nounours``` et ```CarnetDeSante```
|
|
|
|
Un ```Nounours``` contient différentes propriétés (```Nom```, ```DateDeNaissance```, ```NbPoils```) dont ```Carnet``` de type ```CarnetDeSante```.
|
|
Un ```CarnetDeSante``` possède une propriété de type ```DateTime``` (```LastModified```), et une propriété ```Owner``` de type ```Nounours```.
|
|
On a donc bien une relation *One To One* puisqu'un ```Nounours``` possède un ```CarnetDeSante``` et qu'un ```CarnetDeSante``` possède un ```Nounours```.
|
|
<img src="../ex_042_012_OneToOne_conventions/readme_files/ex_042_012_classDiagram.svg"/>
|
|
|
|
Ce qu'il faut noter :
|
|
* ```Nounours``` possède une association vers ```CarnetDeSante```
|
|
```csharp
|
|
public CarnetDeSante Carnet { get; set; }
|
|
```
|
|
* ```CarnetDeSante``` possède une association vers ```Nounours```
|
|
```csharp
|
|
public Nounours Owner { get; set; }
|
|
```
|
|
* ```Nounours``` possède un identifiant unique ```UniqueId```.
|
|
* ```CarnetDeSante``` possède un identifiant unique ```UniqueId```.
|
|
|
|
|
|
### La classe ```NounoursDBEntities```
|
|
|
|
* Comme dans les exemples précédents, ```NounoursDBEntities``` dérive de ```DbContext```.
|
|
* ```NounoursDBEntities``` déclare deux ```DbSet``` : un de ```Nounours``` et l'autre de ```CarnetDeSante```.
|
|
```csharp
|
|
public DbSet<Nounours> NounoursSet { get; set; }
|
|
public DbSet<CarnetDeSante> Carnets { get; set; }
|
|
```
|
|
* La classe réécrit ensuite la méthode ```OnModelCreating``` :
|
|
```csharp
|
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
|
{
|
|
//création de la table TableNounours
|
|
modelBuilder.Entity<Nounours>().ToTable("TableNounours"); //nom de la table
|
|
modelBuilder.Entity<Nounours>().HasKey(n => n.UniqueId); //définition de la clé primaire
|
|
modelBuilder.Entity<Nounours>().Property(n => n.UniqueId)
|
|
.ValueGeneratedOnAdd(); //définition du mode de génération de la clé : génération à l'insertion
|
|
modelBuilder.Entity<Nounours>().Property(n => n.Nom).IsRequired()
|
|
.HasMaxLength(256); //définition de la colonne Nom
|
|
modelBuilder.Entity<Nounours>().Property(n => n.DateDeNaissance).HasColumnName("Naissance").HasColumnType("date"); //changement du nom de la colonne Naissance
|
|
|
|
//création de la table "Carnets"
|
|
modelBuilder.Entity<CarnetDeSante>().ToTable("Carnets"); // nom de la table
|
|
modelBuilder.Entity<CarnetDeSante>().HasKey(c => c.UniqueId); //définition de la clé primaire
|
|
modelBuilder.Entity<CarnetDeSante>().Property(c => c.UniqueId)
|
|
.ValueGeneratedNever(); // définition du mode de génération de la clé : pas de génération automatique
|
|
//note : la colonne LastModified n'est pas touchée : utilisation des conventions EF
|
|
|
|
|
|
//on précise qu'il y a une relation entre CarnetDeSante et Nounours
|
|
modelBuilder.Entity<Nounours>() //l'entité Nounours...
|
|
.HasOne(n => n.Carnet) //a une propriété obligatoire Carnet...
|
|
.WithOne(c => c.Owner) //reliée à la propriété Owner du Carnet...
|
|
.HasForeignKey<CarnetDeSante>(c => c.UniqueId);//dont la propriété UniqueId est une Foreign Key
|
|
//remplace la ForeignKey
|
|
|
|
base.OnModelCreating(modelBuilder);
|
|
}
|
|
```
|
|
Voyons cette méthode plus en détails.
|
|
Tout d'abord, il y a la définition des détails de la table de ```Nounours``` :
|
|
* le changement de nom de la table :
|
|
```csharp
|
|
modelBuilder.Entity<Nounours>().ToTable("TableNounours");
|
|
```
|
|
* la définition de la clé primaire :
|
|
```csharp
|
|
modelBuilder.Entity<Nounours>().HasKey(n => n.UniqueId);
|
|
modelBuilder.Entity<Nounours>().Property(n => n.UniqueId)
|
|
.ValueGeneratedOnAdd();
|
|
```
|
|
* définition de contraintes sur les colonnes de ```Nounours``` :
|
|
```csharp
|
|
modelBuilder.Entity<Nounours>().Property(n => n.Nom)
|
|
.IsRequired()
|
|
.HasMaxLength(256);
|
|
modelBuilder.Entity<Nounours>().Property(n => n.DateDeNaissance)
|
|
.HasColumnName("Naissance")
|
|
.HasColumnType("date");
|
|
```
|
|
On continue avec la définition des détails de la table de ```CarnetDeSante``` :
|
|
* le changement de nom de la table :
|
|
```csharp
|
|
modelBuilder.Entity<CarnetDeSante>().ToTable("Carnets");
|
|
```
|
|
* la définition de la clé primaire (*notez qu'on ne demande pas ici à la base de générer la clé, puisqu'on va utiliser une clé étrangère*) :
|
|
```csharp
|
|
modelBuilder.Entity<CarnetDeSante>().HasKey(c => c.UniqueId);
|
|
modelBuilder.Entity<CarnetDeSante>().Property(c => c.UniqueId)
|
|
.ValueGeneratedNever();
|
|
```
|
|
On s'intéresse enfin à la **relation _One To One_**, où l'on précise qu'une entité ```Nounours``` possède une association vers l'entité de type ```CarnetDeSante``` grâce à la propriété ```Carnet``` (```.HasOne(n => n.Carnet)```).
|
|
Cette entité ```CarnetDeSante``` possède elle-même une association vers une entité de type ```Nounours``` grâce à sa propriété ```Owner``` (```.WithOne(c => c.Owner)```),
|
|
et on précise que cette entité ```CarnetDeSante``` voit sa propriété ```UniqueId``` utilisée comme clé étrangère (```.HasForeignKey<CarnetDeSante>(c => c.UniqueId)```). Elle automatiquement reliée à la clé primaire de ```Nounours```.
|
|
```csharp
|
|
modelBuilder.Entity<Nounours>() //l'entité Nounours...
|
|
.HasOne(n => n.Carnet) //a une propriété obligatoire Carnet...
|
|
.WithOne(c => c.Owner) //reliée à la propriété Owner du Carnet...
|
|
.HasForeignKey<CarnetDeSante>(c => c.UniqueId);//dont la propriété UniqueId est une Foreign Key
|
|
```
|
|
Pour pouvoir relier une entité ```Nounours``` à une entité ```CarnetDeSante``` de manière bidirectionnelle, les propriétés ```Nounours.Carnet``` et ```CarnetDeSante.Owner``` sont suffisantes, à condition qu'une clé étrangère soit déclarée et utilisée.
|
|
C'est le cas grâce au code précédent qui s'occupe de définir de quelle manière les deux entités sont reliées.
|
|
|
|
### La classe ```StubbedContext```
|
|
|
|
* ```StubbedContext``` est une classe fille de ```NounoursDBEntities```.
|
|
Son rôle est de proposer un Stub en plus de ce que sait déjà faire sa classe mère. Elle ne sera donc utilisée que pour des tests unitaires ou fonctionnels.
|
|
En conséquence, elle reprend tout ce que fait sa classe mère et ne change que la méthode ```OnModelCreating``` qui appelle la méthode de la classe mère puis ajoute des instances d'entités, grâce à la méthode d'extension ```HasData```.
|
|
```csharp
|
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
|
{
|
|
base.OnModelCreating(modelBuilder);
|
|
|
|
modelBuilder.Entity<CarnetDeSante>().HasData(
|
|
new CarnetDeSante { UniqueId=1, LastModified = DateTime.Today },
|
|
new CarnetDeSante { UniqueId=2, LastModified = new DateTime(1980, 5, 21) },
|
|
new CarnetDeSante { UniqueId=3, LastModified = new DateTime(1983, 5, 25) }
|
|
);
|
|
|
|
modelBuilder.Entity<Nounours>().HasData(
|
|
new Nounours { UniqueId=1, Nom = "Chewbacca", DateDeNaissance = new DateTime(1977, 5, 27), NbPoils = 1234567 },
|
|
new Nounours { UniqueId=2, Nom = "Yoda", DateDeNaissance = new DateTime(1980, 5, 21), NbPoils = 3 },
|
|
new Nounours { UniqueId=3, Nom = "Ewok", DateDeNaissance = new DateTime(1983, 5, 25), NbPoils = 3456789 }
|
|
);
|
|
}
|
|
```
|
|
* Remarquez que __À AUCUN MOMENT__ nous ne précisons les valeurs des propriétés ```Owner``` de ```CarnetDeSante``` et ```Carnet``` de ```Nounours```.
|
|
Le simple fait d'utiliser la même clé (propriétés ```UniqueId```) est suffisant puisque celle de ```CarnetDeSante``` est une clé étrangère.
|
|
* Notez que ce ne sera pas le cas lors d'une utilisation *classique* de nos classes (ajout, modification...). Nous ne donnerons plus les identifiants directement mais les références des propriétés ```Carnet``` et ```Owner```.
|
|
|
|
### La classe ```Program```
|
|
* La classe ```StubbedContext``` est ensuite indirectement utilisée dans ```Program``` pour remplir la base de manière tout à fait classique et ne nécessite aucun appel supplémentaire : ceci est fait lors de la migration et la création de la base.
|
|
* On affiche tout d'abord le contenu de la base (c'est-à-dire rien ou d'anciennes données si la migration est faite à partir de ```NounoursDBEntites```) ou le stub (si la migration est faite à partir de ```StubbedContext```).
|
|
*Notez l'utilisation d'```Include``` dans ```db.NounoursSet.Include(n => n.Carnet)``` sinon, les ```CarnetDeSante``` ne sont pas chargés. ```Include``` n'est pas utilisé ensuite dans ```db.Carnets``` car les ```Nounours``` ont déjà été chargés depuis la connexion. Mais on aurait pu faire les accès dans l'autre sens et dans ce cas d'abord ```db.Carnets.Include(c => c.Owner)``` puis simplement ```db.NounoursSet```.*
|
|
```csharp
|
|
using (NounoursDBEntities db = new NounoursDBEntities())
|
|
{
|
|
WriteLine("Contenu de la base (nounours) : ");
|
|
foreach (var n in db.NounoursSet.Include(n => n.Carnet))
|
|
{
|
|
WriteLine($"\t{n}, LastModified: {n.Carnet.LastModified.ToString("d")}");
|
|
}
|
|
|
|
WriteLine("Contenu de la base (carnets de santé) : ");
|
|
foreach (var c in db.Carnets)
|
|
{
|
|
WriteLine($"\t{c}");
|
|
}
|
|
|
|
//...
|
|
}
|
|
```
|
|
* La suite de l'exemple ajoute un nouveau ```Nounours``` et son ```CarnetDeSante``` puis affiche le contenu de la base de données.
|
|
```csharp
|
|
using (NounoursDBEntities db = new NounoursDBEntities())
|
|
{
|
|
//...
|
|
|
|
WriteLine("\nAjout d'un nounours et de son carnet de santé\n");
|
|
|
|
Nounours porg = new Nounours { Nom = "Porg", DateDeNaissance = new DateTime(2017, 7, 19), NbPoils = 123 };
|
|
CarnetDeSante carnetPorg = new CarnetDeSante { LastModified = DateTime.Now, Owner = porg };
|
|
porg.Carnet = carnetPorg;
|
|
|
|
db.AddRange(porg, carnetPorg);
|
|
|
|
db.SaveChanges();
|
|
}
|
|
```
|
|
```csharp
|
|
using (NounoursDBEntities db = new NounoursDBEntities())
|
|
{
|
|
WriteLine("Contenu de la base (nounours) : ");
|
|
foreach (var n in db.NounoursSet.Include(n => n.Carnet))
|
|
{
|
|
WriteLine($"\t{n}, LastModified: {n.Carnet.LastModified.ToString("d")}");
|
|
}
|
|
|
|
WriteLine("Contenu de la base (carnets de santé) : ");
|
|
foreach (var c in db.Carnets)
|
|
{
|
|
WriteLine($"\t{c}");
|
|
}
|
|
}
|
|
```
|
|
## 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* sous Windows ou le *Terminal* sous MacOSX, 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_013_OneToOne_FluentAPI
|
|
```
|
|
*Note*:
|
|
si vous n'avez pas installé correctement EntityFrameworkCore, il vous faudra peut-être utiliser également :
|
|
|
|
* ```dotnet tool install --global dotnet-ef``` si vous utilisez la dernière version de .NET Core (3.1 aujourd'hui),
|
|
|
|
* ```dotnet tool install --global dotnet-ef --version 3.0.0``` si vous vous utiliser spécifiquement .NET Core 3.0.
|
|
|
|
|
|
* Migration :
|
|
```
|
|
dotnet ef migrations add ex_042_013 --context StubbedContext
|
|
```
|
|
* Création de la table :
|
|
```
|
|
dotnet ef database update --context StubbedContext
|
|
```
|
|
* Génération et exécution
|
|
Vous pouvez maintenant générer et exécuter l'exemple **ex_042_013_OneToOne_FluentAPI**.
|
|
|
|
* Le résultat de l'exécution va ressembler à :
|
|
```
|
|
Contenu de la base (nounours) :
|
|
1: Chewbacca (27/05/1977, 1234567 poils), LastModified: 20/01/2020
|
|
2: Yoda (21/05/1980, 3 poils), LastModified: 21/05/1980
|
|
3: Ewok (25/05/1983, 3456789 poils), LastModified: 25/05/1983
|
|
Contenu de la base (carnets de santé) :
|
|
1 : carnet de Chewbacca, modifié la dernière fois le 20/01/2020
|
|
2 : carnet de Yoda, modifié la dernière fois le 21/05/1980
|
|
3 : carnet de Ewok, modifié la dernière fois le 25/05/1983
|
|
|
|
Ajout d'un nounours et de son carnet de santé
|
|
|
|
Contenu de la base (nounours) :
|
|
1: Chewbacca (27/05/1977, 1234567 poils), LastModified: 20/01/2020
|
|
2: Yoda (21/05/1980, 3 poils), LastModified: 21/05/1980
|
|
3: Ewok (25/05/1983, 3456789 poils), LastModified: 25/05/1983
|
|
4: Porg (19/07/2017, 123 poils), LastModified: 20/01/2020
|
|
Contenu de la base (carnets de santé) :
|
|
1 : carnet de Chewbacca, modifié la dernière fois le 20/01/2020
|
|
2 : carnet de Yoda, modifié la dernière fois le 21/05/1980
|
|
3 : carnet de Ewok, modifié la dernière fois le 25/05/1983
|
|
4 : carnet de Porg, modifié la dernière fois le 20/01/2020
|
|
```
|
|
*Note : l'identifiant du dernier ```Nounours``` sera vraisemblablement différent puisqu'il est créé par la base lors de l'insertion.*
|
|
|
|
## Comment exécuter cet exemple sans le stub ?
|
|
Il suffit de faire exactement comme dans le paragraphe précédent, mais en choisissant le contexte ```NounoursDBEntities``` à la place de ```StubbedContext``` :
|
|
```
|
|
dotnet ef migrations add ex_042_013 --context NounoursDBEntities
|
|
dotnet ef database update --context NounoursDBEntities
|
|
```
|
|
Lors de l'exécution, le résultat sera évidemment différent puisqu'il n'y aura pas les 3 nounours du stub.
|
|
|
|
## Comment vérifier quelles base et tables ont été créées et leur contenu ?
|
|
Pour vérifier le contenu de votre base SQLite, vous pouvez utiliser le programme *DB Browser* :
|
|
* Rendez-vous sur la page : https://sqlitebrowser.org/dl/ et téléchargez le programme *DB Browser*.
|
|
* Lancez *DB Browser for SQLite*
|
|
* Glissez-déposez au milieu de la fenêtre de *DB Browser for SQLite* le fichier *ex_042_013_OneToOne_FluentAPI.Nounours.db* qui a été généré par l'exécution du programme et qui se trouve près de *ex_042_012_OneToOne_conventions.csproj*.
|
|
![DB Browser for SQLite](../ex_042_012_OneToOne_conventions/readme_files/dbbrowser01.png)
|
|
* Choisissez ensuite l'onglet *Parcourir les données*
|
|
* Observez les résultats obtenus des deux tables
|
|
![DB Browser for SQLite](../ex_042_012_OneToOne_conventions/readme_files/dbbrowser02.png)
|
|
![DB Browser for SQLite](../ex_042_012_OneToOne_conventions/readme_files/dbbrowser03.png)
|
|
|
|
|