Cartographie avec Orika

Cartographie avec Orika

1. Vue d'ensemble

Orika est un framework de mappage Java Bean querecursively copies data from one object to another. Cela peut être très utile lors du développement d'applications multicouches.

Lorsque vous déplacez des objets de données entre ces couches, il est courant de convertir des objets d'une instance en une autre afin de prendre en charge différentes API.

Voici quelques moyens d'y parvenir:hard coding the copying logic or to implement bean mappers like Dozer. Cependant, il peut être utilisé pour simplifier le processus de mappage entre une couche d'objet et une autre.

Orikauses byte code generation to create fast mappers avec une surcharge minimale, ce qui le rend beaucoup plus rapide que les autres mappeurs basés sur la réflexion commeDozer.

2. Exemple simple

La pierre angulaire de base du cadre de cartographie est la classeMapperFactory. C'est la classe que nous utiliserons pour configurer les mappages et obtenir l'instanceMapperFacade qui effectue le travail de mappage réel.

Nous créons un objetMapperFactory comme ceci:

MapperFactory mapperFactory = new DefaultMapperFactory.Builder().build();

En supposant alors que nous avons un objet de données source,Source.java, avec deux champs:

public class Source {
    private String name;
    private int age;

    public Source(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // standard getters and setters
}

Et un objet de données de destination similaire,Dest.java:

public class Dest {
    private String name;
    private int age;

    public Dest(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // standard getters and setters
}

Voici la cartographie de haricot la plus élémentaire utilisant Orika:

@Test
public void givenSrcAndDest_whenMaps_thenCorrect() {
    mapperFactory.classMap(Source.class, Dest.class);
    MapperFacade mapper = mapperFactory.getMapperFacade();
    Source src = new Source("example", 10);
    Dest dest = mapper.map(src, Dest.class);

    assertEquals(dest.getAge(), src.getAge());
    assertEquals(dest.getName(), src.getName());
}

Comme nous pouvons le constater, nous avons créé un objetDest avec des champs identiques àSource, simplement par mappage. Le mappage bidirectionnel ou inverse est également possible par défaut:

@Test
public void givenSrcAndDest_whenMapsReverse_thenCorrect() {
    mapperFactory.classMap(Source.class, Dest.class).byDefault();
    MapperFacade mapper = mapperFactory.getMapperFacade();
    Dest src = new Dest("example", 10);
    Source dest = mapper.map(src, Source.class);

    assertEquals(dest.getAge(), src.getAge());
    assertEquals(dest.getName(), src.getName());
}

3. Maven Setup

Pour utiliser le mappeur Orika dans nos projets maven, nous devons avoir la dépendanceorika-core danspom.xml:


    ma.glasnost.orika
    orika-core
    1.4.6

La dernière version peut toujours être trouvéehere.

3. Travailler avecMapperFactory

Le modèle général de mappage avec Orika consiste à créer un objetMapperFactory, en le configurant au cas où nous aurions besoin de modifier le comportement de mappage par défaut, en obtenant un objetMapperFacade à partir de celui-ci et enfin, le mappage réel.

Nous observerons ce modèle dans tous nos exemples. Mais notre tout premier exemple a montré le comportement par défaut du mappeur sans aucun ajustement de notre part.

3.1. LesBoundMapperFacade vsMapperFacade

Une chose à noter est que nous pourrions choisir d'utiliserBoundMapperFacade sur lesMapperFacade par défaut, ce qui est assez lent. Ce sont des cas où nous avons une paire spécifique de types à mapper.

Notre test initial deviendrait alors:

@Test
public void givenSrcAndDest_whenMapsUsingBoundMapper_thenCorrect() {
    BoundMapperFacade
      boundMapper = mapperFactory.getMapperFacade(Source.class, Dest.class);
    Source src = new Source("example", 10);
    Dest dest = boundMapper.map(src);

    assertEquals(dest.getAge(), src.getAge());
    assertEquals(dest.getName(), src.getName());
}

Cependant, pour queBoundMapperFacade mappe bidirectionnellement, nous devons appeler explicitement la méthodemapReverse plutôt que la méthode map que nous avons examinée pour le cas desMapperFacade par défaut:

@Test
public void givenSrcAndDest_whenMapsUsingBoundMapperInReverse_thenCorrect() {
    BoundMapperFacade
      boundMapper = mapperFactory.getMapperFacade(Source.class, Dest.class);
    Dest src = new Dest("example", 10);
    Source dest = boundMapper.mapReverse(src);

    assertEquals(dest.getAge(), src.getAge());
    assertEquals(dest.getName(), src.getName());
}

Le test échouera sinon.

3.2. Configurer les mappages de champs

Les exemples que nous avons examinés jusqu'ici impliquent des classes source et de destination avec des noms de champs identiques. Cette sous-section aborde le cas où il existe une différence entre les deux.

Considérons un objet source,Person, avec trois champs à savoirname,nickname etage:

public class Person {
    private String name;
    private String nickname;
    private int age;

