Обновить

Комментарии 24

Спасибо за статью. Мне интересно, а spring data jdbc хоть на сколько-то производительнее jpa?

К производительности иногда возникают вопросы, но она определённо более предсказуема

Не встречал ни одного высоконагруженного проекта, где бы использовался JPA. Все на определенном этапе слезают с него на что то более предсказуемое/низкоуровневое, иногда на свои велосипеды, но все они низкоуровневые

Проблема настолько распространена, что породила в сообществе целые баталии на тему «где правильно делать маппинг Entity ↔ DTO».

Если использовать многослойную архитектуру, то ответ на этот вопрос очевиден. DTO находится в более высоко лежащем слое приложения, чем слой в котором находится Entity. Соответственно слой с Entity не знает, что существуют объекты DTO - слои связаны между собой однонаправленно. Поэтому маппинг Entity ↔ DTO происходит в слое в котором находятся объекты DTO.

и в каком слое происходит управление транзакцией?
в слое, где находятся DTO? тогда это нарушение принципов слоеной архитектуры
в слое, где идет работа с Entity? тогда добро пожаловать в LazyInitializationExeption, либо слой работы с Entity должен как-то знать, что именно надо вытащить из БД, чтобы на слоях выше LazyInitializationExeption не возникало. а значит у нас знание о структуре DTO неявно перетекает на уровень работы с Entity.

простите, но все эти "слоеные архитектуры" - очередной marketing bullshit для продажи книг и курсов. ну и еще отличный баттлфилд для холиваров

Отличный комментарий - смешаны транзакции, Entity, LazyInitializationException и "слоеные архитектуры" как очередной marketing bullshit.

Но мне ответить очень легко.

  1. DTO - это не обязательно какой-то аналог Entity. DTO может включать в себя информацию из разных Entity объектов и слой, содержащий DTO, конечно не имеет никакого отношения к тому как и где реализованы транзакции.

  2. Что касается LazyInitializationException, то совершенно не обязательно, что Entity наполняется при помощи ORM фреймворка, который вызывает упомянутую исключительную ситуацию. Но даже при использовании ORM функционал слоя работающего с Entity должен быть реализован так, чтобы исключить возникновение LazyInitializationException.

  3. Что касается транзакций, то типовой алгоритм следующий. Допустим работа с Entity идёт в logic layer. Функционал работающий с Entity помещается внутри функционала use case или по Фаулеру это application logic (это тоже logic layer). При старте метода в объекте application logic запускается транзакция, а затем отрабатывает нужный функционал с Entity. Если при работе метода не возникли exception, то по завершению функционала метода application logic отрабатывает commit транзакции, в противном случае rollback. Возникновение LazyInitializationException откатит транзакцию, а сообщение об этой ошибке покажет разработчику её источник.

при чем тут commit/rollback?!
мы пока только про выборки. этого достаточно.
так где же идет управление транзакцией и каким это образом "функционал слоя работающего с Entity должен быть реализован так, чтобы исключить возникновение LazyInitializationException"?
чтобы исключить LazyInitializationException слой работы с Entity должен как-то знать, что именно потом будет запрошено из этой Entity.
то есть, слой работы с Entity должен неявно знать структуру будущего DTO.
и вот у нас уже потекли слои друг в друга.
причем, неявно.

Если у меня есть сущность Product и внутри нее есть множество Tag (замапленных как OneToMany), а у Tag есть color
то вот я выбрал Product по ID и передал его в верхний слой.
а там у моего Product спросили product.getTags() и побежали по этому списку спрашивая getColor() у каждого элемента.
что же мы получим в слое работы с DTO, если заранее не вытащить join fetch Tag
?

все еще bullshit

Если речь идёт о работе с транзакциями, то конечно надо знать условия вызова commit/rollback.

слой работы с Entity должен неявно знать структуру будущего DTO

Слой работающий с DTO более высоко лежащий, чем слой с Entity. И как раз слой с DTO знает всё про наличие и структуру Entity. И это вполне естественно, так как DTO наполняется данными из объектов Entity. А слой с Entity ничего не знает про слой с DTO. Такова логика многослойной архитектуры. Если ею не пользоваться, то ничего не могу сказать по этому вопросу.

