Далеко не всегда дело обходится одними только объектами слоя entity. Порой нужно создать классы, которые напоминают классы сущностей, но они будут упрощены или модифицированы. Это делается для нужд транспортировки данных. Отсюда и название – DTO, Data Transfer Object.
Предлагаю приступить к обсуждению, зачем используются DTO, какие принципы есть при создании DTO и как их вообще создавать.
Что такое DTO и зачем нужны
DTO представляют собой объекты, которые используются для передачи данных между различными частями приложения. Они служат в качестве контейнеров для данных и обычно содержат только поля данных и методы доступа к этим данным.
Использование DTO в структуре Spring-проекта помогает разделить бизнес-логику приложения от его представления и обеспечивает более гибкую структуру. Они также уменьшают объём данных, передаваемых между клиентом и сервером, а это, на минуточку, оптимизация нагрузки на сеть.
Ещё одна немаловажная причина использовать DTO – возможность рекурсивного доступа объектов друг к другу. Например, сущность Author имеет в списке список объектов Book. Сущность Book, в свою очередь, имеет в себе поле Author. Таким образом, при передаче объекта Book или Author клиенту как JSON-объект, мы столкнёмся со StackOverflowError. Это ошибка из иерархии ошибок и исключений в Java, которая возникает при переполнении стека. А переполнился он, потому что две сущности бесконечно переводили стрелки друг на друга, не сумев остановиться на ком-то одном. Вот DTO-шки и приходят на помощь – обычно с нейтрализацией в одной из двух сущностей ссылки на другую сущность.
Как создать DTO
Для создания DTO в Java обычно используются простые классы с private полями данных и public методами доступа к этим полям. Например, та же книжка Book, но уже без ссылки на автора Author:
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 |
public class BookDto { private Long id; private String title; private int publishingYear; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getTitle() { return title; } public void setTitle(String title) { this.title= title; } public String getPublishingYear() { return publishingYear; } public void setPublishingYear(String publishingYear) { this.publishingYear= publishingYear; } } |
Заметьте, что я называю класс не BookDTO, а BookDto. Всё из-за соглашения о кодировании на языке Java, конвенции о написании кода. Там говорится, что аббревиатуры лучше писать не капсом, а как обычное слово. Так названия классов лучше читаются, когда слов несколько – сразу видны их границы. Например, HttpRequest явно лучше, чем HTTPRequest.
Виды
DTO могут быть различных типов в зависимости от контекста приложения и специфики данных, которые необходимо передавать. В основном DTO бывают такого рода:
- Request. Используется для передачи данных от клиента к серверу.
- Response. Используется для передачи данных от сервера к клиенту в ответ на запросы клиента.
- Entity. Используется для передачи данных сущностей между различными слоями приложения.
В нашем с вами случае это будут Request и Response DTO, так как наше Spring-приложение может принимать и отдавать данные.
Пример с User и Post
Итак, вернёмся к примеру созданных ранее сущностей User и Post, которые были написаны по образу и подобию таблиц users и posts в БД.
Сущность User выглядит так:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
@Data @NoArgsConstructor @Entity @Table(name = "users") public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String email; private String password; private String username; @OneToMany(mappedBy = "user") private List<Post> posts = new ArrayList<>(); } |
Вот так выглядит сущность Post:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
@Data @NoArgsConstructor @Entity @Table(name = "posts") public class Post { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @ManyToOne @JoinColumn(name = "user_id") private User user; private String title; private String text; @Temporal(TemporalType.TIMESTAMP) private ZonedDateTime publishingTime; } |
И да, они ссылаются друг на друга. Давайте уберём одну из связей, чтобы избежать рекурсии. Условимся, что клиенту, получающему DTO от сервера, будет важнее знать пользователя, написавшего запись в сущности Post, чем получать список всех записей пользователя в сущности User. Ведь список всех записей по пользователю можно и так произвести при помощи соответствующего запроса.
Итак, убираем связь со стороны User. Также я не буду прописывать геттеры и сеттеры, а также класс-строитель Builder своими руками. Я для этого буду использовать библиотеку Lombok, которая позволит мне сделать это в несколько аннотаций: @Getter, @Setter и @Builder.
Вот как будет выглядеть DTO для сущности пользователя:
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; } |
Да, такой маленький и миленький класс. Удивительные чудеса творят современные инструменты разработки. А вот DTO для сущности записи:
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; } |
Учтите, что ссылки на другие сущности в классе DTO должны быть заменены также на DTO. То есть в PostDto используется поле UserDto, а не User.
Неплохо поработали!
DTO мы создали. Что делать дальше? Как их применить? А для этого нам потребуются специальные конвертеры. Они будут конвертировать entity в Data Transfer Object и обратно. Такие конвертеры называются mapper-ы. И о них – в следующей статье.