Комментарии 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.
Но мне ответить очень легко.
DTO - это не обязательно какой-то аналог Entity. DTO может включать в себя информацию из разных Entity объектов и слой, содержащий DTO, конечно не имеет никакого отношения к тому как и где реализованы транзакции.
Что касается LazyInitializationException, то совершенно не обязательно, что Entity наполняется при помощи ORM фреймворка, который вызывает упомянутую исключительную ситуацию. Но даже при использовании ORM функционал слоя работающего с Entity должен быть реализован так, чтобы исключить возникновение LazyInitializationException.
Что касается транзакций, то типовой алгоритм следующий. Допустим работа с 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 назад?
Почему Я выбираю Spring Data JDBC