    public Person(String name, String nickname, int age) {
        this.name = name;
        this.nickname = nickname;
        this.age = age;
    }

    // standard getters and setters
}

Ensuite, une autre couche de l'application contient un objet similaire, mais écrit par un programmeur français. Disons que cela s'appellePersonne, avec les champsnom,surnom etage, tous correspondant aux trois ci-dessus:

public class Personne {
    private String nom;
    private String surnom;
    private int age;

    public Personne(String nom, String surnom, int age) {
        this.nom = nom;
        this.surnom = surnom;
        this.age = age;
    }

    // standard getters and setters
}

Orika ne peut pas résoudre automatiquement ces différences. Mais nous pouvons utiliser lesClassMapBuilderAPI to register these unique mappings.

Nous l'avons déjà utilisé auparavant, mais nous n'avons encore utilisé aucune de ses puissantes fonctionnalités. La première ligne de chacun de nos tests précédents utilisant la valeur par défautMapperFacade utilisait lesClassMapBuilderAPI to register the two classes we wanted to map:

mapperFactory.classMap(Source.class, Dest.class);

Nous pourrions également mapper tous les champs en utilisant la configuration par défaut, pour le rendre plus clair:

mapperFactory.classMap(Source.class, Dest.class).byDefault()

En ajoutant l'appel de méthodebyDefault(), nous configurons déjà le comportement du mappeur en utilisant lesClassMapBuilderAPI.

Nous voulons maintenant pouvoir mapperPersonne àPerson, donc nous configurons également les mappages de champs sur le mappeur en utilisantClassMapBuilderAPI:

@Test
public void givenSrcAndDestWithDifferentFieldNames_whenMaps_thenCorrect() {
    mapperFactory.classMap(Personne.class, Person.class)
      .field("nom", "name").field("surnom", "nickname")
      .field("age", "age").register();
    MapperFacade mapper = mapperFactory.getMapperFacade();
    Personne frenchPerson = new Personne("Claire", "cla", 25);
    Person englishPerson = mapper.map(frenchPerson, Person.class);

    assertEquals(englishPerson.getName(), frenchPerson.getNom());
    assertEquals(englishPerson.getNickname(), frenchPerson.getSurnom());
    assertEquals(englishPerson.getAge(), frenchPerson.getAge());
}

N'oubliez pas d'appeler la méthode APIregister() afin d'enregistrer la configuration avec lesMapperFactory.

Même si un seul champ diffère, suivre cette route signifie que nous devons enregistrer explicitement les mappages de champsall, y comprisage qui est le même dans les deux objets, sinon le champ non enregistré ne sera pas mappé et le test serait échouer.

Cela deviendra bientôt fastidieux,what if we only want to map one field out of 20, devons-nous configurer tous leurs mappages?

Non, pas lorsque nous demandons au mappeur d’utiliser la configuration de mappage par défaut dans les cas où nous n’avons pas explicitement défini de mappage:

mapperFactory.classMap(Personne.class, Person.class)
  .field("nom", "name").field("surnom", "nickname").byDefault().register();

Ici, nous n'avons pas défini de mapping pour le champage, mais néanmoins le test passera.

3.3. Exclure un champ

En supposant que nous souhaitons exclure le champnom dePersonne du mappage - de sorte que l'objetPerson ne reçoive que de nouvelles valeurs pour les champs qui ne sont pas exclus:

@Test
public void givenSrcAndDest_whenCanExcludeField_thenCorrect() {
    mapperFactory.classMap(Personne.class, Person.class).exclude("nom")
      .field("surnom", "nickname").field("age", "age").register();
    MapperFacade mapper = mapperFactory.getMapperFacade();
    Personne frenchPerson = new Personne("Claire", "cla", 25);
    Person englishPerson = mapper.map(frenchPerson, Person.class);

    assertEquals(null, englishPerson.getName());
    assertEquals(englishPerson.getNickname(), frenchPerson.getSurnom());
    assertEquals(englishPerson.getAge(), frenchPerson.getAge());
}

Remarquez comment nous l'excluons dans la configuration desMapperFactory puis notons également la première assertion où nous nous attendons à ce que la valeur dename dans l'objetPerson restenull, comme en raison de son exclusion du mappage.

4. Cartographie des collections

Parfois, l'objet de destination peut avoir des attributs uniques alors que l'objet source ne conserve que toutes les propriétés d'une collection.

4.1. Listes et tableaux

Prenons l'exemple d'un objet de données source qui n'a qu'un seul champ, une liste des noms d'une personne:

public class PersonNameList {
    private List nameList;