так где же должно быть управление транзакциями в вашей многослойной архитектуре?!
если слой работы с Entity не знает, что потом из Entity запросят для формирования DTO, то что же он должен вытащить из БД для предотвращения появления LazyInitializationException в будущем при обращении к "ленивым" полям Entity?!

казалось бы, ну вытаскивай joing fetch ленивые поля всегда и нет проблем!
но они есть
если у меня по 100 тегов у каждого продукта и я вытаскиваю 100 продуктов, значит из БД мне приедет 10000 строк.
и зачем мне гонять эти данные, если потом при формировании DTO к коллекции тегов никто не обратится?
а если у меня десяток ленивых полей в Entity и для разных DTO используются разные поля?

и вот у нас уже знание о структуре DTO неявно потекло на уровень работы с Entity

что же мы получим в слое работы с DTO, если заранее не вытащить join fetch Tag

В чём проблема извлечь все объекты Tag? Дайте нужные настройки при выполнении этого запроса.

в том, что надо знать, когда они нужны, а когда нет.
нужно знать, в каком DTO используются поля из Tags, а в каком не используются.
а это уже знание о структуре DTO на уровне слоя для работы с Entity

Похоже не совсем понимаю проблему у автора комментария. В одном DTO добавляю поля Tags, а в другом DTO нет таких полей, если они не нужны в функционале использующем DTO.

а теперь в том же DTO надо добавить новое поле, которое в Entity есть, но оно тоже ленивое. Что придется делать? Придется пойти и поправить запрос на уровне работающем с Entity!
Таким образом, информация о структуре DTO протекает на уровень работы с Entity.
И вот уже нет никакой "слоистой" архитектуры. Есть очередная "дырявая", в которой слои протекают друг в друга.

Извините, но то, что вы описываете здесь - высосанные из пальца проблемы и бредовые бессмысленные примеры. Тут либо слабое понимание того как это вообще работает и незнание о Projections, EntityGrath. Либо просто набрасывание на вентилятор.
И да - Pageble не работает @Query.Вы используете запрос то и используйте явные ограничения.

с чего это вдруг Pageable не работает с Query?!

я каждый день пишу запросы вроде

@Query("select o.* from OrderEntity o where o.vendor = :vendor")
Slice<OrderEntity> getAllByVendor(@Param("vendor") String vendor, Pageable pageable);

и все прекрасно работает!
limit/offset на месте
вы точно мануал по spring data jpa читали?

Давольно давно перешли на Data JDBC. В целом, мне нравится.

Но у него есть ряд проблем/сложностей.

Главная проблема - Criteria API. То, что он ограниченный - не страшно. Страшно то, что он закрытый для расширения. Равно, in, like - это хорошо, но современные БД - это массивы, json, полнотекстовый поиск и т.д.

Я попробовал реализовать свой оператор для array contains в Postgres, и у меня даже получилось. Но для этого пришлось создать пакеты "org.springframework.data.*" в своем приложении и дюжину классов в них, которые экстендят стандартные (а QueryMapper пришлось вообще почти целиком скопировать с небольшими изменениями). Потому что все package-private, никакого DI - все создается явно в конструкторах.

Другие сложности (эти уже решаемые), с которыми сталкивался:

  • enumeration в БД

  • конвертация из List в json-массив

Мы в своих проектах также ушли от Spring Data JPA в пользу Spring Data JDBC. Опять же, по причине постоянного LazyInitializationException (структуры БД часто таковы, что это исключение возникает рано или поздно, и приходилось его разрешать). Но главный вопрос - производительность. С переходом на JDBC она поднялась в разы. С другой стороны, JPA позволяет очень быстро набросать проект. Потому новые проекты порою начинаются всё в том же JPA, набрасываем крупными мазками свои идеи, и затем прегоняем в JDBC там, где нам нужна скорость (частично) или полностью.

