Mappatura delle raccolte con MapStruct

1. Panoramica

In questo tutorial, daremo un'occhiata a come mappare raccolte di oggetti utilizzando MapStruct.

Poiché questo articolo presuppone già una conoscenza di base di MapStruct, i principianti dovrebbero prima consultare la nostra guida rapida a MapStruct.

2. Mappatura delle raccolte

In generale, la mappatura di raccolte con MapStruct funziona allo stesso modo dei tipi semplici .

Fondamentalmente, dobbiamo creare una semplice interfaccia o una classe astratta e dichiarare i metodi di mappatura. In base alle nostre dichiarazioni, MapStruct genererà automaticamente il codice di mappatura. In genere, il codice generato eseguirà il ciclo sulla raccolta di origine, convertirà ogni elemento nel tipo di destinazione e includerà ciascuno di essi nella raccolta di destinazione .

Diamo un'occhiata a un semplice esempio.

2.1. Elenchi di mappatura

Innanzitutto, per il nostro esempio, consideriamo un semplice POJO come sorgente di mappatura per il nostro mappatore:

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

L'obiettivo sarà un semplice DTO:

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

Successivamente, definiamo il nostro mappatore:

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

Infine, diamo un'occhiata al codice MapStruct generato dalla nostra interfaccia 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; } } 

C'è una cosa importante da notare. Nello specifico, MapStruct ha generato per noi, automaticamente, la mappatura da Employee a EmployeeDTO .

Ci sono casi in cui ciò non è possibile. Ad esempio, supponiamo di voler mappare il nostro modello Employee al seguente modello:

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

In questo caso, se dichiariamo semplicemente il metodo di mappatura da un elenco di dipendenti a un elenco di EmployeeFullNameDTO , riceveremo un errore o un avviso in fase di compilazione come:

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".

Fondamentalmente, questo significa che MapStruct non potrebbe generare automaticamente la mappatura per noi in questo caso . Pertanto, dobbiamo definire, manualmente, la mappatura tra Employee e EmployeeFullNameDTO.

Dati questi punti, definiamolo manualmente:

@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; } }

Il codice generato utilizzerà il metodo abbiamo definito per mappare gli elementi della sorgente List al bersaglio lista .

Questo vale anche in generale. Se abbiamo definito un metodo che mappa il tipo di elemento di origine sul tipo di elemento di destinazione, MapStruct lo utilizzerà.

2.2. Set di mappe e mappe

La mappatura dei set con MapStruct funziona allo stesso modo delle liste. Ad esempio, supponiamo di voler mappare un set di istanze Employee a un set di istanze EmployeeDTO .

Come prima, abbiamo bisogno di un mappatore:

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

E MapStruct genererà il codice appropriato:

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; } }

Lo stesso vale per le mappe. Consideriamo di voler mappare una mappa su una mappa .

Quindi, possiamo seguire gli stessi passaggi di prima:

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

E MapStruct fa il suo lavoro:

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. Strategie di mappatura delle collezioni

Spesso è necessario mappare i tipi di dati con una relazione genitore-figlio. Tipicamente, abbiamo un tipo di dati (genitore) avente come campo una Raccolta di un altro tipo di dati (figlio).

In questi casi, MapStruct offre un modo per scegliere come impostare o aggiungere i figli al tipo genitore. In particolare, l' annotazione @Mapper ha un attributo collectionMappingStrategy che può essere ACCESSOR_ONLY , SETTER_PREFERRED , ADDER_PREFERRED o TARGET_IMMUTABLE .

Tutti questi valori si riferiscono al modo in cui i figli dovrebbero essere impostati o aggiunti al tipo genitore. Il valore predefinito è ACCESSOR_ONLY, il che significa che solo le funzioni di accesso possono essere utilizzate per impostare la raccolta di elementi figlio.

Questa opzione è utile quando il setter per il campo Collection non è disponibile ma abbiamo un sommatore. Un altro caso in cui ciò è utile è quando la Collection è immutabile sul tipo genitore . Di solito, incontriamo questi casi nei tipi di destinazione generati.

3.1. ACCESSOR_ONLY Strategia di mappatura della raccolta

Facciamo un esempio per capire meglio come funziona.

For our example, let's create a Company class as our mapping source:

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

And the target for our mapping will be a 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); } }

Note that we have both the setter, setEmployees, and the adder, addEmployee, available. Also, for the adder, we are responsible for collection initialization.

Now, let's say we want to map a Company to a CompanyDTO. Then, as before we need a mapper:

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

Note that we reused the EmployeeMapper and the default collectionMappingStrategy.

Now, let's take a look at the code MapStruct generated:

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.

In questo caso, alcune implementazioni predefinite vengono utilizzate nel codice generato. Ad esempio, l'implementazione predefinita per List è ArrayList, come si può notare dai nostri esempi sopra.

Possiamo trovare l'elenco completo delle interfacce supportate da MapStruct e le implementazioni predefinite che utilizza per ciascuna interfaccia, nella documentazione di riferimento.

5. conclusione

In questo articolo, abbiamo esplorato come mappare le raccolte utilizzando MapStruct.

Innanzitutto, abbiamo esaminato come mappare diversi tipi di raccolte. Quindi, abbiamo visto come personalizzare i mappatori delle relazioni genitore-figlio, utilizzando strategie di mappatura della raccolta.

Lungo il percorso, abbiamo evidenziato i punti chiave e le cose da tenere a mente durante la mappatura delle raccolte utilizzando MapStruct.

Come al solito, il codice completo è disponibile su GitHub.