    public PersonNameList(List nameList) {
        this.nameList = nameList;
    }
}

Considérons maintenant notre objet de données de destination qui séparefirstName etlastName dans des champs séparés:

public class PersonNameParts {
    private String firstName;
    private String lastName;

    public PersonNameParts(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }
}

Supposons que nous soyons très sûrs qu’à l’index 0 il y aura toujours lesfirstName de la personne et qu’à l’index 1 il y aura toujours leurslastName.

Orika nous permet d'utiliser la notation entre crochets pour accéder aux membres d'une collection:

@Test
public void givenSrcWithListAndDestWithPrimitiveAttributes_whenMaps_thenCorrect() {
    mapperFactory.classMap(PersonNameList.class, PersonNameParts.class)
      .field("nameList[0]", "firstName")
      .field("nameList[1]", "lastName").register();
    MapperFacade mapper = mapperFactory.getMapperFacade();
    List nameList = Arrays.asList(new String[] { "Sylvester", "Stallone" });
    PersonNameList src = new PersonNameList(nameList);
    PersonNameParts dest = mapper.map(src, PersonNameParts.class);

    assertEquals(dest.getFirstName(), "Sylvester");
    assertEquals(dest.getLastName(), "Stallone");
}

Même si au lieu dePersonNameList, nous avionsPersonNameArray, le même test passerait pour un tableau de noms.

4.2. Maps

En supposant que notre objet source possède une carte de valeurs. Nous savons qu'il existe une clé dans cette carte,first, dont la valeur représente lesfirstName d'une personne dans notre objet de destination.

De même, nous savons qu’il existe une autre clé,last, dans la même carte dont la valeur représente leslastName d’une personne dans l’objet de destination.

public class PersonNameMap {
    private Map nameMap;

