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 при этом — всего лишь инструмент, который автоматизирует эту работу.