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