Перейти к основному содержимому

Сопоставление коллекций с помощью MapStruct

· 8 мин. чтения

1. Обзор

В этом руководстве мы рассмотрим, как сопоставлять наборы объектов с помощью MapStruct.

Поскольку эта статья предполагает уже базовое понимание MapStruct, новичкам следует сначала ознакомиться с нашим кратким руководством по MapStruct .

2. Сопоставление коллекций

В целом сопоставление коллекций с помощью MapStruct работает так же, как и для простых типов .

По сути, нам нужно создать простой интерфейс или абстрактный класс и объявить методы сопоставления. На основе наших объявлений MapStruct автоматически сгенерирует код сопоставления. Как правило, сгенерированный код будет перебирать исходную коллекцию, преобразовывать каждый элемент в целевой тип и включать каждый из них в целевую коллекцию .

Давайте рассмотрим простой пример.

2.1. Списки сопоставления

Во-первых, для нашего примера давайте рассмотрим простой POJO в качестве источника отображения для нашего преобразователя:

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

// constructor, getters and setters
}

Целью будет простой DTO:

public class EmployeeDTO {

private String firstName;
private String lastName;

// getters and setters
}

Далее давайте определим наш маппер:

@Mapper
public interface EmployeeMapper {
List<EmployeeDTO> map(List<Employee> employees);
}

Наконец, давайте посмотрим на код MapStruct, сгенерированный из нашего интерфейса EmployeeMapper :

