Les classes non persistées
Pour communiquer entre les différentes couches d'une application, il est généralement conseillé d'utiliser des objets de transfert, appelés aussi Dto. Ces objets sont non persistés, et sont généralement construits à partir des objets persistés. Ils en sont souvent des fractions, et peuvent en aggréger plusieurs.
Définition d'un objet non persisté
Dans TopModel, il est possible de définir un objet non persisté exactement de la même manière qu'un objet persisté. La seule différence étant l'absence de clé primaire, dans un objet de transfert.
Exemple avec la classe UtilisateurDto (à ne pas reproduire. Dans ce chapitre, ne reproduisez les exemples sur votre environnement projet que lorsque vous y serez conviés) :
---
class:
name: UtilisateurDto
comment: Objet de transfert pour la classe Utilisateur
properties:
- name: Nom
comment: Nom du Utilisateur
label: Utilisateur
domain: DO_LIBELLE
required: true
La classe UtilisateurDto ne contient qu'un libellé, pas de clé primaire, et sera donc considérée comme une classe non persistée.
Remarque : Il est préférable de créer les objets non persistés dans un fichier à part comme dans l'exemple précédent.
Alias
Evidemment, il est extrêmement laborieux de définir UtilisateurDto de cette manière. Il serait préférable de matérialiser le lien fort entre la propriété Nom de la classe Utilisateur et la propriété Nom de UtilisateurDto.
Pour cela, TopModel permet de définir des propriétés d'Alias. L'objectif est de référencer une propriété ou un ensemble de propriétés définies dans une autre classe.
Alias vers une propriété
Pour définir un alias vers une unique propriété, nous devons renseigner deux informations : la classe visée, et la propriété concernée. Dans la liste des propriétés d'une classe non persistée, cela se traduit de cette manière :
---
class:
name: UtilisateurDto
comment: Objet de transfert pour la classe Utilisateur
properties:
- alias:
class: Utilisateur # Classe cible de l'alias
property: Nom # Propriété cible de l'alias
La classe en question doit être accessible depuis le fichier sur lequel nous travaillons. Si elle n'est pas définie dans le fichier courant, alors le fichier qui la contient doit être importé dans les
uses.
Ainsi, la classe UtilisateurDto contiendra une propriété Nom, qui aura exactement les mêmes caractéristiques que la propriété Nom dans la classe Utilisateur, à savoir :
- Son domaine
- Son libelle
- Son commentaire
- Le fait qu'elle soit requise
- etc
Néanmoins si la propriété en question est une clé primaire dans Utilisateur, elle ne fera pas de la classe UtilisateurDto une classe persistée.
Préfix/Suffix
Dans l'éventualité ou l'on créerait une classe contenant des alias vers plusieurs classes différentes, nous pourrions nous retrouver avec des conflits sur les noms des propriétés. Pour les distinguer, nous pouvons alors ajouter aux noms des propriétés d'alias un prefix ou un suffix, qui viendront respectivement se placer avant ou après le nom de la propriété d'alias. Si le suffix/prefix est true, alors la chaîne de caractère ajoutée sera le nom de la classe
---
class:
name: UtilisateurDto
comment: Objet de transfert pour la classe Utilisateur
properties:
- alias:
class: Utilisateur
property: Nom
suffix: true
Ici, UtilisateurDto contiendra une propriété NomUtilisateur, ayant les mêmes attributs que la propriété Nom de la classe Utilisateur.
Surcharger les attributs
En dehors du nom et du domaine, il est possible de surcharger tous les attributs de la propriété cible de l'alias. Il suffit pour cela de repréciser la nouvelle valeur dans la propriété d'alias
---
class:
name: UtilisateurDto
comment: Objet de transfert pour la classe Utilisateur
properties:
- alias:
class: Utilisateur
property: Nom
required: false # Surcharge de la valeur du champ required.
Attention : ces surcharges se placent au même niveau d'indentation que le mot clé alias.
Alias vers un ensemble de propriétés
Pour créer des Dtos fragments de classes persistées, il existe des raccourcis permettant d'ajouter des ensembles de propriétés. Dans l'exemple ci-dessus, retirons simplement la précision de la propriété cible, et TopModel comprendra qu'il faut faire un alias sur l'ensemble des propriétés de la classe cible.
Exemple :
---
class:
name: UtilisateurDto
comment: Objet de transfert pour la classe Utilisateur
properties:
- alias:
class: Utilisateur
Les surchages évoquées au paragraphes précédent s'appliqueront à toutes les propriétés aliasées
Include/Exclude
Pour ajuster l'ensemble des propriétés aliasées, nous pouvons soit exclure une liste de propriétés de la classe cible, soit préciser quelles propriétés nous souhaitons reprendre.
Exemple avec include
---
class:
name: UtilisateurDto
comment: Objet de transfert pour la classe Utilisateur
properties:
- alias:
class: Utilisateur
include:
- Nom # La liste des propriétés à inclure. Toutes les autres seront ignorées
Exemple avec exclude
---
class:
name: UtilisateurDto
comment: Objet de transfert pour la classe Utilisateur
properties:
- alias:
class: Utilisateur
exclude:
- Id # La liste des propriétés à exclure. Toutes les autres seront ajoutées à la classe UtilisateurDto
Les deux exemples ci-dessus produisent exactement le même résultat que le premier exemple proposé : UtilisateurDto contiendra une propriété Nom, ayant les mêmes attributs que la propriété Nom de la classe Utilisateur.
Astuce : il est tout à fait possible d'ajouter deux alias vers la même classe. Cette pratique permet notamment de surcharger différemment des ensembles de propriétés.
Maintenant, créez un fichier "Dto.tmd" et copiez-y le code suivant :
---
module: Users
uses:
- Utilisateur
tags: []
---
class:
name: UtilisateurDto
comment: Objet de transfert pour la classe Utilisateur
properties:
- alias:
class: Utilisateur
Composition
Dans certains cas, il est nécessaire de définir des propriétés dont le type est une classe définie dans le modèle. Nous pourrions éventuellement passer par un domaine, mais il vaut mieux utiliser des propriétés de type composition.
Supposons que l'on veuille créer un Dto UtilisateurDto en faisant une composition avec la classe AdresseDto, on aurait :
---
class:
name: UtilisateurDto
comment: Objet de transfert pour la classe Utilisateur
properties:
- alias:
class: Utilisateur
- composition: AdresseDto # Nom de la classe
name: Adresse # Nom de la propriété
comment: Adresse de l'utilisateur # Commentaire obligatoire
Domaine
Si un domaine est renseigné dans l'attribut domain de la composition, alors il doit être générique dans les langages où il est décliné. La composition sera alors du type du domaine, générique de la classe composée.
Exemple avec le domaine DO_PAGE que vous pouvez ajouter dans votre fichier "Domains.tmd" :
---
domain:
name: DO_PAGE
label: Date
ts:
genericType: Page<{T}>
imports:
- "@/services/api-types" # Imports nécessaires au bon fonctionnement
java:
genericType: Page<{T}>
imports:
- "org.springframework.data.domain.Page" # Imports nécessaires au bon fonctionnement de la classe Java
DO_PAGE défini, on peut l'utiliser dans notre objet UtilisateurDto. Ajoutez les lignes suivantes dans votre fichier "Dto.tmd" :
---
class:
name: UtilisateurDto
comment: Objet de transfert pour la classe Utilisateur
properties:
- alias:
class: Utilisateur
- composition: AdresseDto # Nom de la classe
name: Adresse # Nom de la propriété
comment: Adresse de l'utilisateur # Commentaire obligatoire
domain: DO_PAGE # Type de composition
Le type de composition donnera le type générique Page<UtilisateurDto>.
Mappers
Avec la possibililité de créer aisément des Dtos vient la nécessité de transformer nos entités persistées en dtos et vice et versa. L'usage de mappers par convention de nommage peut être hasardeuse, et TopModel peut mieux faire. En effet, le modèle contient, dans sa description, le lien fort qu'entretiennent les propriétés des deux côtés des alias. C'est pourquoi TopModel donne la possibilité de créer des mappers. La correspondance entre les champs se fera d'abord par correspondance entre alias. Puis, les propriétés restantes seront mappées avec la règle même nom et même domaine.
S'il y a ambiguité sur les mappings, où si certains champs doivent être ignorés, il est possible de donner des précisions sous l'attribut mappings.
Mapper To
Prenons d'abord la classe UtilisateurCreateDto définie telle que :
---
class:
name: UtilisateurCreateDto
comment: Objet de transfert pour la classe Utilisateur
properties:
- alias:
class: Utilisateur
exclude:
- Id
prefix: true
Dans l'application qui utilisera ce modèle, on souhaite donner la possibilité de renseigner un nouvel utilisateur en renseignant toutes ses propriétés, sauf l'identifiant technique.
Lorsque sont saisies ces informations, nous obtenons un UtilisateurCreateDto que nous souhaiterons convertir en Utilisateur afin de le sauvegarder en base de données. Créons donc un mapper UtilisateurCreateDto -> Utilisateur.
Il serait en théorie possible de créer soit :
- Un mapper
fromsur la classeUtilisateurqui prend comme paramètre unUtilisateurCreateDto - Un mapper
tosur la classeUtilisateurCreateDtoqui prend comme classe destination unUtilisateur
Ici, nous préfèrerons créer un mapper to sur la classe UtilisateurCreateDto.
Voici comment l'ajouter à notre définition de classe. Ajoutez les lignes suivantes à votre fichier "Dto.tmd" :
---
class:
name: UtilisateurCreateDto
comment: Objet de transfert pour la classe Utilisateur dans le cas d'une création
properties:
- alias:
class: Utilisateur
exclude:
- Id
prefix: true
mappers: # Définition des mappers
to: # Mappers to ou from
- class: Utilisateur # Définition de notre premier mapper, vers la classe Utilisateur
Nous avons donc défini un mapper de la classe UtilisateurCreateDto vers la classe Utilisateur
Mapper From
Considérons maintenant la classe UtilisateurSearchResultDto, qui, à titre d'exemple représente les résultats d'une recherche dans la table Utilisateur. elle se définit comme suit :
---
class:
name: UtilisateurSearchResultDto
comment: Objet de transfert pour la classe Utilisateur, dans le cas d'une recherche
properties:
- alias:
class: Utilisateur
exclude:
- Id
- alias:
class: TypeUtilisateur
include:
- Libelle
suffix: true
Ici, notre résultat de recherche devra renvoyer tous les champs de la classe Utilisateur (hormis son Id), ainsi que le label de son TypeUtilisateur.
Nous aurions donc besoin d'un mapper pour construire ces objets UtilisateurSearchResultDto. Idéalement, ce mapper doit pouvoir prendre en paramètres une instance de la classe Utilisateur, et une instance de la classe Utilisateur correspondante.
Un tel mapper s'écrit de la façon suivante :
---
class:
name: UtilisateurSearchResultDto
comment: Objet de transfert pour la classe Utilisateur, dans le cas d'une recherche
properties:
- alias:
class: Utilisateur
exclude:
- Id
- alias:
class: TypeUtilisateur
include:
- Libelle
suffix: true
mappers:
from: # Liste des mappers From
- params: # Liste des paramètres du mapper
- class: Utilisateur # Premier paramètre, une instance de la classe Utilisateur
- class: TypeUtilisateur # Deuxième paramètre, une instance de la classe TypeUtilisateur
Nous avons donc défini un mapper from, prenant deux paramètres, Utilisateur et TypeUtilisateur, permettant de créer une instance de la classe UtilisateurSearchResultDto.
Pour plus de détails sur les cas d'usage avancés (exclusion de propriétés, cas de mappings ambigus, héritage, nommage des paramètres et des mappers...), se rapporter à la section Mappers.
Répertoire projet
Nous venons de couvrir beaucoup de notions essentielles. Au début du chapitre, notre répertoire projet était constitué des éléments suivants:
- Projet
- topmodel.config
- Utilisateur.tmd
- Domains.tmd
- References.tmd
- Dto.tmd
Exemple de code généré
Classes non persistées
- Java
- C#
package hello world.dtos.users;
/**
* Objet de transfert pour la classe Utilisateur, dans le cas d'une recherche.
*/
@Generated("TopModel : https://github.com/klee-contrib/topmodel")
public class UtilisateurSearchResultDto implements Serializable {
/**
* Serial ID.
*/
@Serial
private static final long serialVersionUID = 1L;
/**
* Adresse mail de l'utilisateur.
* Alias of {@link hello world.entities.users.Utilisateur#getEmail() Utilisateur#getEmail()}
*/
@NotNull
@Size(max = 50)
private String email;
/**
* Nom de l'utilisateur.
* Alias of {@link hello world.entities.users.Utilisateur#getNom() Utilisateur#getNom()}
*/
@Size(max = 15)
private String nom;
/**
* Date d'inscription.
* Alias of {@link hello world.entities.users.Utilisateur#getDateInscription() Utilisateur#getDateInscription()}
*/
private LocalDate dateInscription;
/**
* Type de l'utilisateur.
* Alias of {@link hello world.entities.users.Utilisateur#getTypeUtilisateurCode() Utilisateur#getTypeUtilisateurCode()}
*/
private TypeUtilisateurCode typeUtilisateurCode;
/**
* Libellé du type d'utilisateur.
* Alias of {@link hello world.entities.refs.TypeUtilisateur#getLibelle() TypeUtilisateur#getLibelle()}
*/
@NotNull
@Size(max = 15)
private String libelleTypeUtilisateur;
// ...
namespace Hello World.Users.Models;
/// <summary>
/// Objet de transfert pour la classe Utilisateur, dans le cas d'une recherche.
/// </summary>
public partial record UtilisateurSearchResultDto
{
/// <summary>
/// Adresse mail de l'utilisateur.
/// </summary>
[Required]
[Domain(Domains.Email)]
[StringLength(50)]
public string Email { get; set; }
/// <summary>
/// Nom de l'utilisateur.
/// </summary>
[Domain(Domains.Libelle)]
[StringLength(15)]
public string Nom { get; set; }
/// <summary>
/// Date d'inscription.
/// </summary>
[Domain(Domains.Date)]
public DateTime? DateInscription { get; set; }
/// <summary>
/// Type de l'utilisateur.
/// </summary>
[ReferencedType(typeof(TypeUtilisateur))]
[Domain(Domains.Code)]
public TypeUtilisateur.Codes? TypeUtilisateurCode { get; set; }
/// <summary>
/// Libellé du type d'utilisateur.
/// </summary>
[Required]
[Domain(Domains.Libelle)]
[StringLength(15)]
public string LibelleTypeUtilisateur { get; set; }
}
Mappers
- Java
- C#
package hello world.entities.users;
@Generated("TopModel : https://github.com/klee-contrib/topmodel")
public class UsersMappers {
private UsersMappers() {
// private constructor to hide implicite public one
}
/**
* Crée une nouvelle instance de la classe 'UtilisateurSearchResultDto' en mappant les champs sources.
* @param utilisateur Instance de 'Utilisateur' source.
* @param typeUtilisateur Instance de 'TypeUtilisateur' source.
*
* @return Une nouvelle instance de 'UtilisateurSearchResultDto' sur laquelle les champs sources ont été mappés.
*/
public static UtilisateurSearchResultDto createUtilisateurSearchResultDto(Utilisateur utilisateur, TypeUtilisateur typeUtilisateur) {
return mapUtilisateurSearchResultDto(utilisateur, typeUtilisateur, new UtilisateurSearchResultDto());
}
/**
* Mappe les champs sources sur l'instance de la classe 'UtilisateurSearchResultDto' passée en paramètre.
* @param utilisateur Instance de 'Utilisateur' source.
* @param typeUtilisateur Instance de 'TypeUtilisateur' source.
* @param target Instance de 'UtilisateurSearchResultDto' cible.
*
* @return L'instance de 'UtilisateurSearchResultDto' passée en paramètres sur lesquels les champs sources ont été mappés.
*/
public static UtilisateurSearchResultDto mapUtilisateurSearchResultDto(Utilisateur utilisateur, TypeUtilisateur typeUtilisateur, UtilisateurSearchResultDto target) {
if (target == null) {
throw new IllegalArgumentException("target cannot be null");
}
if (utilisateur == null) {
throw new IllegalArgumentException("utilisateur cannot be null");
}
if (typeUtilisateur == null) {
throw new IllegalArgumentException("typeUtilisateur cannot be null");
}
target.setEmail(utilisateur.getEmail());
target.setNom(utilisateur.getNom());
target.setDateInscription(utilisateur.getDateInscription());
target.setTypeUtilisateurCode(utilisateur.getTypeUtilisateurCode());
target.setLibelleTypeUtilisateur(typeUtilisateur.getLibelle());
return target;
}
/**
* Mappe 'Utilisateur' vers une nouvelle instance de 'UtilisateurCreateDto'.
* @param source Instance de 'UtilisateurCreateDto' à mapper.
*
* @return Nouvelle instance de 'UtilisateurCreateDto' mappée depuis 'utilisateur'.
*/
public static Utilisateur toUtilisateur(UtilisateurCreateDto source) {
return toUtilisateur(source, new Utilisateur());
}
/**
* Mappe 'Utilisateur' vers une nouvelle instance ou bien sur l'instance passée en paramètres.
* @param source Instance de 'UtilisateurCreateDto' à mapper.
* @param target Instance de 'Utilisateur' sur laquelle mapper.
*
* @return Nouvelle instance ou bien l'instance passée en paramètres mappée depuis 'utilisateur'.
*/
public static Utilisateur toUtilisateur(UtilisateurCreateDto source, Utilisateur target) {
if (source == null) {
throw new IllegalArgumentException("source cannot be null");
}
if (target == null) {
throw new IllegalArgumentException("target cannot be null");
}
target.setEmail(source.getUtilisateurEmail());
target.setNom(source.getUtilisateurNom());
target.setDateInscription(source.getUtilisateurDateInscription());
target.setTypeUtilisateurCode(source.getUtilisateurTypeUtilisateurCode());
return target;
}
}
namespace Hello World.Users.Models;
/// <summary>
/// Mappers pour le module 'Users'.
/// </summary>
public static class UsersMappers
{
/// <summary>
/// Crée une nouvelle instance de 'UtilisateurSearchResultDto'.
/// </summary>
/// <param name="utilisateur">Instance de 'Utilisateur'.</param>
/// <param name="typeUtilisateur">Instance de 'TypeUtilisateur'.</param>
/// <returns>Une nouvelle instance de 'UtilisateurSearchResultDto'.</returns>
public static UtilisateurSearchResultDto CreateUtilisateurSearchResultDto(Utilisateur utilisateur, TypeUtilisateur typeUtilisateur)
{
ArgumentNullException.ThrowIfNull(utilisateur);
ArgumentNullException.ThrowIfNull(typeUtilisateur);
return new UtilisateurSearchResultDto
{
Email = utilisateur.Email,
Nom = utilisateur.Nom,
DateInscription = utilisateur.DateInscription,
TypeUtilisateurCode = utilisateur.TypeUtilisateurCode,
LibelleTypeUtilisateur = typeUtilisateur.Libelle
};
}
/// <summary>
/// Mappe 'UtilisateurCreateDto' vers 'Utilisateur'.
/// </summary>
/// <param name="source">Instance de 'UtilisateurCreateDto'.</param>
/// <param name="id">Identifiant unique de l'utilisateur.</param>
/// <returns>Une nouvelle instance de 'Utilisateur'.</returns>
public static Utilisateur ToUtilisateur(this UtilisateurCreateDto source, long? id = null)
{
return new Utilisateur
{
Email = source.UtilisateurEmail,
Nom = source.UtilisateurNom,
DateInscription = source.UtilisateurDateInscription,
TypeUtilisateurCode = source.UtilisateurTypeUtilisateurCode,
Id = id
};
}
/// <summary>
/// Mappe 'UtilisateurCreateDto' vers 'Utilisateur'.
/// </summary>
/// <param name="source">Instance de 'UtilisateurCreateDto'.</param>
/// <param name="dest">Instance pré-existante de 'Utilisateur'.</param>
/// <returns>L'instance pré-existante de 'Utilisateur'.</returns>
public static Utilisateur ToUtilisateur(this UtilisateurCreateDto source, Utilisateur dest)
{
dest.Email = source.UtilisateurEmail;
dest.Nom = source.UtilisateurNom;
dest.DateInscription = source.UtilisateurDateInscription;
dest.TypeUtilisateurCode = source.UtilisateurTypeUtilisateurCode;
return dest;
}
}