    public PersonNameMap(Map nameMap) {
        this.nameMap = nameMap;
    }
}

Comme dans le cas précédent, nous utilisons la notation entre crochets, mais au lieu de passer un index, nous passons à la clé dont la valeur doit être mappée sur le champ de destination donné.

Orika accepte deux manières de récupérer la clé, les deux sont représentées dans le test suivant:

@Test
public void givenSrcWithMapAndDestWithPrimitiveAttributes_whenMaps_thenCorrect() {
    mapperFactory.classMap(PersonNameMap.class, PersonNameParts.class)
      .field("nameMap['first']", "firstName")
      .field("nameMap[\"last\"]", "lastName")
      .register();
    MapperFacade mapper = mapperFactory.getMapperFacade();
    Map nameMap = new HashMap<>();
    nameMap.put("first", "Leornado");
    nameMap.put("last", "DiCaprio");
    PersonNameMap src = new PersonNameMap(nameMap);
    PersonNameParts dest = mapper.map(src, PersonNameParts.class);

    assertEquals(dest.getFirstName(), "Leornado");
    assertEquals(dest.getLastName(), "DiCaprio");
}

Nous pouvons utiliser des guillemets simples ou des guillemets doubles, mais nous devons échapper à ces derniers.

5. Mapper les champs imbriqués

Comme pour les exemples de collections précédents, supposons qu’au sein de notre objet de données source, il existe un autre objet de transfert de données (DTO) contenant les valeurs que nous voulons mapper.

public class PersonContainer {
    private Name name;

    public PersonContainer(Name name) {
        this.name = name;
    }
}
public class Name {
    private String firstName;
    private String lastName;