public class EmployeeMapperImpl implements EmployeeMapper {

@Override
public List<EmployeeDTO> map(List<Employee> employees) {
if (employees == null) {
return null;
}

List<EmployeeDTO> list = new ArrayList<EmployeeDTO>(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;
}
}

Следует отметить одну важную вещь. В частности, MapStruct автоматически сгенерировал для нас сопоставление Employee с EmployeeDTO .

Бывают случаи, когда это невозможно. Например, предположим, что мы хотим сопоставить нашу модель Employee со следующей моделью:

public class EmployeeFullNameDTO {

private String fullName;

// getter and setter
}

В этом случае, если мы просто объявим метод сопоставления из списка сотрудников в список EmployeeFullNameDTO , мы получим ошибку времени компиляции или предупреждение, например :

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

По сути, это означает, что в данном случае MapStruct не может автоматически сгенерировать для нас сопоставление . Поэтому нам нужно вручную определить сопоставление между Employee и EmployeeFullNameDTO. ****

Учитывая эти моменты, давайте вручную определим его:

@Mapper
public interface EmployeeFullNameMapper {

List<EmployeeFullNameDTO> map(List<Employee> employees);

default EmployeeFullNameDTO map(Employee employee) {
EmployeeFullNameDTO employeeInfoDTO = new EmployeeFullNameDTO();
employeeInfoDTO.setFullName(employee.getFirstName() + " " + employee.getLastName());

return employeeInfoDTO;
}
}

Сгенерированный код будет использовать метод, который мы определили для сопоставления элементов исходного списка с целевым списком .

Это также применимо в целом. Если мы определили метод, который сопоставляет тип исходного элемента с типом целевого элемента, MapStruct будет использовать его.

2.2. Наборы карт и карты

Сопоставление наборов с помощью MapStruct работает так же, как и со списками. Например, предположим, что мы хотим сопоставить экземпляры Set of Employee с экземплярами Set of EmployeeDTO .

Как и прежде, нам понадобится маппер:

@Mapper
public interface EmployeeMapper {

Set<EmployeeDTO> map(Set<Employee> employees);
}

И MapStruct сгенерирует соответствующий код:

public class EmployeeMapperImpl implements EmployeeMapper {

@Override
public Set<EmployeeDTO> map(Set<Employee> employees) {
if (employees == null) {
return null;
}

Set<EmployeeDTO> set =
new HashSet<EmployeeDTO>(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;
}
}

То же самое относится и к картам. Предположим, мы хотим сопоставить Map<String, Employee> с Map<String, EmployeeDTO> .

Затем мы можем выполнить те же шаги, что и раньше:

@Mapper
public interface EmployeeMapper {

Map<String, EmployeeDTO> map(Map<String, Employee> idEmployeeMap);
}

И MapStruct делает свое дело:

public class EmployeeMapperImpl implements EmployeeMapper {

@Override
public Map<String, EmployeeDTO> map(Map<String, Employee> idEmployeeMap) {
if (idEmployeeMap == null) {
return null;
}

Map<String, EmployeeDTO> map = new HashMap<String, EmployeeDTO>(Math.max((int)(idEmployeeMap.size() / .75f) + 1, 16));

for (java.util.Map.Entry<String, Employee> 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. Стратегии сопоставления коллекций

Часто нам нужно сопоставлять типы данных, имеющие отношение родитель-потомок. Как правило, у нас есть тип данных (родительский), имеющий в качестве поля коллекцию другого типа данных (дочернего).

Для таких случаев MapStruct предлагает способ выбрать, как установить или добавить дочерние элементы к родительскому типу. В частности, аннотация @Mapper имеет атрибут collectionMappingStrategy , который может быть ACCESSOR_ONLY , SETTER_PREFERRED , ADDER_PREFERRED или TARGET_IMMUTABLE .

Все эти значения относятся к тому, как дочерние элементы должны быть установлены или добавлены к родительскому типу. Значение по умолчанию — ACCESSOR_ONLY, что означает, что для установки коллекции дочерних элементов можно использовать только средства доступа .

Эта опция удобна, когда сеттер для поля Коллекция недоступен, но у нас есть сумматор. Другой случай, когда это полезно, — это когда коллекция неизменяема для родительского типа . Обычно мы сталкиваемся с такими случаями в сгенерированных целевых типах.

3.1. ACCESSOR_ONLY Стратегия сопоставления коллекций

Давайте возьмем пример, чтобы лучше понять, как это работает.

В нашем примере давайте создадим класс Company в качестве источника отображения:

public class Company {

private List<Employee> employees;

// getter and setter
}

А целью нашего сопоставления будет простой DTO:

public class CompanyDTO {

private List<EmployeeDTO> employees;

public List<EmployeeDTO> getEmployees() {
return employees;
}

public void setEmployees(List<EmployeeDTO> employees) {
this.employees = employees;
}

public void addEmployee(EmployeeDTO employeeDTO) {
if (employees == null) {
employees = new ArrayList<>();
}

employees.add(employeeDTO);
}
}

Обратите внимание, что у нас есть как установщик setEmployees, так и сумматор addEmployee . Также для сумматора мы отвечаем за инициализацию коллекции.

Теперь предположим, что мы хотим сопоставить Company с CompanyDTO. Затем, как и прежде, нам понадобится маппер:

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

Обратите внимание, что мы повторно использовали EmployeeMapper и collectionMappingStrategy по умолчанию . ``

Теперь давайте посмотрим на сгенерированный 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;
}
}

Как видно, MapStruct использует установщик setEmployees для установки списка экземпляров EmployeeDTO . Это происходит из-за того, что здесь мы используем collectionMappingStrategy по умолчанию, ACCESSOR_ONLY.

Кроме того, MapStruct нашел метод сопоставления List<Employee> с List<EmployeeDTO> в EmployeeMapper и повторно использовал его.

3.2. ADDER_PREFERRED Стратегия сопоставления коллекций

Напротив, давайте рассмотрим, что мы использовали ADDER_PREFERRED в качестве collectionMappingStrategy :

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

Опять же, мы хотим повторно использовать EmployeeMapper . Однако нам нужно явно добавить метод, который может сначала преобразовать одного сотрудника в EmployeeDTO :

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

Это связано с тем, что MapStruct будет использовать сумматор для добавления экземпляров EmployeeDTO к целевому экземпляру CompanyDTO один за другим :

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

Если бы сумматор был недоступен, использовался бы сеттер.

Полное описание всех стратегий сопоставления коллекций мы можем найти в справочной документации MapStruct .

4. Типы реализации для целевой коллекции

MapStruct поддерживает интерфейсы коллекций в качестве целевых типов для методов сопоставления.

В этом случае в сгенерированном коде используются некоторые реализации по умолчанию. Например, реализация по умолчанию для List — это ArrayList , как видно из наших примеров выше.

Мы можем найти полный список поддерживаемых MapStruct интерфейсов и реализации по умолчанию, которые он использует для каждого интерфейса, в справочной документации .

5. Вывод

В этой статье мы рассмотрели, как сопоставлять коллекции с помощью MapStruct.

Во-первых, мы рассмотрели, как мы можем отображать различные типы коллекций. Затем мы увидели, как можно настраивать сопоставители отношений родитель-потомок, используя стратегии сопоставления коллекций.

Попутно мы выделили ключевые моменты и вещи, о которых следует помнить при сопоставлении коллекций с помощью MapStruct.

Как обычно, полный код доступен на GitHub .