Cartographie des collections avec MapStruct

1. Vue d'ensemble

Dans ce didacticiel, nous allons voir comment mapper des collections d'objets à l'aide de MapStruct.

Étant donné que cet article suppose déjà une compréhension de base de MapStruct, les débutants devraient d'abord consulter notre guide rapide de MapStruct.

2. Cartographie des collections

En général, le mappage de collections avec MapStruct fonctionne de la même manière que pour les types simples .

Fondamentalement, nous devons créer une interface simple ou une classe abstraite et déclarer les méthodes de mappage. Sur la base de nos déclarations, MapStruct générera automatiquement le code de mappage. En règle générale, le code généré boucle sur la collection source, convertit chaque élément en type cible et inclut chacun d'entre eux dans la collection cible .

Jetons un coup d'œil à un exemple simple.

2.1. Listes de mappage

Tout d'abord, pour notre exemple, considérons un simple POJO comme source de mappage pour notre mappeur:

public class Employee { private String firstName; private String lastName; // constructor, getters and setters } 

La cible sera un simple DTO:

public class EmployeeDTO { private String firstName; private String lastName; // getters and setters }

Ensuite, définissons notre mappeur:

@Mapper public interface EmployeeMapper { List map(List employees); } 

Enfin, regardons le code MapStruct généré à partir de notre interface EmployeeMapper :

public class EmployeeMapperImpl implements EmployeeMapper { @Override public List map(List employees) { if (employees == null) { return null; } List list = new ArrayList(employees.size()); for (Employee employee : employees) { list.add(employeeToEmployeeDTO(employee)); } return list; } protected EmployeeDTO employeeToEmployeeDTO(Employee employee) { if (employee == null) { return null; } EmployeeDTO employeeDTO = new EmployeeDTO(); employeeDTO.setFirstName(employee.getFirstName()); employeeDTO.setLastName(employee.getLastName()); return employeeDTO; } } 

Il y a une chose importante à noter. Plus précisément, MapStruct a généré pour nous, automatiquement, le mappage de Employee à EmployeeDTO .

Il y a des cas où cela n'est pas possible. Par exemple, disons que nous voulons mapper notre modèle Employé au modèle suivant:

public class EmployeeFullNameDTO { private String fullName; // getter and setter }

Dans ce cas, si nous déclarons simplement la méthode de mappage d'une liste d' employés à une liste d' employésFullNameDTO, nous recevrons une erreur de compilation ou un avertissement comme:

Warning:(11, 31) java: Unmapped target property: "fullName". Mapping from Collection element "com.baeldung.mapstruct.mappingCollections.model.Employee employee" to "com.baeldung.mapstruct.mappingCollections.dto.EmployeeFullNameDTO employeeFullNameDTO".

Fondamentalement, cela signifie que MapStruct ne pouvait pas générer le mappage automatiquement pour nous dans ce cas . Par conséquent, nous devons définir, manuellement, le mappage entre Employee et EmployeeFullNameDTO.

Compte tenu de ces points, définissons-le manuellement:

@Mapper public interface EmployeeFullNameMapper { List map(List employees); default EmployeeFullNameDTO map(Employee employee) { EmployeeFullNameDTO employeeInfoDTO = new EmployeeFullNameDTO(); employeeInfoDTO.setFullName(employee.getFirstName() + " " + employee.getLastName()); return employeeInfoDTO; } }

Le code généré utilisera la méthode que nous avons définie pour mapper les éléments de la liste source à la liste cible .

Ceci s'applique également en général. Si nous avons défini une méthode qui mappe le type d'élément source au type d'élément cible, MapStruct l'utilisera.

2.2. Ensembles de mappage et cartes

Les ensembles de mappage avec MapStruct fonctionnent de la même manière qu'avec les listes. Par exemple, disons que nous voulons mapper un ensemble d' instances Employee à un ensemble d' instances EmployeeDTO .

Comme auparavant, nous avons besoin d'un mappeur:

@Mapper public interface EmployeeMapper { Set map(Set employees); }

Et MapStruct générera le code approprié:

public class EmployeeMapperImpl implements EmployeeMapper { @Override public Set map(Set employees) { if (employees == null) { return null; } Set set = new HashSet(Math.max((int)(employees.size() / .75f ) + 1, 16)); for (Employee employee : employees) { set.add(employeeToEmployeeDTO(employee)); } return set; } protected EmployeeDTO employeeToEmployeeDTO(Employee employee) { if (employee == null) { return null; } EmployeeDTO employeeDTO = new EmployeeDTO(); employeeDTO.setFirstName(employee.getFirstName()); employeeDTO.setLastName(employee.getLastName()); return employeeDTO; } }

La même chose s'applique aux cartes. Considérons que nous voulons mapper une carte à une carte .

Ensuite, nous pouvons suivre les mêmes étapes que précédemment:

@Mapper public interface EmployeeMapper { Map map(Map idEmployeeMap); }

Et MapStruct fait son travail:

public class EmployeeMapperImpl implements EmployeeMapper { @Override public Map map(Map idEmployeeMap) { if (idEmployeeMap == null) { return null; } Map map = new HashMap(Math.max((int)(idEmployeeMap.size() / .75f) + 1, 16)); for (java.util.Map.Entry entry : idEmployeeMap.entrySet()) { String key = entry.getKey(); EmployeeDTO value = employeeToEmployeeDTO(entry.getValue()); map.put(key, value); } return map; } protected EmployeeDTO employeeToEmployeeDTO(Employee employee) { if (employee == null) { return null; } EmployeeDTO employeeDTO = new EmployeeDTO(); employeeDTO.setFirstName(employee.getFirstName()); employeeDTO.setLastName(employee.getLastName()); return employeeDTO; } }

3. Stratégies de cartographie des collections

Souvent, nous devons mapper les types de données ayant une relation parent-enfant. Typiquement, nous avons un type de données (parent) ayant comme champ une Collection d'un autre type de données (enfant).

Dans de tels cas, MapStruct offre un moyen de choisir comment définir ou ajouter les enfants au type parent. En particulier, l' annotation @Mapper a un attribut collectionMappingStrategy qui peut être ACCESSOR_ONLY , SETTER_PREFERRED , ADDER_PREFERRED ou TARGET_IMMUTABLE .

Toutes ces valeurs font référence à la manière dont les enfants doivent être définis ou ajoutés au type parent. La valeur par défaut est ACCESSOR_ONLY, ce qui signifie que seuls les accesseurs peuvent être utilisés pour définir la collection d'enfants.

Cette option est pratique lorsque le setter pour le champ Collection n'est pas disponible mais que nous avons un additionneur. Un autre cas dans lequel cela est utile est lorsque la collection est immuable sur le type parent . Habituellement, nous rencontrons ces cas dans les types de cible générés.

3.1. ACCESSOR_ONLY Stratégie de mappage de collection

Prenons un exemple pour mieux comprendre comment cela fonctionne.

Pour notre exemple, créons une classe Company comme source de mappage:

public class Company { private List employees; // getter and setter }

Et la cible de notre cartographie sera un simple DTO:

public class CompanyDTO { private List employees; public List getEmployees() { return employees; } public void setEmployees(List employees) { this.employees = employees; } public void addEmployee(EmployeeDTO employeeDTO) { if (employees == null) { employees = new ArrayList(); } employees.add(employeeDTO); } }

Notez que nous avons à la fois le setter, setEmployees, et l'additionneur, addEmployee, disponibles. De plus, pour l'additionneur, nous sommes responsables de l'initialisation de la collection.

Maintenant, disons que nous voulons mapper une entreprise à un CompanyDTO. Ensuite, comme auparavant, nous avons besoin d'un mappeur:

@Mapper(uses = EmployeeMapper.class) public interface CompanyMapper { CompanyDTO map(Company company); }

Notez que nous avons réutilisé EmployeeMapper et la collectionMappingStrategy par défaut .

Voyons maintenant le code généré par MapStruct:

public class CompanyMapperImpl implements CompanyMapper { private final EmployeeMapper employeeMapper = Mappers.getMapper(EmployeeMapper.class); @Override public CompanyDTO map(Company company) { if (company == null) { return null; } CompanyDTO companyDTO = new CompanyDTO(); companyDTO.setEmployees(employeeMapper.map(company.getEmployees())); return companyDTO; } }

As can be seen, MapStruct uses the setter, setEmployees, to set the List of EmployeeDTO instances. This happens because here we use the default collectionMappingStrategy,ACCESSOR_ONLY.

Also, MapStruct found a method mapping a List to a List in EmployeeMapper and reused it.

3.2. ADDER_PREFERRED Collection Mapping Strategy

In contrast, let's consider we used ADDER_PREFERRED as collectionMappingStrategy:

@Mapper(collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED, uses = EmployeeMapper.class) public interface CompanyMapperAdderPreferred { CompanyDTO map(Company company); }

Again, we want to reuse the EmployeeMapper. However, we need to explicitly add a method that can convert a single Employee to an EmployeeDTO first:

@Mapper public interface EmployeeMapper { EmployeeDTO map(Employee employee); List map(List employees); Set map(Set employees); Map map(Map idEmployeeMap); }

This is because MapStruct will use the adder to add EmployeeDTO instances to the target CompanyDTO instance one by one:

public class CompanyMapperAdderPreferredImpl implements CompanyMapperAdderPreferred { private final EmployeeMapper employeeMapper = Mappers.getMapper( EmployeeMapper.class ); @Override public CompanyDTO map(Company company) { if ( company == null ) { return null; } CompanyDTO companyDTO = new CompanyDTO(); if ( company.getEmployees() != null ) { for ( Employee employee : company.getEmployees() ) { companyDTO.addEmployee( employeeMapper.map( employee ) ); } } return companyDTO; } }

In case the adder was not available, the setter would have been used.

We can find a complete description of all the collection mapping strategies in MapStruct's reference documentation.

4. Implementation Types for Target Collection

MapStruct supports collections interfaces as target types to mapping methods.

Dans ce cas, certaines implémentations par défaut sont utilisées dans le code généré. Par exemple, l'implémentation par défaut de List est ArrayList, comme indiqué dans nos exemples ci-dessus.

Nous pouvons trouver la liste complète des interfaces supportées par MapStruct et les implémentations par défaut qu'il utilise pour chaque interface, dans la documentation de référence.

5. Conclusion

Dans cet article, nous avons exploré comment mapper des collections à l'aide de MapStruct.

Tout d'abord, nous avons examiné comment nous pouvons mapper différents types de collections. Ensuite, nous avons vu comment nous pouvons personnaliser les mappeurs de relations parent-enfant, en utilisant des stratégies de mappage de collection.

En cours de route, nous avons mis en évidence les points clés et les éléments à garder à l'esprit lors de la cartographie des collections à l'aide de MapStruct.

Comme d'habitude, le code complet est disponible sur sur GitHub.