COMMENT FAIRE UN MAPPING ENTRE DEUX MODÈLES DE DONNÉES GRÂCE À MAPSTRUCT

I/ PRÉSENTATION

mapstruct

1/ PRÉSENTATION MAPSTRUCT

Dans l’architecture d’une applicative n-tiers, une couche de mapping objet/objet peut intervenir à plusieurs niveaux, il existe plusieurs frameworks Java qui permettent d’automatiser ce mapping, néanmoins les performances du code écrit à la main sont les meilleures.

MapStruct est l’un des frameworks Java de mapping objet/objet qui simplifie grandement la mise en œuvre des mappings entre deux modèles de données et dont les performances se rapprochent le plus des performances d’un code écrit manuellement. Ces bonnes performances s’expliquent par le fait qu’il génère le code source à l’aide de l’Annotation Processor introduit par Java 6 (JSR-269).

Le code de mappage généré par MapStruct utilise des invocations de méthodes simples. Il est donc rapide, sécurisé et facile à comprendre.

L’objectif de cet article est de vous montrer et de vous expliquer comment faire un mapping entre deux modèles de données très différents l’un de l’autre.

 

2/ PRÉSENTATION DES MODÈLES DE DONNÉES

  • 1er Modèle de données :

Le premier modèle de données est un peu complexe, la classe principale est « InsuranceAgreement », elle est constituée d’un ensemble d’attributs. Parmi ces attributs il y a une liste d’objets de type « AgreementComponent ». Ce type est une généralisation de trois classes : « AgreementDeductibleComponent »,  « AgreementLimitComponent » et  « CoverageComponent ».

La classe « CoverageComponent » contient une liste de « CoverageDeductible », une liste de « CoverageLimit » et une instance de l’objet  « CoverageOption ».

Les classes « AgreementDeductibleComponent », « AgreementLimitComponent », « CoverageComponent », « CoverageDeductible », « CoverageLimit » et  « CoverageOption »  contiennent chacune une instance de l’objet « CurrencyAmount ».

image001

Cliquer pour agrandir

  • 2 ème modèle de données :

Ce modèle de données est plus simple que le premier, il ne contient que quatre classes, une classe « Contrat » composée d’un ensemble d’attributs. Parmi ces attributs on trouve deux listes : la première est une liste de limites globales (de type « Limite ») et la deuxième est une liste de « Garantie » (chaque « Garantie » contient une liste de « Limite » et une liste de « Franchise » propres à la garantie).

image003

Cliquer pour agrandir

 

II/ MAPPING DES MODÈLES

 

Pour mapper ces deux modèles il faut :

1 – Mapper « InsuranceAgreement » avec « Contrat »

2 – Mapper « CoverageComponent » avec « Garantie »

3 – Mapper « AgreementLimitComponent » avec « Limite »

4 – Mapper « CoverageLimit » avec « Limite »

5 – Mapper « CoverageDeductible » avec « Franchise »

6 – Mapper « CurrencyAmount » avec le champ montant de type double

 

1/ PROBLÈMATIQUE

Le problème auquel on va faire face pour mapper ces deux modèles est le mapping de la classe « InsuranceAgreement » avec « Contrat » : puisque « InsuranceAgreement » contient une seule liste de « IncludesComponent » alors que « Contrat » contient deux listes.

 

2/ SOLUTION

On va créer une classe qu’on appellera dans notre exemple « ContratUtils » dans laquelle on implémentera deux méthodes :

  • Une première méthode « mapContrat » prend en paramètre un objet de type «InsuranceAgreement » et renvoie en retour un objet de type « Contrat ». Cette méthode crée deux listes de type « CoverageComponent » et de type « AgreementLimitComponent » à partir de la liste « IncludesComponent » et les mappe respectivement avec la liste des « Garantie » et la liste des « LimiteGlobale » du contrat lors de l’appel de la méthode « insuranceAgreementToContrat» du Mapper.

Code 