    public Name(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }
}

Pour pouvoir accéder aux propriétés du DTO imbriqué et les mapper sur notre objet de destination, nous utilisons la notation par points, comme suit:

@Test
public void givenSrcWithNestedFields_whenMaps_thenCorrect() {
    mapperFactory.classMap(PersonContainer.class, PersonNameParts.class)
      .field("name.firstName", "firstName")
      .field("name.lastName", "lastName").register();
    MapperFacade mapper = mapperFactory.getMapperFacade();
    PersonContainer src = new PersonContainer(new Name("Nick", "Canon"));
    PersonNameParts dest = mapper.map(src, PersonNameParts.class);

    assertEquals(dest.getFirstName(), "Nick");
    assertEquals(dest.getLastName(), "Canon");
}

6. Mappage de valeurs nulles

Dans certains cas, vous souhaiterez peut-être contrôler si les valeurs NULL sont mappées ou ignorées lorsqu'elles sont rencontrées. Par défaut, Orika mappera les valeurs nulles lorsqu'elles seront rencontrées:

@Test
public void givenSrcWithNullField_whenMapsThenCorrect() {
    mapperFactory.classMap(Source.class, Dest.class).byDefault();
    MapperFacade mapper = mapperFactory.getMapperFacade();
    Source src = new Source(null, 10);
    Dest dest = mapper.map(src, Dest.class);

    assertEquals(dest.getAge(), src.getAge());
    assertEquals(dest.getName(), src.getName());
}

Ce comportement peut être personnalisé à différents niveaux en fonction de la spécificité souhaitée.

6.1. Configuration globale

Nous pouvons configurer notre mappeur pour mapper les valeurs nulles ou les ignorer au niveau global avant de créer lesMapperFactoryglobaux. Rappelez-vous comment nous avons créé cet objet dans notre tout premier exemple? Cette fois, nous ajoutons un appel supplémentaire pendant le processus de construction:

MapperFactory mapperFactory = new DefaultMapperFactory.Builder()
  .mapNulls(false).build();

Nous pouvons exécuter un test pour confirmer qu'en effet, les valeurs NULL ne sont pas mappées:

@Test
public void givenSrcWithNullAndGlobalConfigForNoNull_whenFailsToMap_ThenCorrect() {
    mapperFactory.classMap(Source.class, Dest.class);
    MapperFacade mapper = mapperFactory.getMapperFacade();
    Source src = new Source(null, 10);
    Dest dest = new Dest("Clinton", 55);
    mapper.map(src, dest);

    assertEquals(dest.getAge(), src.getAge());
    assertEquals(dest.getName(), "Clinton");
}

Qu'est-ce qui se passe est que, par défaut, les valeurs NULL sont mappées. Cela signifie que même si une valeur de champ dans l’objet source estnull et que la valeur du champ correspondant dans l’objet de destination a une valeur significative, elle sera écrasée.

Dans notre cas, le champ de destination n'est pas écrasé si son champ source correspondant a une valeurnull.

6.2. Configuration locale

Le mappage des valeursnull peut être contrôlé sur unClassMapBuilder en utilisant lesmapNulls(true|false) oumapNullsInReverse(true|false) pour contrôler le mappage des valeurs nulles dans le sens inverse.

En définissant cette valeur sur une instanceClassMapBuilder, tous les mappages de champs créés sur les mêmesClassMapBuilder, une fois la valeur définie, prendront la même valeur.

Illustrons cela avec un exemple de test:

@Test
public void givenSrcWithNullAndLocalConfigForNoNull_whenFailsToMap_ThenCorrect() {
    mapperFactory.classMap(Source.class, Dest.class).field("age", "age")
      .mapNulls(false).field("name", "name").byDefault().register();
    MapperFacade mapper = mapperFactory.getMapperFacade();
    Source src = new Source(null, 10);
    Dest dest = new Dest("Clinton", 55);
    mapper.map(src, dest);

    assertEquals(dest.getAge(), src.getAge());
    assertEquals(dest.getName(), "Clinton");
}

Remarquez comment nous appelonsmapNulls juste avant d'enregistrer le champname, cela entraînera l'ignorance de tous les champs suivant l'appel demapNulls lorsqu'ils ont la valeurnull.

Le mappage bidirectionnel accepte également les valeurs nulles mappées:

@Test
public void givenDestWithNullReverseMappedToSource_whenMapsByDefault_thenCorrect() {
    mapperFactory.classMap(Source.class, Dest.class).byDefault();
    MapperFacade mapper = mapperFactory.getMapperFacade();
    Dest src = new Dest(null, 10);
    Source dest = new Source("Vin", 44);
    mapper.map(src, dest);

    assertEquals(dest.getAge(), src.getAge());
    assertEquals(dest.getName(), src.getName());
}

Nous pouvons également éviter cela en appelantmapNullsInReverse et en passantfalse:

@Test
public void
  givenDestWithNullReverseMappedToSourceAndLocalConfigForNoNull_whenFailsToMap_thenCorrect() {
    mapperFactory.classMap(Source.class, Dest.class).field("age", "age")
      .mapNullsInReverse(false).field("name", "name").byDefault()
      .register();
    MapperFacade mapper = mapperFactory.getMapperFacade();
    Dest src = new Dest(null, 10);
    Source dest = new Source("Vin", 44);
    mapper.map(src, dest);

    assertEquals(dest.getAge(), src.getAge());
    assertEquals(dest.getName(), "Vin");
}

6.3. Configuration au niveau du champ

Nous pouvons configurer cela au niveau du champ en utilisantfieldMap, comme ceci:

mapperFactory.classMap(Source.class, Dest.class).field("age", "age")
  .fieldMap("name", "name").mapNulls(false).add().byDefault().register();

Dans ce cas, la configuration n'affectera que le champname comme nous l'avons appelé au niveau du champ:

@Test
public void givenSrcWithNullAndFieldLevelConfigForNoNull_whenFailsToMap_ThenCorrect() {
    mapperFactory.classMap(Source.class, Dest.class).field("age", "age")
      .fieldMap("name", "name").mapNulls(false).add().byDefault().register();
    MapperFacade mapper = mapperFactory.getMapperFacade();
    Source src = new Source(null, 10);
    Dest dest = new Dest("Clinton", 55);
    mapper.map(src, dest);

    assertEquals(dest.getAge(), src.getAge());
    assertEquals(dest.getName(), "Clinton");
}

7. Cartographie personnalisée Orika

Jusqu'à présent, nous avons examiné des exemples simples de mappage personnalisé en utilisant lesClassMapBuilderAPI. We shall still use the same API but customize our mapping using Orika’s CustomMapper class.

En supposant que nous ayons deux objets de données chacun avec un certain champ appelédtob, représentant la date et l'heure de la naissance d'une personne.

Un objet de données représente cette valeur sous la forme d'undatetime String au format ISO suivant:

2007-06-26T21:22:39Z

et l'autre représente la même chose qu'un typelong dans le format d'horodatage Unix suivant:

1182882159000

De toute évidence, aucune des personnalisations que nous avons couvertes jusqu'à présent ne suffit pour convertir entre les deux formats pendant le processus de mappage, même le convertisseur intégré d'Orika ne peut pas gérer le travail. C'est là que nous devons écrire unCustomMapper pour effectuer la conversion requise lors du mappage.

Laissez-nous créer notre premier objet de données:

public class Person3 {
    private String name;
    private String dtob;

