Mapper в Spring: почему это не просто копирование полей


В Java-проекте часто встречается примерно такой код:

public UserDto toDto(User user) {
return new UserDto(
user.getId(),
user.getName(),
user.getEmail()
);
}

И кажется, что Mapper — это просто технический класс, который экономит несколько строк.
Но у mapper есть гораздо более важная задача:
он отделяет одну модель данных от другой.

Например:

Database
↓
Entity
↓
Mapper
↓
DTO
↓
REST API
Почему нельзя просто отдавать Entity наружу?
Потому что Entity — модель хранения и работы с БД, а DTO — модель контракта API.
И это могут быть совершенно разные вещи.

Например, Entity:
class User {

Long id;
String firstName;
String lastName;

String passwordHash;

LocalDateTime createdAt;
}
А API должно вернуть:

class UserResponse {

Long id;
String fullName;
}
Mapper здесь не просто копирует поля:

public UserResponse toResponse(User user) {
return new UserResponse(
user.getId(),
user.getFirstName() + " " + user.getLastName()
);
}
И вот здесь появляется важный момент.
Mapper — это граница между моделями
Если начать использовать Entity напрямую в контроллерах, слои постепенно начинают знать слишком много друг о друге:

Controller
↓
Entity
↓
Hibernate
↓
Database
А с DTO:

Controller
↓
DTO
↓
Service
↓
Entity
↓
Repository
Теперь можно изменить структуру БД или Entity, не меняя контракт REST API.

А что с MapStruct?
В больших проектах mapper часто не пишут вручную:

@Mapper(componentModel = "spring")
public interface UserMapper {

UserResponse toResponse(User user);

User toEntity(CreateUserRequest request);
}
MapStruct генерирует обычный Java-код и здесь есть интересный нюанс: MapStruct не делает магию во время выполнения.
Это не reflection-based mapper.
На этапе компиляции генерируется примерно такой код:

@Component
public class UserMapperImpl implements UserMapper {

@Override
public UserResponse toResponse(User user) {
if (user == null) {
return null;
}

return new UserResponse(
user.getId(),
user.getName()
);
}
}
То есть в runtime это практически обычный Java-код, а вот где mapper действительно может стать проблемой
Если Entity содержит связи:

class Order {

Long id;

Customer customer;

List<OrderItem> items;

}
и сделать:
OrderResponse toResponse(Order order);
можно неожиданно получить цепочку:

Order
↓
Customer
↓
Orders
↓
Customer
↓
...
или огромное количество обращений к БД из-за lazy-загрузки.
Поэтому mapper не должен быть местом, где мы случайно запускаем загрузку всего графа Entity.
Это уже вопрос архитектуры:

Repository
↓
получили нужные данные
↓
Mapper
↓
DTO
а не:

Mapper
↓
пойдем-ка вызовем все getXXX()
↓
Hibernate догрузил пол-БД
Итого я бы сформулировал так:


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

.А MapStruct при этом — всего лишь инструмент, который автоматизирует эту работу.
Просмотры: 69
Открыть в MAX Сообщить о неточности

Репосты публикации

Публикация канала

Сообщить о неточности

Опишите, что нужно исправить. Если есть источник, добавьте ссылку.

Вы покидаете datool.ru

Ссылка ведёт на внешний сайт. datool.ru не отвечает за его содержимое — проверьте адрес, прежде чем вводить пароли или платёжные данные.

Переход через 10 с

Перейти