«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
}
В этом случае, если мы просто объявим метод сопоставления из List of Employee в List of EmployeeFullNameDTO, мы получим compile -time ошибка или предупреждение, например:
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".
По сути, это означает, что 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 работают так же, как и со списками. Например, предположим, что мы хотим сопоставить набор экземпляров Employee с набором экземпляров 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\u003cString, Employee\u003e с Map\u003cString, EmployeeDTO\u003e.
Затем мы можем выполнить те же шаги, что и раньше:
@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 по умолчанию.
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 код:
Как видно, MapStruct использует сеттер setEmployees для установки списка экземпляров EmployeeDTO. Это происходит из-за того, что здесь мы используем collectionMappingStrategy по умолчанию, ACCESSOR_ONLY.
Кроме того, MapStruct нашел метод сопоставления List\u003cEmployee\u003e с List\u003cEmployeeDTO\u003e в EmployeeMapper и повторно использовал его.
3.2. ADDER_PREFERRED Стратегия отображения коллекции
@Mapper(collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED,
uses = EmployeeMapper.class)
public interface CompanyMapperAdderPreferred {
CompanyDTO map(Company company);
}
В отличие от этого, давайте рассмотрим, что мы использовали ADDER_PREFERRED в качестве collectionMappingStrategy:
@Mapper
public interface EmployeeMapper {
EmployeeDTO map(Employee employee);
List map(List employees);
Set map(Set employees);
Map<String, EmployeeDTO> map(Map<String, Employee> idEmployeeMap);
}
Опять же, мы хотим повторно использовать EmployeeMapper. Однако нам нужно явно добавить метод, который может сначала преобразовать одиночный Employee в EmployeeDTO:
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 будет использовать сумматор для добавления экземпляров EmployeeDTO в целевой экземпляр CompanyDTO один за другим:
Если бы сумматор был недоступен, использовался бы сеттер.
Полное описание всех стратегий сопоставления коллекций можно найти в справочной документации MapStruct.
4. Типы реализации для целевой коллекции
MapStruct поддерживает интерфейсы коллекций в качестве целевых типов для методов отображения.
В этом случае в сгенерированном коде используются некоторые реализации по умолчанию. Например, реализация по умолчанию для List — это ArrayList, как видно из наших примеров выше.
Мы можем найти полный список поддерживаемых MapStruct интерфейсов и реализации по умолчанию, которые он использует для каждого интерфейса, в справочной документации.
5. Заключение
В этой статье мы рассмотрели, как отображать коллекции с помощью MapStruct.
Во-первых, мы рассмотрели, как мы можем отображать различные типы коллекций. Затем мы увидели, как можно настраивать сопоставители отношений родитель-потомок, используя стратегии сопоставления коллекций.
Попутно мы выделили ключевые моменты и вещи, о которых следует помнить при сопоставлении коллекций с помощью MapStruct.