public Contrat mapContrat(InsuranceAgreement insuranceAgreement) {

if (insuranceAgreement == null)
return null;

List<CoverageComponent> listCoverageComponent = new ArrayList<CoverageComponent>();
List<AgreementLimitComponent> listAgreementLimitComponent = new ArrayList<AgreementLimitComponent>();

if( insuranceAgreement.getIncludesComponent() != null
&& insuranceAgreement.getIncludesComponent().size()!=0
){

for (AgreementComponent agreementComponent : insuranceAgreement.getIncludesComponent()) {

if(agreementComponent instanceof CoverageComponent){
listCoverageComponent.add((CoverageComponent)agreementComponent);
}

if(agreementComponent instanceof AgreementLimitComponent){
listAgreementLimitComponent.add((AgreementLimitComponent)agreementComponent);
}
}
}

return InsuranceAgreementMapper.INSTANCE.insuranceAgreementToContrat(insuranceAgreement, listCoverageComponent, listAgreementLimitComponent);
}

  •  La deuxième méthode « mapInsuranceAgreement » prend en paramètre un objet de type « Contrat » et renvoie en retour un objet de type «InsuranceAgreement » et elle crée la liste des « AgreementComponent » à partir des deux listes des « Garantie » et des « LimiteGlobale » du contrat puis appelle la méthode « contratToInsuranceAgreement » du Mapper.

 

Code

public InsuranceAgreement mapInsuranceAgreement( Contrat contrat) {

if (contrat == null)
return null;

List<AgreementComponent> listAgreementComponent = new ArrayList<AgreementComponent>();

if( contrat.getGaranties() != null
&& contrat.getGaranties().size()!=0
){

for (Garantie garantie : contrat.getGaranties()) {
listAgreementComponent.add(InsuranceAgreementMapper.INSTANCE.garantieToCoverage
Component(garantie));
}
}

if( contrat.getLimiteGlobales() != null
&& contrat.getLimiteGlobales().size()!=0
){
for (Limite limiteGlobale : contrat.getLimiteGlobales()) {
listAgreementComponent.add(InsuranceAgreementMapper.INSTANCE.limiteGlobaleToAgreement
LimitComponent(limiteGlobale));
}
}

return InsuranceAgreementMapper.INSTANCE.contratToInsuranceAgreement(contrat, listAgreementComponent);
}

 

3/ DÉPENDANCES MAVEN

La 1ere étape consiste à ajouter les dépendances vers les bibliothèques MapStruct :

<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId><version>1.0.0.Final</version>
</dependency>

<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.0.0.Final</version>
</dependency>

 

4/ CRÉATION DU MAPPER

Créer une interface, dans notre exemple on l’appellera « InsuranceAgreementMapper », et ajouter l’annotation @Mapper. Cette annotation marque l’interface comme interface de mappage et permet au processeur MapStruct de démarrer pendant la compilation.

 

@Mapper

public interface InsuranceAgreementMapper

 

Une instance de l’implémentation d’interface peut être extraite de la classe Mappers. Par convention, l’interface déclare un attribut INSTANCE, fournissant aux clients un accès à l’implémentation du mappeur.

 

InsuranceAgreementMapper INSTANCE = Mappers
.getMapper(InsuranceAgreementMapper.class);

 

Chaque méthode de mappage prend en entrée l’objet source comme paramètre et renvoie l’objet cible, le libellé de la méthode peut être librement choisi.
On doit créer deux méthodes de mappage pour mapper deux classes dans les deux sens.

 

Pour les attributs de même nom, le mappage est automatique alors que pour les attributs avec des noms différents dans l’objet source et cible, l’annotation @Mapping peut être utilisée pour configurer les noms.

 

5/ MÉTHODE DE MAPPING DES CLASSES INSURANCEAGREEMENT ET CONTRAT

On appellera « contratToInsuranceAgreement » la méthode qui va mapper un objet de type « Contrat » à un objet de type  « InsuranceAgreement ».
Cette méthode prend en paramètre  un objet de type « Contrat » et la liste des « AgreementComponent » créée par la méthode « mapInsuranceAgreement » de la classe « ContratUtils » et renvoie en retour un objet de type « InsuranceAgreement ».

 

