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.

401 lines
17 KiB

5 years ago
# ex_042_016_OneToMany_FluentAPI
*25/01/2020 ⋅ Marc Chevaldonné*
---
Cet exemple montre comment réaliser une relation *One To Many* entre deux entités
avec *Entity Framework Core* et la *Fluent API*.
Une version équivalente réalisée avec les *annotations de données* et
l'établissement d'une clé étrangère de manière explicite est disponible dans
l'exemple
[**ex_042_014 : One To Many with data annotations**](../ex_042_015_OneToMany_dataAnnotations)
Une version équivalente réalisée avec les *conventions d'écriture* et
l'établissement d'une clé étrangère de manière implicite est disponible dans
l'exemple
[**ex_042_015 : One To Many with conventions**](../ex_042_015_OneToMany_conventions)
---
## Comment est construit cet exemple ?
* Le projet est de type .NET Core
* Il contient quatre classes :
* ```Album```
* ```Morceau```
* ```AlbumDBEntities```
* ```StubbedContext```
### Les classes entités : ```Album``` et ```Morceau```
La relation **One To Many** est mise en évidence de la manière suivante :
un ```Album``` possède plusieurs ```Morceau```, et un ```Morceau``` possède
un ```Album```.
Un ```Album``` contient différentes propriétés (```Titre```, ```DateDeSortie```)
dont ```Morceaux``` de type ```ICollection<Morceau>```.
Un ```Morceau``` possède une propriété de type ```string``` (```Titre```), et
une propriété ```Album``` de type ```Album```.
<img src="../ex_042_014_OneToMany_dataAnnotations/readme_files/ex_042_014_classDiagram.svg"/>
Ce qu'il faut noter :
* ```Album``` possède une association vers ```Morceau```
```csharp
public ICollection<Morceau> Morceaux { get; set; } = new List<Morceau>();
```
* ```Morceau``` possède une association vers ```Album```
```csharp
public Album Album { get; set; }
```
* ```Album``` possède un identifiant unique ```AlbumId``` (qui sera la clé
* primaire et sera généré par la base lors de l'insertion, cf. ```AlbumDBEntities```).
* ```Morceau``` possède un identifiant unique ```MorceauId``` (qui sera la clé
* primaire et sera généré par la base lors de l'insertion, cf. ```AlbumDBEntities```).
* Le lien entre les deux tables va se faire avec l'ajout d'une colonne dans la table
de ```Morceau``` qui permettra de stocker l'```Album``` auquel ce ```Morceau```
est lié.
Dans le cas de l'utilisation de la *Fluent API*, ceci peut se faire dans la classe
qui dérive de ```DbContext```. Mais il est possible de le faire de manière explicite
en utilisant les *annotations de données* (cf. [ex_042_014 : One To Many with data annotations](../ex_042_014_OneToMany_dataAnnotations))
ou de manière implicite en utilisant les *conventions d'écriture*
(cf. [ex_042_015 : One To Many with conventions](../ex_042_016_OneToMany_conventions)).
5 years ago
Ici, on __N'__ ajoute __PAS__ de nouvelle propriété à ```Morceau``` pour stocker
l'identifiant unique de ```Album``` comme dans l'exemple [ex_042_014 : One To Many with data annotations](../ex_042_014_OneToMany_dataAnnotations),
mais on pourrait le faire (cf. commentaires plus bas).
### La classe ```AlbumDBEntities```
* Comme dans les exemples précédents, ```AlbumDBEntities``` dérive de ```DbContext```.
* ```AlbumDBEntities``` déclare deux ```DbSet``` : un de ```Album``` et
l'autre de ```Morceau```.
```csharp
public DbSet<Album> Albums { get; set; }
public DbSet<Morceau> Morceaux { get; set; }
```
Dans la méthode ```OnModelCreating``` :
* on définit la clé primaire de ```Album``` (ceci n'est pas obligatoire, puisqu'on
ne fait que refaire ce que font déjà les convetions d'écriture).
```csharp
//création de la table Album
modelBuilder.Entity<Album>().HasKey(a => a.AlbumId); //définition de la clé primaire
modelBuilder.Entity<Album>().Property(a => a.AlbumId)
.ValueGeneratedOnAdd(); //définition du mode de génération de la clé : génération à l'insertion
```
* on définit la clé primaire de ```Morceau``` (ceci n'est pas obligatoire, puisqu'on
ne fait que refaire ce que font déjà les convetions d'écriture).
```csharp
//création de la table Morceau
modelBuilder.Entity<Morceau>().HasKey(m => m.MorceauId); //définition de la clé primaire
modelBuilder.Entity<Morceau>().Property(m => m.MorceauId)
.ValueGeneratedOnAdd(); //définition du mode de génération de la clé : génération à l'insertion
```
* on ajoute la clé étrangère dans l'entité ```Morceau``` qui permettra d'établir la
5 years ago
relation OnToMany avec l'```Album``` concerné. Comme dit plus haut, le lien
entre les deux tables va se faire avec l'ajout d'une colonne dans la table
de ```Morceau``` qui permettra de stocker l'```Album``` auquel ce ```Morceau```
est lié.
```csharp
// Add the shadow property to the model
modelBuilder.Entity<Morceau>()
.Property<int>("AlbumForeignKey");
```
5 years ago
<img src="./readme_files/ex_042_016_classDiagram2.svg"/>
* on décrit ensuite la relation *OneToMany* avec les lignes suivantes, qu'on
peut interpréter de la manière suivante :
> 1 ```Morceau``` est lié à 1 ```Album``` qui lui-même est lié à
PLUSIEURS ```Morceau``` en utilisant la clé étrangère ```HasForeignKey```
5 years ago
```csharp
// Use the shadow property as a foreign key
modelBuilder.Entity<Morceau>()
.HasOne(m => m.Album)
.WithMany(a => a.Morceaux)
.HasForeignKey("AlbumForeignKey");
```
* notez enfin que la clé étrangère est ici également une *shadow property*
puisqu'elle est définie dans ```OnModelCreating``` et pas directement dans la
classe entité (ici ```Morceau```).
### La classe ```StubbedContext```
* ```StubbedContext``` est une classe fille de ```AlbumDBEntities```.
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);
Album kindofblue = new Album { AlbumId=1, Titre = "Kind of Blue", DateDeSortie = new DateTime(1959, 8, 17) };
Album dialogue = new Album { AlbumId=2, Titre = "Dialogue", DateDeSortie = new DateTime(1965, 9, 1) };
modelBuilder.Entity<Album>().HasData(kindofblue, dialogue);
modelBuilder.Entity<Morceau>().HasData(new { MorceauId = 1, AlbumForeignKey = 1, Titre = "So What" },
new { MorceauId = 2, AlbumForeignKey = 1, Titre = "Freddie Freeloader" },
new { MorceauId = 3, AlbumForeignKey = 1, Titre = "Blue in Green" },
new { MorceauId = 4, AlbumForeignKey = 1, Titre = "All Blues" },
new { MorceauId = 5, AlbumForeignKey = 1, Titre = "Flamenco Sketches" },
new { MorceauId = 6, AlbumForeignKey = 2, Titre = "Catta" },
new { MorceauId = 7, AlbumForeignKey = 2, Titre = "Idle While" },
new { MorceauId = 8, AlbumForeignKey = 2, Titre = "Les Noirs Marchant" },
new { MorceauId = 9, AlbumForeignKey = 2, Titre = "Dialogue" },
new { MorceauId = 10, AlbumForeignKey = 2, Titre = "Ghetto Lights" },
new { MorceauId = 11, AlbumForeignKey = 2, Titre = "Jasper" }
);
}
```
* Contrairement aux deux exemples précédents, il n'est pas nécessaire de préciser
qu'elle est la clé étrangère pour relier les tables puisque cela a été fait
dans la classe ```AlbumDBEntities```
* Remarquez que __À AUCUN MOMENT__ nous ne précisons les valeurs des propriétés
```Album``` de ```Morceau``` et ```Morceaux``` de ```Album```.
Le simple fait d'utiliser la clé étrangère (propriété ```AlbumForeignKey```
ajoutée précédemment) dans ```Morceau``` est suffisant.
* 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 ```Morceaux``` et ```Album```.
### La classe ```Program```
* 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 ```AlbumDBEntites```) ou le stub
(si la migration est faite à partir de ```StubbedContext```).
*Notez l'utilisation d'```Include``` dans
```db.Albums.Include(a => a.Morceaux)``` sinon, les ```Morceau``` ne sont pas
chargés.
```Include``` n'est pas utilisé ensuite dans ```db.Morceaux``` car les
```Album``` 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.Morceaux.Include(m => m.Album)``` puis simplement ```db.Albums```.*
```csharp
using (AlbumDBEntities db = new AlbumDBEntities())
{
WriteLine("Albums : ");
foreach (var a in db.Albums.Include(a => a.Morceaux))
{
WriteLine($"\t{a.AlbumId}: {a.Titre} (sorti le : {a.DateDeSortie.ToString("d")})");
foreach (var m in a.Morceaux)
{
WriteLine($"\t\t{m.Titre}");
}
}
WriteLine();
WriteLine("Morceaux :");
foreach (var m in db.Morceaux)
{
WriteLine($"\t{m.MorceauId}: {m.Titre} (album : {m.Album.Titre})");
}
//...
}
```
* La suite de l'exemple ajoute un nouvel ```Album``` et ses ```Morceau``` puis affiche le contenu de la base de données.
```csharp
using (AlbumDBEntities db = new AlbumDBEntities())
{
//...
WriteLine("\nAjout d'un album et 6 morceaux...\n");
Album captainMarvel = new Album { Titre = "Captain Marvel", DateDeSortie = new DateTime(1972, 3, 3) };
Morceau[] morceaux = { new Morceau { Titre = "La Fiesta", Album = captainMarvel },
new Morceau { Titre = "Five Hundred Miles High", Album = captainMarvel },
new Morceau { Titre = "Captain Marvel", Album = captainMarvel },
new Morceau { Titre = "Time's Lie", Album = captainMarvel },
new Morceau { Titre = "Lush Life", Album = captainMarvel },
new Morceau { Titre = "Day Waves", Album = captainMarvel }
};
foreach (var m in morceaux)
{
captainMarvel.Morceaux.Add(m);
}
db.Add(captainMarvel);
db.SaveChanges();
}
```
```csharp
using (AlbumDBEntities db = new AlbumDBEntities())
{
WriteLine("Albums : ");
foreach (var a in db.Albums.Include(a => a.Morceaux))
{
WriteLine($"\t{a.AlbumId}: {a.Titre} (sorti le : {a.DateDeSortie.ToString("d")})");
foreach (var m in a.Morceaux)
{
WriteLine($"\t\t{m.Titre}");
}
}
WriteLine();
WriteLine("Morceaux :");
foreach (var m in db.Morceaux)
{
WriteLine($"\t{m.MorceauId}: {m.Titre} (album : {m.Album.Titre})");
}
}
```
## 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 (pour cela,
dirigez-vous dans le menu *Outils*, puis *Gestionnaire de package NuGet*, puis
*Console du Gestionnaire de package*) ou le *Terminal* sous MacOSX.
* 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_016_OneToMany_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 : vous devez préciser la classe fille de ```DbContext``` à utiliser :
soit ```AlbumDBEntities```, soit ```StubbedContext```.
```
dotnet ef migrations add ex_042_016 --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_016_OneToMany_FluentAPI**.
* Le résultat de l'exécution va ressembler à (si vous avez utilisé
```StubbedContext```) :
```
Albums :
1: Kind of Blue (sorti le : 17/08/1959)
So What
Freddie Freeloader
Blue in Green
All Blues
Flamenco Sketches
2: Dialogue (sorti le : 01/09/1965)
Catta
Idle While
Les Noirs Marchant
Dialogue
Ghetto Lights
Jasper
Morceaux :
1: So What (album : Kind of Blue)
2: Freddie Freeloader (album : Kind of Blue)
3: Blue in Green (album : Kind of Blue)
4: All Blues (album : Kind of Blue)
5: Flamenco Sketches (album : Kind of Blue)
6: Catta (album : Dialogue)
7: Idle While (album : Dialogue)
8: Les Noirs Marchant (album : Dialogue)
9: Dialogue (album : Dialogue)
10: Ghetto Lights (album : Dialogue)
11: Jasper (album : Dialogue)
Ajout d'un album et 6 morceaux...
Albums :
1: Kind of Blue (sorti le : 17/08/1959)
So What
Freddie Freeloader
Blue in Green
All Blues
Flamenco Sketches
2: Dialogue (sorti le : 01/09/1965)
Catta
Idle While
Les Noirs Marchant
Dialogue
Ghetto Lights
Jasper
3: Captain Marvel (sorti le : 03/03/1972)
La Fiesta
Five Hundred Miles High
Captain Marvel
Time's Lie
Lush Life
Day Waves
Morceaux :
1: So What (album : Kind of Blue)
2: Freddie Freeloader (album : Kind of Blue)
3: Blue in Green (album : Kind of Blue)
4: All Blues (album : Kind of Blue)
5: Flamenco Sketches (album : Kind of Blue)
6: Catta (album : Dialogue)
7: Idle While (album : Dialogue)
8: Les Noirs Marchant (album : Dialogue)
9: Dialogue (album : Dialogue)
10: Ghetto Lights (album : Dialogue)
11: Jasper (album : Dialogue)
12: La Fiesta (album : Captain Marvel)
13: Five Hundred Miles High (album : Captain Marvel)
14: Captain Marvel (album : Captain Marvel)
15: Time's Lie (album : Captain Marvel)
16: Lush Life (album : Captain Marvel)
17: Day Waves (album : Captain Marvel)
```
## 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 ```AlbumDBEntities``` à la place de ```StubbedContext``` :
```
dotnet ef migrations add ex_042_016 --context AlbumDBEntities
dotnet ef database update --context AlbumDBEntities
```
Lors de l'exécution, le résultat sera évidemment différent puisqu'il n'y aura pas les
2 albums du stub.
Il pourra ressembler à :
```
Albums :
Morceaux :
Ajout d'un album et 6 morceaux...
Albums :
1: Captain Marvel (sorti le : 03/03/1972)
La Fiesta
Five Hundred Miles High
Captain Marvel
Time's Lie
Lush Life
Day Waves
Morceaux :
1: La Fiesta (album : Captain Marvel)
2: Five Hundred Miles High (album : Captain Marvel)
3: Captain Marvel (album : Captain Marvel)
4: Time's Lie (album : Captain Marvel)
5: Lush Life (album : Captain Marvel)
6: Day Waves (album : Captain Marvel)
```
## 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_016_OneToMany_FluentAPI.Albums.db* qui a été généré par l'exécution du
programme et qui se trouve près de *ex_042_016_OneToMany_FluentAPI.csproj*.
![DB Browser for SQLite](../ex_042_015_OneToMany_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_015_OneToMany_conventions/readme_files/dbbrowser02.png)
![DB Browser for SQLite](../ex_042_015_OneToMany_conventions/readme_files/dbbrowser03.png)