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 .