Есть такая штука, как DTO – Data Transfer Object. Это такие объекты для транспортировки данных. Но сами собой они не рождаются. Нужно конвертировать объект сущности (entity) в объект DTO. И наоборот. Как?
Как раз-таки конвертацией и занимаются mapper-ы. В контексте Spring-приложения это конвертация entity в DTO и обратно. Например, у вас есть сущность Book, представляющая книгу в базе данных, и объект BookDto, используемый для передачи данных о книге между клиентом и сервером. Маппер позволит вам легко преобразовывать данные из Book в BookDto и обратно. Приступим!
DTO-классы сущностей User и Post
Раньше мы с вами создали таблицы в БД, users и posts. Затем мы создали классы сущностей (entity), User и Post. После чего занялись созданием DTO для них – UserDto и PostDto. И вот теперь мы напишем для них мапперы.
Но для начала вспомним, как выглядели DTO-шки для User и Post. Я их создавала с участием библиотеки Lombok. Она позволила не писать геттеры, сеттеры и класс-строитель Builder, а просто указать три аннотации: @Getter, @Setter и @Builder. Удобно!
Класс UserDto:
1 2 3 4 5 6 7 8 9 |
@Getter @Setter @Builder public class UserDto { private Long id; private String email; private String password; private String username; } |
Класс PostDto:
1 2 3 4 5 6 7 8 9 10 |
@Getter @Setter @Builder public class PostDto { private Long id; private UserDto user; private String title; private String text; private ZonedDateTime publishingTime; } |
Вот такие краткие и лаконичные классы получились. Приступим к написанию мапперов.
Ручное создание mapper-а
Конвертеры можно написать самостоятельно. В таком случае, нужно будет просто вручную создавать новые объекты и передавать туда параметры старых. Вот, как будет выглядеть “ручной” mapper для конвертации сущности User:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
@Component public class UserMapper { public UserDto toDto(User user) { return UserDto.builder() .id(user.getId()) .username(user.getUsername()) .email(user.getEmail()) .password(user.getPassword()) .build(); } public User fromDto(UserDto userDto) { User user = new User(); user.setId(userDto.getId()); user.setUsername(userDto.getUsername()); user.setEmail(userDto.getEmail()); user.setPassword(userDto.getPassword()); return user; } public List<UserDto> toDtos(List<User> users) { List<UserDto> result = new ArrayList<>(); for (User user : users) { result.add(toDto(user)); } return result; } } |
Как видите, ничего сверхъестественного. Просто переносим информацию из одних объектов в другие. Метод toDtos(), конвертирующий целый список сущностей User в DTO, просто использует метод toDto() для каждого из элементов. Как правило, конвертировать список DTO-шек в сущности не приходится.
Также класс UserMapper помечен как @Component, чтобы мы могли его использовать в других местах легко и непринуждённо. То есть позволяет Spring управлять его жизненным циклом и внедрять его в другие компоненты.
А вот и mapper для сущности Post:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
@Component public class PostMapper { @Autowired private UserMapper userMapper; public PostDto toDto(Post post) { return PostDto.builder() .build(); } public Post fromDto(PostDto postDto) { Post post = new Post(); post.setId(postDto.getId()); post.setTitle(postDto.getTitle()); post.setText(postDto.getText()); post.setPublishingTime(postDto.getPublishingTime()); User user = userMapper.fromDto(postDto.getUser()); post.setUser(user); return post; } public List<PostDto> toDtos(List<Post> posts) { List<PostDto> result = new ArrayList<>(); for (Post post : posts) { result.add(toDto(post)); } return result; } } |
PostMapper использует UserMapper для конвертации своего пользователя. Поэтому мы внедряем UserMapper внутрь PostMapper с помощью аннотации @Autowired.
Однако, как можно заметить, здесь есть повторяющийся, шаблонный код. И всё бы ничего, но что, если у сущностей не по 3-5, а по 10 полей? И самих сущностей за 20. Для каждой из них писать такие мапперы? Есть решение получше – библиотеки!
Создание mapper-а с помощью библиотеки Mapstruct
Библиотека MapStruct предлагает автоматическую генерацию мапперов на основе интерфейсов. Для использования MapStruct в проекте Spring с Maven, добавьте зависимость в файл pom.xml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
<?xml version="1.0" encoding="UTF-8"?> <project ... <parent> ... </parent> ... <properties> <java.version>17</java.version> <org.mapstruct.version>1.5.5.Final</org.mapstruct.version> </properties> <dependencies> ... <dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct</artifactId> <version>${org.mapstruct.version}</version> </dependency> </dependencies> <build> <plugins> ... <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.1</version> <configuration> <source>1.8</source> <target>1.8</target> <annotationProcessorPaths> ... <path> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-processor</artifactId> <version>${org.mapstruct.version}</version> </path> </annotationProcessorPaths> </configuration> </plugin> </plugins> </build> </project> |
Теперь можно приступать к самому интересному. А самое интересное то, что писать мапперы с помощью Mapstruct легко и просто. Просто посмотрите, как изменится объём кода мапперов.
UserMapper:
1 2 3 4 5 6 |
@Mapper(componentModel = "spring") public interface UserMapper { public UserDto toDto(User user); public User fromDto(UserDto userDto); public List<UserDto> toDtos(List<User> users); } |
PostMapper:
1 2 3 4 5 6 |
@Mapper(componentModel = "spring") public interface PostMapper { public PostDto toDto(Post post); public Post fromDto(PostDto postDto); public List<PostDto> toDtos(List<Post> posts); } |
И да, теперь это не классы, а интерфейсы. Библиотека Mapstruct реализует их автоматически. Указывая аннотацию @Mapper(componentModel = “spring”), мы даём понять проекту, что нужно будет производить инъекцию этой зависимости. Проще говоря, легко и непринуждённо вставлять маппер в нужные нам классы.
Она может конвертировать entity в DTO и обратно потому, что поля называются одинаково и в сущностях, и в DTO-шках. Например, email, password, username и другие. Но если бы у вас была другая ситуация, вам бы понадобилось дополнительно пометить этот момент. Например, если бы в сущности User поле называлось username, а в UserDto – просто name:
1 2 3 4 5 6 7 8 9 10 11 |
@Mapper(componentModel = "spring") public interface UserMapper { @Mapping(source = "username", target = "name") public UserDto toDto(User user); @Mapping(source = "name", target = "username") public User fromDto(UserDto userDto); @IterableMapping(qualifiedByName = "toDto") public List<UserDto> toDtos(List<User> users); } |
Аннотация @Mapping указывает с помощью параметров source и target названия соответствующих друг другу полей в классах. Аннотация @IterableMapping говорит методу toDtos(), что для конвертации объектов в списке нужно для каждого из них применять метод toDto().
Проверка работы
Итак, мы закончили создание слоя mapper. Как и всех слоёв программы из структуры Spring-приложения! А это означает, что пора проверять, как это работает и наслаждаться результатом.
Только сначала нужно заменить отдаваемые и принимаемые controller-ом сущности на DTO-шки.
UserController с применением DTO и mapper-а:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
@RestController @RequiredArgsConstructor @RequestMapping("/users") public class UserController { private final UserService userService; private final UserMapper userMapper; @GetMapping public List<UserDto> getAllUsers() { List<User> users = userService.getAllUsers(); return userMapper.toDtos(users); } @GetMapping("/{id}") public UserDto getUserById(@PathVariable Long id) { User user = userService.getUserById(id); return userMapper.toDto(user); } @PostMapping public UserDto createUser(@RequestBody UserDto userDto) { User user = userMapper.fromDto(userDto); user = userService.saveUser(user); return userMapper.toDto(user); } @PutMapping("/{id}") public UserDto updateUser(@PathVariable Long id, @RequestBody UserDto userDto) { User user = userMapper.fromDto(userDto); user.setId(id); user = userService.saveUser(user); return userMapper.toDto(user); } @DeleteMapping("/{id}") public void deleteUser(@PathVariable Long id) { userService.deleteUserById(id); } } |
PostController:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
@RestController @RequiredArgsConstructor @RequestMapping("/posts") public class PostController { private final PostService postService; private final PostMapper postMapper; @GetMapping public List<PostDto> getAllPosts() { List<Post> posts = postService.getAllPosts(); return postMapper.toDtos(posts); } @GetMapping("/{id}") public PostDto getPostById(@PathVariable Long id) { Post post = postService.getPostById(id); return postMapper.toDto(post); } @PostMapping public PostDto createPost(@RequestBody PostDto postDto) { Post post = postMapper.fromDto(postDto); post = postService.savePost(post); return postMapper.toDto(post); } @PutMapping("/{id}") public PostDto updatePost(@PathVariable Long id, @RequestBody PostDto postDto) { Post post = postMapper.fromDto(postDto); post.setId(id); post = postService.savePost(post); return postMapper.toDto(post); } @DeleteMapping("/{id}") public void deletePost(@PathVariable Long id) { postService.deletePostById(id); } @GetMapping("/user/{user_id}") public List<PostDto> getUserPosts(@PathVariable("user_id") Long userId) { List<Post> posts = postService.getPostsByUserId(userId); return postMapper.toDtos(posts); } } |
Отлично. Перед тем, как начать делать http-запросы к контроллерам, создадим содержимое в таблицах users и posts в БД:
Таблица posts
Таблица users
Отлично. У нас есть два пользователя. У одного из них – 2 поста, у другого – 1. Пора делать запросы. Нужно запустить Spring-приложение, после чего ввести в строку запроса localhost:порт/путь. Обычно порт стоит 8080, но он у меня занят, и поэтому в файле application.properties я поменяла его на 8090. Путь – это строка, которая формируется из того, что мы указывали в controller-е.
Итак, результаты запросов:
Всё работает именно так, как предполагается! Controller-ы отдают данные, и при этом никаких рекурсий. Да, конечно, controller-ы, которые мы написали, могут обрабатывать не только get-запросы, но и всякие post, put и delete. Но из обычного браузера это сделать не так легко. Обычно на клиентской части приложения уже создают возможность отправлять эти самые post, put и delete. Но даже без них видно – работа была проделана на ура!
Вот и подошло к концу создание простого Spring back-end приложения. Надеюсь, вы тоже поразились удобству, которое предоставляет Spring Framework. Поверьте, я ведь до знакомства со Spring писала всё это ручками, писала классы DAO, в которых прописывала реальные SQL-запросы, для некоторых сущностей они были огромнейшими ввиду множества связей с другими сущностями. Сервлеты, jsp… Всё это было. Spring действительно сильно упрощает шаблонные задачи, позволяя экономить время и силы, направляя их в более творческое русло. Например, обдумывание концепции приложения.
Всем удачной веб-разработки!