Aller au contenu principal

11-1 Modèle et requête Dapper

Votre modèle événement devrait ressembler à quelque chose comme ceci:

public class Evenement
{
public int Id { get; set; }
public required string Nom { get; set; }
public string? Description { get; set; }
public string? ImagePath { get; set; }
public DateTime Date { get; set; }
public Decimal Prix { get; set; }
public int Capacite { get; set; }
public required int VilleId { get; set; }
}

Concernant la ville, VilleId c'est bien, mais on aimerait évidemment avoir l'objet de ville associé à l'événement, pas seulement son id.

Pour afficher un libellé avec le nom de la ville sur chaque événement, on pourrait:

  • récupérer la liste d'événements
  • récupérer la liste de villes
  • faire l'association manuellement pour chaque événement entre ce dernier et l'objet de ville
  • afficher la ville de l'événement dans un libellé

Fonctionnel, surement, mais fastidieux! Associer deux listes d'objet ensemble n'est pas en soi un problème techniquement complexe, mais il doit bien y avoir une meilleure solution.

Dapper nous offre en effet un petit raccourci pour faire ce genre d'associations fréquentes automatiquement. Ce concept est appelé en Dapper Multi Mapping.

Ajouter Ville au modèle Evenement

On ajoute premièrement une propriété optionnelle Ville à Evenement qui pourra contenir une ville (l'objet).

On la met optionnelle puisqu'elle ne sera pas présente dans tous les cas de figure. VilleId oui, mais pas l'objet complet Ville.

Snowfall.Domain/Models/Evenement.cs
public class Evenement
{
public int Id { get; set; }
public required string Nom { get; set; }
public string? Description { get; set; }
public string? ImagePath { get; set; }
public DateTime Date { get; set; }
public Decimal Prix { get; set; }
public int Capacite { get; set; }
public required int VilleId { get; set; }
public Ville? Ville { get; set; }
}

Modifier les requêtes Dapper pour récupérer la ville associée

Dapper ne fait pas de magie (quoi que...), il faut modifier nos requêtes pour ajouter un INNER JOIN et ensuite procéder au mapping.

Modifier GetAll()

Commençons par GetAll() dans EvenementRepository.

On peut commencer par modifier la requête pour qu'elle retourne les villes associées via INNER JOIN:

Snowfall.Data/Repositories/EvenementRepository.cs
public async Task<List<Evenement>> GetAll()
{
string sql = @"
SELECT * from evenements e
INNER JOIN villes v on e.ville_id = v.id;
";

// ...

Ensuite, la magie opère dans la requête que nous faisons via Dapper.

Snowfall.Data/Repositories/EvenementRepository.cs
public async Task<List<Evenement>> GetAll()
{
string sql = @"
SELECT * from evenements e
INNER JOIN villes v on e.ville_id = v.id;
";

using (IDbConnection connection = _dbContext.CreateConnection())
{
IEnumerable<Evenement> evenements = await connection.QueryAsync<Evenement, Ville, Evenement>(sql,
(evenement, ville) =>
{
evenement.Ville = ville;
return evenement;
});
return evenements.ToList();
}
}
info

La fonction QueryAsync de Dapper peut préciser plusieurs types. Nous n'utilisions que la version à un type auparavant.

Ici, on précise <Evenement, Ville, Evenement>. Ce que ceci veut dire:

  • Les deux premiers paramètres sont les deux objets que Dapper pourra trouver dans le résultat de la requête: des événements et des villes. En effet, pour chaque rangée retournée par la requête SQL, une portion du retour est un événement et une autre portion est une ville.
  • Le dernier paramètre est l'objet final, soit l'objet dans lequel seront fusionnés les deux objets d'entrée.
  • En résumé, on dit: pour chaque rangée de résultats, tu trouveras un événement, une ville et j'aimerais que le tout soit fusionné dans un objet de type Evenement.
  • Pour savoir où "couper" la requête et donc quelle portion est l'événement et quelle portion est la ville, Dapper utilise la colonne id. Cet aspect est expliqué plus en détail plus bas.

Ainsi, on dit: ma requête retournera des événements et des villes et j'aimerais qu'au final, le tout soit fusionné dans un objet de type Evenement.

On fournit une fonction anonyme (=>) qui recevra les deux premiers paramètres et sera responsable de les fusionner. C'est à nous de dire à Dapper comment faire ce travail.

La fonction anonyme ci-bas dit donc: associe à la propriété Ville de l'événement, l'objet ville que tu trouveras dans la requête. Finalement, on retourne l'événement fusionné avec la ville.

(evenement, ville) =>
{
evenement.Ville = ville;
return evenement;
});

Pour le reste, rien ne change.

Modifier FindById()

Idem, même principe pour FindById().

Snowfall.Data/Repositories/EvenementRepository.cs
public async Task<Evenement?> FindById(int id)
{
string sql = @"
SELECT * from evenements e
INNER JOIN villes on e.ville_id = villes.id
WHERE e.id = @Id;
";

using (IDbConnection connection = _dbContext.CreateConnection())
{
var param = new
{
Id = id
};
IEnumerable<Evenement> resultat = await connection.QueryAsync<Evenement, Ville, Evenement>(sql,
(evenement, ville) =>
{
evenement.Ville = ville;
return evenement;
}, param);

return resultat.FirstOrDefault();
}
}
info

Notez que le Multi Mapping n'est disponible que sur la fonction QueryAsync. C'est pourquoi la fonction a été modifiée légèrement pour retourner une liste (d'un élément). D'où l'appel sur resultat.FirstOrDefault() qui permet d'obtenir le même comportement que précédemment.

Comment Dapper sait-il quoi est quoi?

On fait l'association, mais Dapper a tout de même été en mesure de séparer événements et villes pour nous.

Comment sait-il quelles colonnes appartiennent à l'événement et lesquelles appartiennent à la ville?

Au fond, il ne le sait pas vraiment, tout est une question de convention!

Premièrement l'ordre est important et ensuite, il s'appuie sur le fait que nos clés primaires de tables sont simplement Id.

Imgur

Dapper fait simplement la coupure lorsqu'il rencontre un Id.

Les premières colonnes sont automatiquement associées à Evenement puisque c'est le premier type mentionné dans QueryAsync. **Dès qu'il rencontre une autre colonne Id, il associe le reste au prochain type, soit Ville.

astuce

Si jamais on ne suit pas cette convention (Id comme clé primaire), il est possible d'utiliser un paramètre SplitOn pour préciser le nom de la colonne où faire la coupure.