Comments 13
А сколько шаблонного кода требует Java для описания стандартных DTO?
По мне, этот подход решает конкретные (и не надуманые) задачи. Плюс создает некоторое подобие type alias
из Haskell (еще один аналог — DOMAIN в SQL), что значительно (субъективно) усиливает систему типов Java.
Чаще всего ваши DTO так или иначе повторяют Domain model. Поэтому по большей части приходится дублировать свойства плюс писать ненужные мэпперы к каждому классу. Мы стараемся сразу определить у domain-класса группы полей, которые потом будут использоваться в DTO при помощи соответствующих интерфейсов:
class User implements BasicInfo, ExtendedInfo, SecurityInfo {
...
}
interface BasicInfo {
String getId();
String getName();
String getEmail();
LocalDate getBirthDate();
}
interface SecurityInfo {
EnumSet<Role> getRoles();
LocalDateTime getLastLoginTime();
}
Сериализатору указывается интерфейс DTO, и он пишет только соответствующий сет свойств.
В итоге:
- исключает дублирование объектов и свойств
- не нужны мепперы (филдсет сериализуется автоматически)
- поддержка IDE и все такое
не нужны мепперы (филдсет сериализуется автоматически)
Получается, что котроллер фактически отдаёт наружу доменный объект, я правильно понимаю?
Не совсем. Сериализатор настроен так, чтобы отдавать только сет свойств, указанных в возвращаемом интерфейсе.
@GET
@Path("/users/{userId}")
public BasicInfo getUserBasicInfo(@PathParam("userId") String userId) {
User user = userDao.findById(userId);
return user;
}
В этом случае контроллер отдаст JSON:
{
"id": "12345",
"name": "Vasya",
"email" : "vasya@mysite.com",
"birthDate" : "1970-01-01"
}
Понимаете, какая тут загогулина: фактические userDao.findById(userId)
возвращает сущность (если я правильно понял userDao
— это реализация JpaRepository
).
Теперь представьте, что одним из свойств, входящих во множество, отдаваемое клиенту, является ленивой дочерней сущностью или @Lob
. В этом случае мы получим LazyInitException
с сообщением "No session", т.к. область действия транзакции прекращается по выходу из findById
.
И тут одной из трёх: либо добавлять @Transactional
на контроллер, что является грубейшим нарушением принципа разделения слоёв приложения, либо явно загружать ленивую сущность (нужно приседать с графами или явно прописывать fetch
), либо возвращать не сущность, а DTO.
Поэтому идея кажется сомнительной, как и внедрение дао/репозиториев в контроллер.
добавлять @Transactional на контроллер, что является грубейшим нарушением принципа разделения слоёв приложения
Принципы у всех разные, и не стоит абсолютизировать никакой из них. Для меня принципиальна бизнес логика приложения. Если она тонет в куче сопроводительного бойлерплейта, то стоит пересмотреть модель использования. Я всегда стараюсь, весь бойлерплейт замести под ковер и автоматизировать по мере возможности.
В данном случае не вижу ничего зазорного, чтобы контроллер был @Transactional. Даже если некоторые properties грузятся как lazy, при сериализации они корректно догрузятся.
либо явно загружать ленивую сущность (нужно приседать с графами или явно прописывать fetch)
Суперинтерфейсы (BasicInfo, ExtendedInfo, SecurityInfo, etc...) как раз по сути определяют наборы свойств, которые необходимо вытащить в каждом конкретном случае, поэтому ничто не мешает модифицировать репозиторий и сделать автоматическую генерацию графа для каждого из суперинтерфейсов.
// dao-метод возвращает список User entity с загруженными свойствами только для суперинтерфейса T
<T super User> List<T> findAll(Class<T> superClass);
...
List<BasicInfo> userInfos = findAll(BasicInfo.class);
Вот вам и автоматизация DTO.
Ну не знаю, как по мне принцип единой ответственности и прочее, как и ТБ писаны кровью, чересчур вольное обращение с ними чревато.
По последнему пункту: если производительность не важна, то проекции созданные из интерфейсов хорошо зайдут. В противном случае будет больно, я писал отдельную статью о причинах этого.
Бенчмарк показывает существенную дороговизну проекций по сравнению с DTO
(count) Score Error Units
findAllByName 1 16.188 ± 0.643 us/op
findAllByNameUO 1 13.991 ± 0.208 us/op
findAllByName 100 235.077 ± 2.407 us/op
findAllByNameUO 100 65.713 ± 1.618 us/op
findAllByName:·gc.alloc.rate.norm 1 20842.539 ± 24.394 B/op
findAllByNameUO:·gc.alloc.rate.norm 1 13802.823 ± 29.680 B/op
findAllByName:·gc.alloc.rate.norm 100 519894.926 ± 1588.438 B/op
findAllByNameUO:·gc.alloc.rate.norm 100 41812.605 ± 40.003 B/op
Для хранения цены мы используем тип данных Double, но в реальных проектах вы должны использовать BigDecimal.
Вот не надо так, даже в тестовых проектах лучше сразу вырабатывать привычку и писать в боевом режиме.
class Counterparty {
private Req req {
prvate String inn;
}
}
Для карточки свойств уровень вложенности мы не меняем, тогда как для отображения в таблице мне делаем что то вроде того:
class Counterparty {
prvate String inn;
}
Теперь нам на клиенте, во-первых, надо иметь 2 модели: одну для карточки, другую для таблицы, во-вторых, если мы реализуем поиск по таблице, то мы не сможем его использовать в карточки(под реализуем я подразумеваю, что мы сможем отправить запрос на сервер с фильтрацией по ИНН), потому что ИНН находится на разных уровня и придется чуть иначе формировать запрос. И ради чего? Ради просто наличия DTO как шаблона, который в таком варианте, я вообще сомневаюсь что решает какие-то насущные проблемы.
DTO для меня имеют реальный смысл в двух случаях: если очень сложные модели на бэке и их надо упрощать для клиента; если мы используем DDD, тогда user может быть и сотрудником, и пользователем системы, из-за чего реально одна сущность будет проецироваться в разные поля на клиенте.
Переосмысление DTO в Java