web-dev-qa-db-fra.com

Comment personnaliser ModelMapper

Je veux utiliser ModelMapper pour convertir l'entité en DTO et vice-versa. Généralement, cela fonctionne, mais comment le personnaliser. Il a tellement d'options qu'il est difficile de savoir par où commencer. Quelle est la meilleure pratique?

Je vais y répondre moi-même ci-dessous, mais si une autre réponse est meilleure, je l'accepterai.

8
John Henckel

Voici d'abord quelques liens

Mon impression de mm est qu'il est très bien conçu. Le code est solide et agréable à lire. Cependant, la documentation est très concise, avec très peu d'exemples. L'API est également déroutant car il semble y avoir 10 façons de faire quoi que ce soit, et aucune indication de la raison pour laquelle vous le faites d'une manière ou d'une autre.

Il existe deux alternatives: Dozer est la plus populaire, et Orika obtient de bonnes critiques pour une facilité d'utilisation.

En supposant que vous souhaitiez toujours utiliser mm, voici ce que j'ai appris à ce sujet.

La classe principale, ModelMapper, doit être un singleton dans votre application. Pour moi, cela signifiait un @Bean utilisant Spring. Cela fonctionne hors de la boîte pour les cas simples. Par exemple, supposons que vous ayez deux classes:

class DogData
{
    private String name;
    private int mass;
}

class DogInfo
{
    private String name;
    private boolean large;
}

avec des getters/setters appropriés. Tu peux le faire:

    ModelMapper mm = new ModelMapper();
    DogData dd = new DogData();
    dd.setName("fido");
    dd.setMass(70);
    DogInfo di = mm.map(dd, DogInfo.class);

et le "nom" sera copié de dd vers di.

Il existe de nombreuses façons de personnaliser mm, mais vous devez d'abord comprendre comment cela fonctionne.

L'objet mm contient un TypeMap pour chaque paire ordonnée de types, tels que <DogInfo, DogData> et <DogData, DogInfo> serait deux TypeMaps.

Chaque TypeMap contient un PropertyMap avec une liste de mappages. Ainsi, dans l'exemple, le mm crée automatiquement un TypeMap <DogData, DogInfo> qui contient un PropertyMap qui a un seul mappage.

Nous pouvons écrire ceci

    TypeMap<DogData, DogInfo> tm = mm.getTypeMap(DogData.class, DogInfo.class);
    List<Mapping> list = tm.getMappings();
    for (Mapping m : list)
    {
        System.out.println(m);
    }

et il sortira

PropertyMapping[DogData.name -> DogInfo.name]

Lorsque vous appelez mm.map () c'est ce qu'il fait,

  1. voir si le TypeMap existe encore, sinon créer le TypeMap pour les types de source/destination <S, D>
  2. appeler la condition TypeMap , si elle retourne FALSE, ne rien faire et STOP
  3. appeler le fournisseur TypeMap pour construire un nouvel objet de destination si nécessaire
  4. appeler le TypeMap PreConverter s'il en a un
  5. effectuez l'une des actions suivantes:
    • si le TypeMap a un convertisseur personnalisé , appelez-le
    • ou, générez un PropertyMap (basé sur drapeaux de configuration plus tout mappages personnalisés qui ont été ajoutés), et utilisez-les (Remarque: le TypeMap possède également des Pre/PostPropertyConverters personnalisés facultatifs que je pense s'exécutera à ce point avant et après chaque mappage .)
  6. appeler le TypeMap PostConverter s'il en a un

Mise en garde: cet organigramme est en quelque sorte documenté mais je devais en deviner beaucoup, donc ce n'est peut-être pas tout à fait correct!

Vous pouvez personnaliser chaque étape de ce processus. Mais les deux plus courants sont

  • étape 5a. - écrire un convertisseur TypeMap personnalisé, ou
  • étape 5b. - écrire un mappage de propriété personnalisé.

Voici un exemple d'un convertisseur TypeMap personnalisé :

    Converter<DogData, DogInfo> myConverter = new Converter<DogData, DogInfo>()
    {
        public DogInfo convert(MappingContext<DogData, DogInfo> context)
        {
            DogData s = context.getSource();
            DogInfo d = context.getDestination();
            d.setName(s.getName());
            d.setLarge(s.getMass() > 25);
            return d;
        }
    };

    mm.addConverter(myConverter);

Remarque le convertisseur est unidirectionnel . Vous devez en écrire un autre si vous souhaitez personnaliser DogInfo en DogData.

Voici un exemple de PropertyMap personnalisé :

    Converter<Integer, Boolean> convertMassToLarge = new Converter<Integer, Boolean>()
    {
        public Boolean convert(MappingContext<Integer, Boolean> context)
        {
            // If the dog weighs more than 25, then it must be large
            return context.getSource() > 25;
        }
    };

    PropertyMap<DogData, DogInfo> mymap = new PropertyMap<DogData, DogInfo>()
    {
        protected void configure()
        {
            // Note: this is not normal code. It is "EDSL" so don't get confused
            map(source.getName()).setName(null);
            using(convertMassToLarge).map(source.getMass()).setLarge(false);
        }
    };

    mm.addMappings(mymap);

La fonction pm.configure est vraiment géniale. Ce n'est pas du vrai code. C'est un mannequin code EDSL qui est interprété d'une manière ou d'une autre. Par exemple, le paramètre du setter n'est pas pertinent, c'est juste un espace réservé. Vous pouvez faire beaucoup de choses ici, comme

  • quand (condition) .map (getter) .setter
  • when (condition) .skip (). setter - ignorer le champ en toute sécurité.
  • using (converter) .map (getter) .setter - convertisseur de champ personnalisé
  • with (provider) .map (getter) .setter - constructeur de champ personnalisé

Remarque les mappages personnalisés sont ajoutés aux mappages par défaut, vous le faites donc pas besoin, par exemple, de spécifier

            map(source.getName()).setName(null);

dans votre PropertyMap.configure () personnalisé.

Dans cet exemple, j'ai dû écrire un convertisseur pour mapper l'entier en booléen. Dans la plupart des cas, cela ne sera pas nécessaire car mm convertira automatiquement Integer en String, etc.

On me dit que vous pouvez également créer des mappages en utilisant Java 8 lambda expressions. J'ai essayé, mais je n'ai pas pu le comprendre.

Recommandations finales et meilleures pratiques

Par défaut, mm utilise MatchingStrategies.STANDARD, Ce qui est dangereux. Il peut facilement choisir le mauvais mappage et provoquer des bogues étranges et difficiles à trouver. Et si l'année prochaine quelqu'un d'autre ajoute une nouvelle colonne à la base de données? Alors ne le fais pas. Assurez-vous d'utiliser le mode STRICT:

    mm.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT);