Года три назад мне до ненависти осточертел JPA с его хибернейтами, что решил выбрать другой инструмент. Наткнулся на Jooq и доволен. Да, теперь всякие джойны и пагинации надо писать руками, но как минимум я знаю, как выполнится мой запрос без всяких N+1, все под моим контролем. И даже просто raw sql в логах увижу и не надо городить костыли как в Хибернейте чтобы увидеть обычный SQL и при надобности скопировать его и отладить уже в другом месте, в каком-нибудь приложении для DB (вот до сих пор не пойму - неужели это так трудно вывести строку запроса для отладки, чего хибернейт не умеет кажется до сих пор). Еще из коробки получаю маппинг в DTO, не идеальный, но в большинстве случаев его хватает, очень хорошую интеграцию с Котлином, максимальную гибкость с составлением динамических запросов на лету. Да все и не вспомнить.

Конечно же hibernate умеет показывать в логах сгенерированный параметризированный SQL запрос! Кажется, с самой первой версии умел.
Показать параметры запроса в логах тоже не проблема.

Разве? Помню когда я с ним работал, то он умел показывать запрос типа SELECT field1, field2 where firled3=? и следом параметры. Он не умел сразу готовый sql выдавать в логах, где уже все параметры на местах вместо "?".

Проблема заключалась в том, что у меня были многоэтажные огромные запросы, с 20+ параметрами и дебаг превращался в пытку. Я могу ошибаться конечно, и всё он умеет, и я на тот момент может не смог его правильно настроить. Но я помню там были какие-то отдельные решения в виде подключаемых библиотек, выглядело костыльно и мне не подходило по ряду причин, одна из них, это то, что запрещено было подключать что-то неодобренное по секьюрити.

Ну вот, выясняются интересные подробности. Сначала речь шла про raw-sql (который вполне себе пишется в лог), а теперь выясняется, что ожидался лог с sql, где параметры уже подставлены в параметризированный запрос.
Все верно, в логах будет запрос вида

select col_1, col_2, col_3 from my_table where col_1 = ? and col_2 = ?

а следом будет напечатан массив со значениями параметров.
так работает потому, что хибер, как и полагается хорошему ORM-у, создает PreparedStatement, а потом отдельно передает для него нужный набор параметров.
Вот он и в лог выводит параметризованный запрос отдельно и набор параметров отдельно.
для 98% случаев этого более чем достаточно.
но даже с 20-этажным запросом я не вижу особой проблемы прочитать лог в таком виде, или быстро превратить 2 части лога в одну строку

raw sql - я имел в виду именно уже готовый sql. Ок, здесь возникло недопонимание.

Но вот в чем дело - Jooq тоже создает Prepared Statement (да и любой другой ОРМ в мире джава - это просто маст хев от инъекций) а не конкатенатит параметры. Удивительно, да? Но при этом у Jooq есть функционал, где он в логи пишет как выглядит SQL который в данный момент выполнился на сервере, вот прям уже готовый SQL. И также в логи может нарисовать табличку с вернувшимся результатом. Эта мелочь исключительно для удобства разработчика. Но вот хибер такое не умеет. Хороший или плохой ОРМ Хибернейт не буду тут утверждать, кому-то он нравится, мне - нет по множеству причин, в том числе озвученных в этой статье.

И да, превратить после логов хибера SQL в строку конечно можно, каким-нибудь скриптом (что я в принципе и делал), или если есть доступ к серверу БД, то и там можно посмотреть, что выполнилось. Но опять же - это создает неудобство.

Прочитал и ужаснулся!

С чем приходится жить людям, которые не знают, что уже есть Go.

раскажи пожалуйста, каким образом язык программирования Go решает описанные проблемы объектно-реляционного маппинга?
мне всегда казалось, что концепция ORM не зависит от языка программирования.
видимо, я ошибаюсь, но очень хочу расширить свои знания по этому поводу!
у Go-шных ORM нет ленивой загрузки? но это не решение проблемы же!
или ленивая загрузка есть? но как тогда решается проблема состояний объекта относительно соединения с БД (attached/detached)?
у Go-шных ORM нет проблемы с N+1? а как же они тогда работают с полями объекта типа "коллекция"?
или просто в экосистеме Go даже близко нет аналога JPA/Hibernate и все, что экосистема Go может предложить в качестве ORM-а - это аналог JOOQ в том виде, в каком он был лет 10 назад?

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Информация

Сайт
www.haulmont.ru
Дата регистрации
Дата основания
Численность
501–1 000 человек
Местоположение
Россия