@Mappings({
@Mapping(target = « productCode », source = « contrat.codeProduit »),
@Mapping(target = « identifier », source = « contrat.identifiant »),
@Mapping(target = « includesComponent », source = « listAgreementComponent ») })
InsuranceAgreement contratToInsuranceAgreement(Contrat contrat,
List<AgreementComponent> listAgreementComponent);

On appellera « insuranceAgreementToContrat » la méthode qui va mapper un objet de type « InsuranceAgreement » à un objet de type  «Contrat ».

 

Cette méthode prend en paramètre  un objet de type « InsuranceAgreement » et deux listes, la liste des « CoverageComponent» et la liste des « AgreementLimitComponent», créées par la méthode « mapContrat » de la classe « ContratUtils » et renvoie en retour un objet de type « InsuranceAgreement ».

@Mappings({
@Mapping(target = « codeProduit », source = « insuranceAgreement.productCode »),
@Mapping(target = « identifiant », source = « insuranceAgreement.identifier »),
@Mapping(target = « garanties », source = « listeCoverageComponent »),
@Mapping(target = « limiteGlobales », source = « listAgreementLimitComponent ») })
Contrat insuranceAgreementToContrat(InsuranceAgreement insuranceAgreement, List<CoverageComponent> listeCoverageComponent,
List<AgreementLimitComponent> listAgreementLimitComponent);

 

6/ MÉTHODE DE MAPPING DES CLASSES COVERAGECOMPONENT ET GARANTIE

La méthode « garantieToCoverageComponent » permet de mapper un objet de type « Garantie » à un objet de type « CoverageComponent ».

@Mappings({
@Mapping(target = « isSelected », source = « selectionnee »),
@Mapping(target = « agreementCoverageLabel », source = « libelleGarantie »),
@Mapping(target = « coverageCode », source = « codeGarantie »),
@Mapping(target = « deductible », source = « franchises »),
@Mapping(target = « limit », source = « limites »),
@Mapping(target = « option », ignore = true),
@Mapping(target = « agreementCoverageFamilly », source = « typeGarantie ») })
CoverageComponent garantieToCoverageComponent(Garantie garantie);

 

Si tous les paramètres des deux types à mapper sont de type simple, on peut créer la méthode de mapping dans le deuxième sens de la façon suivante :

@InheritInverseConfiguration
Garantie coverageComponentToGarantie(CoverageComponent coverageComponent);

Pour créer une méthode permettant de mapper une liste d’objets de type « Garantie » à une liste d’objets de type « CoverageComponent », il suffit de créer une nouvelle méthode qui prend en paramètre une liste d’objets de type source et renvoie une liste d’objets de type cible.

List<Garantie> coverageComponentToGaranties(List<CoverageComponent> coverageComponent);

 

7/ MÉTHODE DE MAPPING DES CLASSES AGREEMENTLIMITCOMPONENT ET LIMITE

@Mappings({
@Mapping(target = « agreementLimitAmount », source = « montantLimite »),
@Mapping(target = « agreementLimitCode », source = « codeLimite »),
@Mapping(target = « agreementLimitLabel », source = « libelleLimite »),
@Mapping(target = « agreementLimitValue », source = « valeurLimite »),
@Mapping(target = « agreementLimitType », source = « typeLimite »),
@Mapping(target = « agreementLimitUnit », source = « uniteLimite ») })
AgreementLimitComponent limiteGlobaleToAgreementLimitComponent(Limite limiteGlobale);

List<AgreementLimitComponent> limiteGlobalesToAgreementLimitComponent(List<Limite> limiteGlobale);

 

@Mappings({
@Mapping(target = « montantLimite », source = « agreementLimitComponent.agreementLimitAmount.amount »),
@Mapping(target = « codeLimite », source = « agreementLimitCode »),
@Mapping(target = « libelleLimite », source = « agreementLimitLabel »),
@Mapping(target = « valeurLimite », source = « agreementLimitValue »),
@Mapping(target = « typeLimite », source = « agreementLimitType »),
@Mapping(target = « uniteLimite », source = « agreementLimitUnit ») })
Limite agreementLimitComponentToLimiteGlobales(AgreementLimitComponent agreementLimitComponent);

 

List<Limite> agreementLimitComponentToLimiteGlobales(List<AgreementLimitComponent> agreementLimitComponents);

 