    public Person3(String name, String dtob) {
        this.name = name;
        this.dtob = dtob;
    }
}

alors notre deuxième objet de données:

public class Personne3 {
    private String name;
    private long dtob;

    public Personne3(String name, long dtob) {
        this.name = name;
        this.dtob = dtob;
    }
}

Nous n'indiquons pas quelle est la source et quelle est la destination pour le moment car lesCustomMapper nous permettent de gérer le mappage bidirectionnel.

Voici notre implémentation concrète de la classe abstraiteCustomMapper:

class PersonCustomMapper extends CustomMapper {

    @Override
    public void mapAtoB(Personne3 a, Person3 b, MappingContext context) {
        Date date = new Date(a.getDtob());
        DateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
        String isoDate = format.format(date);
        b.setDtob(isoDate);
    }

    @Override
    public void mapBtoA(Person3 b, Personne3 a, MappingContext context) {
        DateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
        Date date = format.parse(b.getDtob());
        long timestamp = date.getTime();
        a.setDtob(timestamp);
    }
};

Notez que nous avons implémenté les méthodesmapAtoB etmapBtoA. La mise en œuvre des deux rend notre fonction de cartographie bidirectionnelle.

Each method exposes the data objects we are mapping and we take care of copying the field values from one to the other.

C'est là que nous écrivons le code personnalisé pour manipuler les données source selon nos besoins avant de les écrire dans l'objet de destination.

Lançons un test pour confirmer que notre mappeur personnalisé fonctionne:

@Test
public void givenSrcAndDest_whenCustomMapperWorks_thenCorrect() {
    mapperFactory.classMap(Personne3.class, Person3.class)
      .customize(customMapper).register();
    MapperFacade mapper = mapperFactory.getMapperFacade();
    String dateTime = "2007-06-26T21:22:39Z";
    long timestamp = new Long("1182882159000");
    Personne3 personne3 = new Personne3("Leornardo", timestamp);
    Person3 person3 = mapper.map(personne3, Person3.class);

    assertEquals(person3.getDtob(), dateTime);
}

Notez que nous transmettons toujours le mappeur personnalisé au mappeur d'Orika viaClassMapBuilderAPI, just like all other simple customizations.

Nous pouvons également confirmer que la cartographie bidirectionnelle fonctionne:

@Test
public void givenSrcAndDest_whenCustomMapperWorksBidirectionally_thenCorrect() {
    mapperFactory.classMap(Personne3.class, Person3.class)
      .customize(customMapper).register();
    MapperFacade mapper = mapperFactory.getMapperFacade();
    String dateTime = "2007-06-26T21:22:39Z";
    long timestamp = new Long("1182882159000");
    Person3 person3 = new Person3("Leornardo", dateTime);
    Personne3 personne3 = mapper.map(person3, Personne3.class);

    assertEquals(person3.getDtob(), timestamp);
}

8. Conclusion

Dans cet article, nous avonsexplored the most important features of the Orika mapping framework.

Il existe certainement des fonctionnalités plus avancées qui nous donnent beaucoup plus de contrôle, mais dans la plupart des cas d'utilisation, celles décrites ici seront plus que suffisantes.

Le code complet du projet et tous les exemples se trouvent dans mygithub project. N'oubliez pas de consulter également notre tutoriel sur lesDozer mapping framework, car ils résolvent tous les deux plus ou moins le même problème.