Écrivez toujours des tests unitaires et assurez-vous que tous les mappages sont validés.

    DogInfo di = mm.map(dd, DogInfo.class);
    mm.validate();   // make sure nothing in the destination is accidentally skipped

Corrigez tous les échecs de validation avec mm.addMappings() comme indiqué ci-dessus.

Placez tous vos mappages dans un emplacement central, où le singleton mm est créé.

61
John Henckel

Je l'utilise depuis 6 mois, je vais expliquer certaines de mes réflexions à ce sujet:

Tout d'abord, il est recommandé de l'utiliser comme une instance unique (singleton, spring bean, ...), cela est expliqué dans le manuel, et je pense que tous sont d'accord avec cela.

ModelMapper est une excellente bibliothèque de cartographie et très flexible. En raison de sa flexibilité, il existe de nombreuses façons d'obtenir le même résultat, et c'est pourquoi il devrait figurer dans le manuel des meilleures pratiques pour savoir quand utiliser l'une ou l'autre façon de faire la même chose.

Commencer par ModelMapper est un peu difficile, il a une courbe d'apprentissage très serrée et parfois il n'est pas facile de comprendre les meilleures façons de faire quelque chose, ou comment faire autre chose. Donc, pour commencer, il est nécessaire de lire et de comprendre le manuel avec précision.

Vous pouvez configurer votre mappage comme vous le souhaitez en utilisant les paramètres suivants:

Access level
Field matching
Naming convention
Name transformer
Name tokenizer 
Matching strategy

La configuration par défaut est tout simplement la meilleure ( http://modelmapper.org/user-manual/configuration/ ), mais si vous souhaitez la personnaliser, vous pouvez le faire.

Juste une chose liée à la configuration de la stratégie de correspondance, je pense que c'est la configuration la plus importante et qu'il faut y faire attention. J'utiliserais le Strict ou Standard mais jamais le Loose, pourquoi?

  • Due Loose est le mappeur le plus flexible et le plus intelligent. Alors, définitivement, soyez prudent avec cela. Je pense qu'il est préférable de créer votre propre PropertyMap et d'utiliser des convertisseurs si nécessaire au lieu de le configurer en tant que Loose.

Sinon, il est important de validate toutes les correspondances de propriété, vous vérifiez tout cela fonctionne, et avec ModelMapper c'est plus nécessaire en raison du mappage intelligent, cela se fait par réflexion afin que vous n'ayez pas l'aide du compilateur, il continuera à compiler mais le mappage échouera sans s'en rendre compte. C'est l'une des choses que j'aime le moins, mais elle doit éviter le passe-partout et la cartographie manuelle.

Enfin, si vous êtes sûr d'utiliser ModelMapper dans votre projet, vous devez l'utiliser en utilisant la façon dont il le propose, ne le mélangez pas avec des mappages manuels (par exemple), utilisez simplement ModelMapper, si vous ne savez pas comment faire quelque chose sûr est possible (enquêter, ...). Il est parfois difficile de le faire avec le mappeur de modèles (je n'aime pas non plus) comme le fait à la main, mais c'est le prix à payer pour éviter les mappages standard dans d'autres POJO.

2
Pau