Génération des mappers
| Nom | Condition d'activation | Objets ciblés | Fichiers générés |
|---|---|---|---|
JpaMapperGenerator | Toujours | Mappers | Classe statique contenant des méthodes statiques, correspondant aux mappers définis dans le modèle |
Les mappers sont générés comme des méthodes statiques dans une classe statique. Cette classe rassemble tous les mappers d'un module racine. Elle est positionnée dans le package des entités si l'une des classes impliquées est persistée, et dans le package des DTOs sinon.
Remarque : Le module utilisé pour un mapper est celui de la classe persistée qui a été trouvée, ou à défaut celui de la classe qui définit le mapper.
Localisation et nommage des fichiers générés
Les mappers d'un même module racine sont regroupés dans une unique classe Java. Le nom du fichier dépend du package dans lequel le fichier est généré :
- Dans le package contenant les classes persistées (
entitites):{ModulePascalCase}Mappers.java(exemple :RestaurantMappers) - Dans le package contenant les classes non persistées (
dtos) :{ModulePascalCase}DTOMappers.java(exemple :RestaurantDTOMappers)
Une classe est considérée comme persistée si elle a une clé primaire, ou si l'un de ses tags est listé dans la configuration mapperTagsOverrides :
Si une des classes impliquées dans le mapper est une classe persistée, alors le mapper sera généré dans le package des classes persistées (entities).
Si toutes les classes impliquées dans le mappers sont non persistées, alors le mapper sera généré dans le package des classes non persistées (dtos).
L'option mapperTagsOverrides permet ainsi de forcer la génération dans entities/ même lorsqu'aucune des classes impliquées n'est effectivement persistée.
Structure d'un fichier de mappers
Chaque fichier contient une classe public portant le nom décrit ci-dessus et :
- Une annotation
@Generated("TopModel : https://github.com/klee-contrib/topmodel")si l'optiongeneratedHintest activée. - Un constructeur
private(sans paramètre) afin d'empêcher l'instanciation de la classe utilitaire. - Toutes les méthodes statiques correspondant aux mappers
frompuis aux mappersto.
@Generated("TopModel : https://github.com/klee-contrib/topmodel")
public class RestaurantMappers {
private RestaurantMappers() {
// private constructor to hide implicite public one
}
// ... méthodes générées ...
}
Mappers from
Les mappers from sont générés sous deux formes :
create[Nom de la classe à créer]: crée une nouvelle instance de la classe cible en mappant les champs sources. Cette méthode délègue àmap[Nom de la classe à créer]en lui passant une nouvelle instance comme cible.map[Nom de la classe à créer]: mappe les champs sources sur une instance de la classe cible passée en paramètre. Cette méthode peut être utilisée pour peupler une instance existante.
Les deux méthodes prennent en entrée la liste des paramètres d'entrée définis dans le mapper : d'abord les classParams (instances d'autres classes), puis les propertyParams (valeurs scalaires additionnelles). La méthode mapXXX prend également une instance de la classe cible en dernier paramètre.
Remarque : la méthode createXXX n'est pas générée pour les classes abstraites (seule la méthode mapXXX l'est).
Contrôles de nullité
La méthode mapXXX lève une IllegalArgumentException dans les cas suivants :
- Si
targetestnull. - Pour chaque
classParamdontrequiredvauttrue: si le paramètre correspondant estnull. - Pour chaque
propertyParamdont la propriété estrequired: si la valeur correspondante estnull. Les paramètres correspondant à une association persistante sur une classe persistante sont ignorés pour ce contrôle (la clé étrangère étant gérée via l'association).
Exemple
public static AvisClientRead createAvisClientRead(AvisClient avisClient) {
return mapAvisClientRead(avisClient, new AvisClientRead());
}
public static AvisClientRead mapAvisClientRead(AvisClient avisClient, AvisClientRead target) {
if (target == null) {
throw new IllegalArgumentException("target cannot be null");
}
if (avisClient == null) {
throw new IllegalArgumentException("avisClient cannot be null");
}
target.setId(avisClient.getId());
target.setNote(avisClient.getNote());
// ... propriétés simples ...
if (avisClient.getClient() != null) {
target.setClientId(avisClient.getClient().getId());
} else {
target.setClientId(null);
}
return target;
}
Mappers from avec propertyParams
Lorsque le mapper déclare des propertyParams (propriétés scalaires additionnelles), ceux-ci sont ajoutés à la signature après les classParams. Les valeurs sont simplement affectées via les setters correspondants.
public static RestaurantAvecStatistiques createRestaurantAvecStatistiques(
Restaurant restaurant, Integer nombrePlats, Integer nombreTables, BigDecimal noteMoyenne) {
return mapRestaurantAvecStatistiques(restaurant, nombrePlats, nombreTables, noteMoyenne, new RestaurantAvecStatistiques());
}
Mappers to
Les mappers to sont nommés d'après le name du mapper défini dans le modèle, converti en camelCase (valeur par défaut : to{NomClasseSource}). Le paramètre source est unique et obligatoire.
Deux formes sont générées :
to[Nom](source): crée une nouvelle instance de la classe cible et délègue à la méthodeto[Nom](source, target). Cette variante n'est pas générée lorsque la classe cible estabstract.to[Nom](source, target): mappe les champs sources sur l'instancetargetpassée en paramètre.
La méthode avec target lève une IllegalArgumentException si source ou target est null.
Exemple (avec mapping d'enum)
public static Plat toPlat(PlatWrite source) {
return toPlat(source, new Plat());
}
public static Plat toPlat(PlatWrite source, Plat target) {
if (source == null) {
throw new IllegalArgumentException("source cannot be null");
}
if (target == null) {
throw new IllegalArgumentException("target cannot be null");
}
target.setNom(source.getNom());
// ... propriétés simples ...
if (source.getCategoriePlatCode() != null) {
target.setCategoriePlat(new CategoriePlat(source.getCategoriePlatCode()));
} else {
target.setCategoriePlat(null);
}
return target;
}
Règles de nullité et de conversion
Le code généré pour chaque mapping dépend du type des propriétés source et cible. Les cas suivants sont gérés (par GetSourceGetter dans JpaMapperGenerator.cs) :
Propriétés simples
Lorsque les deux propriétés sont simples et compatibles, une affectation directe est générée :
target.setNote(avisClient.getNote());
Si les domaines diffèrent, la conversion déclarée par le domaine est appliquée automatiquement.
Association vers clé étrangère (propriété)
Lorsqu'une association simple est projetée vers une clé étrangère (typiquement id), le getter est protégé par un null-check avec fallback à null :
if (avisClient.getClient() != null) {
target.setClientId(avisClient.getClient().getId());
} else {
target.setClientId(null);
}
Collection d'associations vers collection de clés étrangères
Une collection d'associations est projetée via un stream() filtrant les valeurs nulles :
if (restaurant.getMenus() != null) {
target.setMenus(restaurant.getMenus().stream().filter(Objects::nonNull).map(Menu::getId).collect(Collectors.toList()));
} else {
target.setMenus(null);
}
Si l'association source est elle-même multi-niveaux (collection d'associations dont une propriété est également une association), un .map(...).filter(Objects::nonNull) intermédiaire est inséré :
if (restaurant.getPromotions() != null) {
target.setPromotions(restaurant.getPromotions().stream().filter(Objects::nonNull).map(Promotion::getPlat).filter(Objects::nonNull).map(Plat::getId).collect(Collectors.toList()));
} else {
target.setPromotions(null);
}
Composition objet vers objet (classes différentes)
Lorsqu'une composition est projetée vers une composition d'une autre classe, le générateur délègue au mapper approprié (createXXX/mapXXX pour un from, toXXX pour un to). Si la cible possède déjà une instance, la variante mapXXX(source, target.getX())/toXXX(source, target.getX()) est utilisée pour préserver l'instance ; sinon une nouvelle instance est créée :
if (commande.getClient() != null) {
target.setClient(target.getClient() != null
? RestaurantMappers.mapClientRead(commande.getClient(), target.getClient())
: RestaurantMappers.createClientRead(commande.getClient()));
} else {
target.setClient(null);
}
Le même schéma s'applique pour les mappers to :
if (source.getClient() != null) {
target.setClient(target.getClient() != null
? RestaurantMappers.toClient(source.getClient(), target.getClient())
: RestaurantMappers.toClient(source.getClient()));
} else {
target.setClient(null);
}
Collection de compositions (classes différentes)
Les collections de compositions sont projetées en streamant et en déléguant à la méthode createXXX/toXXX via une méthode de référence :
if (commande.getLignes() != null) {
target.setLignes(commande.getLignes().stream().filter(Objects::nonNull).map(RestaurantMappers::createLigneCommandeRead).collect(Collectors.toList()));
} else {
target.setLignes(null);
}
Clé étrangère vers association (enum)
Lorsqu'une clé étrangère scalaire est projetée vers une association (typiquement une enum à code), le générateur instancie la classe cible via son constructeur dédié :
if (source.getCategoriePlatCode() != null) {
target.setCategoriePlat(new CategoriePlat(source.getCategoriePlatCode()));
} else {
target.setCategoriePlat(null);
}
Collection de clés étrangères vers collection d'associations (enums)
Le principe est identique, appliqué sur un stream() :
if (source.getCategoriesPlat() != null) {
target.setCategoriesPlat(source.getCategoriesPlat().stream().filter(Objects::nonNull).map(CategoriePlat::new).collect(Collectors.toList()));
} else {
target.setCategoriesPlat(null);
}
Intégration dans les classes (mappersInClass)
Lorsque mappersInClass: true, les mappers sont également exposés directement sur les classes qui les déclarent :
- Pour chaque mapper
from, un constructeur est généré. Ce constructeur prend en paramètre lesclassParamsetpropertyParamsdu mapper, et délègue à la méthodeMappers.map[Nom de la classe](..., this)de la classe utilitaire. - Si au moins un
FromMappera tous sesclassParamsdisponibles, un constructeur sans argument est également généré (afin de préserver la possibilité d'instancier la classe sans paramètre). - Pour chaque mapper
to, une méthodetoXXX(target)est générée sur la classe source. Cette méthode délègue àMappers.toXXX(this, target). La version sanstargetn'est pas ajoutée dans la classe (elle n'est disponible que sur la classe utilitaire).
Cette option est désactivable (et l'est par défaut) via mappersInClass: false.
Spécificités JDBC (useJdbc: true)
Lorsque le mode JDBC est activé (useJdbc: true), certains mappings sont filtrés ou simplifiés :
- Les mappings impliquant une composition (côté source ou cible) sont ignorés.
- Les mappings impliquant une association multiple sur une classe persistée (côté source ou cible) sont ignorés.
- Les
null-checks sur les getters des associations ne sont pas générés : la conversion de domaine est appliquée directement sur le getter source.
Ces restrictions reflètent le fait que, en mode JDBC, les relations objet ne sont pas matérialisées par des graphes d'entités.
Gestion des erreurs
Si un paramètre d'entrée obligatoire n'est pas renseigné, l'exception IllegalArgumentException est lancée. Cela concerne :
targetdans les méthodesmapXXXettoXXX(source, target);sourcedans les méthodestoXXX(source, target);- chaque
classParammarquérequired: true; - chaque
propertyParamdont la propriété estrequired(sauf association persistante sur classe persistante).
Configuration
mappersInClass
Indique s'il faut ajouter les mappers en tant que méthode (to...) ou constructeur dans les classes qui les déclarent. Si true, les mappers from sont générés comme constructeurs et les mappers to comme méthodes dans les classes concernées.
Valeur par défaut: false
mapperTagsOverrides
Si un mapper contient au moins une classe dont le tag est listé ici, alors ce mapper sera généré avec les tags de cette classe, au lieu du comportement par défaut qui priorise les tags de la classe persistée puis de celle qui définit le mapper.
Ceci permet de forcer la localisation de la génération des mappers pour certains tags particuliers, indépendamment du caractère persisté des classes impliquées.
Valeur par défaut: []
Variables par tag: non
Exemple :
jpa:
- tags:
- entity
mapperTagsOverrides:
- feign
- dto-special