Selma, ou comment mapper ses objets facilement

Quelle que soit l’application que nous développons, nous sommes très souvent amenés à devoir mapper un objet d’un type vers un autre.

En effet, nos applications communiquant avec des services extérieurs, nous sommes souvent amenés à manipuler des Data Transfer Object.

Ils sont, par exemple, utiles lorsque l'on veut passer d’un objet utilisé pour la persistance de données vers un autre objet qui est utilisé côté client. Ou encore lors de l’appel d’un service web SOAP.

Pour faire cela manuellement, la seule solution est de créer nos classes de mapping. Ce qui peut être fastidieux et long. Le code peut être répétitif avec finalement peu de pertinence.

C’est là qu’entrent en jeu les librairies de mapping qui permettent une conversion entre objets avec plus d’aisance.

Le but de cet article est de faire une courte présentation de Selma, une librairie permettant de convertir facilement un bean Java vers un autre.

Présentation

Pour nous permettre de gagner un temps précieux, Selma met à disposition un jeu d'annotations à utiliser dans une interface de mapping que nous verrons un peu plus bas.

Ensuite, grâce à l’AnnotationProcessor de Java, le code des mappers est généré à la compilation. La classe qui en résulte ressemble alors plus ou moins à ce qu’on aurait pu faire manuellement.

La mise en place est plutôt simple, ajoutez simplement ces deux dépendances suivantes dans votre application.

<dependency>
   <groupId>fr.xebia.extras</groupId>
   <artifactId>selma-processor</artifactId>
   <version>1.0</version>
</dependency>
<dependency>
   <groupId>fr.xebia.extras</groupId>
   <artifactId>selma</artifactId>
   <version>1.0</version>
</dependency>

Exemple de base

L’exemple le plus couramment utilisé pour expliquer le principe de base est celui de la personne : nous avons l’objet de classe Person et son DTO qui contient uniquement les informations utiles côté client.

public class Person extends Animal {
    private String name;
    private String lastName;
    private Date birthDay;
    private Long[] indices;
    private GenderEnum gender;
    private Address address;
    
    // + Getters et Setters
}

public class Animal {
    private int weight;

    // + Getters et Setters
}


public class PersonDTO {
    private String firstName;
    private String lastName;
    private int age;
    private GenderEnumDTO gender;
    private AddressDTO address;
    private int weight;

    // + Getters et Setters
}

Pour passer de l’objet Person à l’objet PersonDTO, une simple interface annotée @Mapper suffit.

@Mapper
public interface PersonMapper {

    PersonDto asPersonDTO(Person source);
}

La stratégie de mapping de base est de faire correspondre les attributs ayant les mêmes noms. Ainsi, sans rien indiquer, la classe implémentant PersonMapper générée par Selma se chargera de remplir les champs lastName et weight de PersonDTO.

À noter que pour cela, Selma utilise les accesseurs et mutateurs. Ils se doivent donc d’être présents, en plus d’avoir un constructeur vide.

Pour appliquer le mapping, rien de plus simple, on peut récupérer l’instance du mapper de différentes manières et ensuite appeler la méthode souhaitée.

// Instance récupérée via outil Selma
PersonMapper personMapper = Selma.mapper(PersonMapper.class)

// via injection de bean
public PersonMapper(PersonMapper personMapper) {
	this.personMapper = personMapper;
}

// exécution du mapping
PersonDto personDto = personMapper.asPersonDto(myPerson);

Cas particuliers

Bien évidemment, tout n’est pas toujours aussi simple, mais pas d’inquiétudes, il existe de nombreuses options nous permettant de traiter des cas plus complexes.

@Mapper(withIgnoreFields = {“indices”})
public interface PersonMapper {

    @Maps(
       withCustomFields = {
          @Field({“names”,“firstName”})
       }
    )
    @EnumMapper(from = GenderEnum.class, to = GenderEnumDto.class)
    PersonDto asPersonDTO(Person source);

    PersonDto updatePersonDTOFromPerson(Person source, PersonDto destination);
}

Dans l’exemple ci-dessus, nous ignorons l’attribut indices de la classe Person, nous mappons l’attribut name vers l’attribut firstName de PersonDTO et enfin @EnumMapper nous permet de mapper deux classes d'énumération différentes.

Si tous ces utilitaires ne sont pas suffisants pour nous permettre de faire notre mapping correctement, il est toujours possible d’utiliser notre propre classe. Ici nous voulons convertir notre Date en un int représentant l'âge de la personne. Nous créons donc une méthode dans notre classe custom. Ainsi durant le mapping, dès que Selma rencontrera un champ à mapper d’un type Date vers un type int, notre méthode sera appelée.

Si cela ne suffit toujours pas, une méthode avec comme argument l’objet de départ et de destination dans notre classe custom sera appelée par Selma à la toute fin du mapping, permettant d’ajouter une petite touche finale si besoin. C’est la méthode interceptor ci-dessous.

@Mapper(withMapper = CustomMapper.class)
public interface PersonMapper {

    PersonDto asPersonDTO(Person source);
}

public class CustomMapper {

   int dateToInt(Date birthDay) {
	  // implémentation de notre conversion
   }

   void interceptor(Person from, PersonDTO to) {
	  // méthode appelée en dernier
   }
}

Conclusion

Selma nous permet donc de simplifier grandement l’implémentation de nos mappings dans des cas simples grâce à de nombreuses fonctionnalités faciles à mettre en place, mais aussi lors de cas un peu plus compliqués en laissant la possibilité de personnaliser notre mapper grâce au mapping custom.

Pour cet article, je me suis inspiré de la documentation de Selma, qui liste toutes les fonctionnalités avec des exemples plus précis : http://www.selma-java.org/

Il existe diverses techniques pour effectuer le mapping d’objet. Une autre librairie : MapStruct, est une alternative viable qui propose également d’autres fonctionnalités. Une autre vision des choses est de préférer faire ces mappings à la main dans des objets dédiés pour plus de maîtrise : mapping par constructeur.