8/ MÉTHODE DE MAPPING DES CLASSES COVERAGEDEDUCTIBLE ET FRANCHISE

@Mappings({
@Mapping(target = « coverageDeductibleAmount », source = « montantFranchise »),
@Mapping(target = « coverageDeductibleCode », source = « codeFranchise »),
@Mapping(target = « coverageDeductibleLabel », source = « libelleFranchise »),
@Mapping(target = « coverageDeductibleValue », source = « valeurFranchise »),
@Mapping(target = « coverageDeductibleType », source = « typeFranchise »),
@Mapping(target = « coverageDeductibleUnit », source = « uniteFranchise ») })
CoverageDeductible franchiseToCoverageDeductible(Franchise franchise);

List<CoverageDeductible> FranchisesToCoverageDeductible(List<Franchise> franchise);

 

@Mappings({
@Mapping(target = « montantFranchise », source = « coverageDeductible.coverageDeductibleAmount.amount »),
@Mapping(target = « codeFranchise », source = « coverageDeductibleCode »),
@Mapping(target = « libelleFranchise » , source = « coverageDeductibleLabel » ),
@Mapping(target = « valeurFranchise », source = « coverageDeductibleValue »),
@Mapping(target = « typeFranchise », source = « coverageDeductibleType »),
@Mapping(target = « uniteFranchise », source = « coverageDeductibleUnit » ) })
Franchise coverageDeductibleToFranchises(CoverageDeductible coverageDeductible);

List<Franchise> coverageDeductibleToFranchises(List<CoverageDeductible> coverageDeductible);

 

9/ MÉTHODE DE MAPPING DES CLASSES AGREEMENTLIMITCOMPONENT ET LIMITE

@Mappings({
@Mapping(target = « coverageLimitAmount », source = « montantLimite »),
@Mapping(target = « coverageLimitCode », source = « codeLimite »),
@Mapping(target = « coverageLimitLabel », source = « libelleLimite »),
@Mapping(target = « coverageLimitValue », source = « valeurLimite »),
@Mapping(target = « coverageLimitType », source = « typeLimite »),
@Mapping(target = « coverageLimitUnit », source = « uniteLimite ») })
CoverageLimit limiteToCoverageLimit(Limite limite);

List<CoverageLimit> limitesToCoverageLimit(List<Limite> limite);

 

@Mappings({
@Mapping(target = « montantLimite », source = « coverageLimit.coverageLimitAmount.amount »),
@Mapping(target = « codeLimite », source = « coverageLimitCode »),
@Mapping(target = « libelleLimite », source = « coverageLimitLabel »),
@Mapping(target = « valeurLimite », source = « coverageLimitValue »),
@Mapping(target = « typeLimite », source = « coverageLimitType »),
@Mapping(target = « uniteLimite », source = « coverageLimitUnit ») })
Limite coverageLimitToLimite(CoverageLimit coverageLimit);

List<Limite> coverageLimitToLimites(List<CoverageLimit> coverageLimits);

 

10/ MÉTHODE DE MAPPING DE LA CLASSE CURRENCYAMOUNT AVEC LE CHAMP MONTANT DE TYPE DOUBLE

@Mappings({
@Mapping(target = « amount », source= »montant » )})
CurrencyAmount limiteToCurrencyAmount(Double montant);

 

III/ CONCLUSION

 

Comme on a vu dans notre exemple, MapStruct offre des facilités pour écrire le code de mapping objet/objet, même pour le mapping de deux modèles totalement différents. Le mapping pourrait être beaucoup plus simple si les classes à mapper avaient les mêmes noms de variables et si on n’avait pas le problème d’héritage. Un autre grand avantage de MapStruct est que toute la magie de ce framework se passe pendant la phase de compilation ce qui le rend utile et sûr. En fait, pendant cette phase, MapStruct génère un joli code source lisible qui va prendre en charge la tâche de mapping.

MapStruct a beaucoup plus de caractéristiques, que vous pouvez découvrir par vous-même sur http://mapstruct.org.

 

Par Aymen Sliti, Ingénieur études et développement JAVA/J2EE chez AXONES