Комментарии 9
Спасибо, интересно, шаг в правильном направлении. Имена методов в Spring Data могут монструозно разрастаться, после чего придется делать прыжок изменений, чтобы перейти хотя-бы к @Query
, а значит переписать весь запрос заново, что опасно. В Jakarta Data же, как я понял, имена методов не функциональны как таковые, что закладывает более надежную базу для расширения в будущем. Ну и проверка на этапе компиляции это плюс, хоть и окольными путями.
Но все же, Ebean круче по всем этим параметрам пока что, не говоря уже об остальном. Если вдруг кто при виде Jakarta Data подумал "щас заживем", лучше посмотрите на Ebean.
Ну это же просто прекрасно:
List<Customer> customers = new QCustomer()
.status.equalTo(Status.NEW)
.billingAddress.city.equalTo("Auckland")
.findList();
Как легко и прозрачно можно обратиться ко вложенному полю (customer.billingAddress.city
), типобезопасность, читабельные предикаты (например, вместо убожества Criteria API _builder.equal(_entity.get(Book_.title), title)
было бы просто .title.equalTo(title)
).
Поверх этого, Entity Graph, использование которого в JPA до сих пор боль как будто это что-то инородное, в Ebean оно programmatic и легко:
var fetchGroup = QCustomer.forFetchGroup()
.billingAddress.fetch()
.billingAddress.city.fetch()
.company.fetch()
List<Customer> customers = new QCustomer()
.select(fetchGroup)
...;
Не говоря уже о том, что Ebean по своей сути sessionless ORM (хотя можно и с ним), и dirty tracking идёт на уровне самой сущности, никаких тебе entity manager.
Или еще, например, в JPA нельзя просто так взять и сделать
person.setName(...)
а потом persist, если этот person не managed. Будь добр сделай сначала merge (бестолковый вызов к бд), и вообще всегда парься как и откуда person получен, managed он или нет...Был так же другой прикол (это возможно особенность именно реализации Spring Data, не уточнял) - если загрузить сущность с нужным тебе Entity Graph (т.е. вместе с прогрузкой нужных тебе связанных сущностей), а позже сделать
repository.save(entity)
, то сохранение работает, но граф сущностей пропадает.Его величество
LazyInitializationException
. Это ж надо было Hibernate умудриться сделать такое, что потом невозможно спокойно конвертировать сущности в DTO на Web слое, и нельзя делатьentity.setCompany()
если ты не удосужился это company поле прогрузить когда грузил саму entity... да боже не нужно мне делать join текущего значения company, если я просто хочу обновить это поле на другое company! В Ebean такая проблема отсутствует.Его величество
MultipleBagFetchException
. В 2025 ты как разработчик не достоин сделать такой простой запрос:
SELECT c FROM Category c
JOIN FETCH c.topics t
JOIN FETCH t.posts p
WHERE ...
потому что Hibernate паникует об cartesian product и не знает что делать. Посмотрите на предлагаемое решение (https://stackoverflow.com/a/30093606/2816631), это просто жесть. В ответ на замечание сие недоразумения для наших дней, что ORM мягко говоря не выполняет свою обязанность по абстрагированию пользователя от деталей исполнения запросов, Vlad Mihalcea сказал:
If you think ... is a disaster, then you are going to be very disappointed working with relational databases, no matter what data access framework you use.
Ну да... а Ebean говорит, братан, загружай че хочешь, я справлюсь, если надо я несколько запросов сделаю, мне не в падлу:
Ebean will never generate a SQL cartesian product. No matter how complex or big your ORM query gets Ebean will not generate a SQL cartesian product but instead break the query up into multiple SQL queries.
и решается повседневным Fetch Group:
var fetchGroup = QCategory.forFetchGroup()
.topics.fetch()
.topics.posts.fetch()
а адепты JPA похоже даже и не знают, что так можно.
Туда же Set vs List для OneToMany. Для Hibernate обязательно Set, иначе проблемы и вообще дублированные результаты, ведь для Hibernate, шок, что JOIN в табличном виде выдаёт дублированные данные. https://www.baeldung.com/spring-jpa-onetomany-list-vs-set#2-sets-and-cartesian-product И потом мучайся с этим Set, когда по бизнес логике везде List. Ebean же говорит, используй List.
Аналогично, до 6-ой версии Hibernate (которая вышла где-то лишь в 2020!) нельзя было написать такой простой запрос:
select p from Post p left join fetch p.comments where ...
ведь, шок, JOIN вернет дублированный post столько раз, сколько там comments. И ты был обязан делать:
select distinct p ... .setHint(QueryHints.HINT_PASS_DISTINCT_THROUGH, false)
ведь Hibernate не мог сделать это за тебя. https://stackoverflow.com/a/53406102/2816631
Куча других плюсов в Ebean... хотел короткий комментарий, но боль не даёт.
Я это к тому, что улучшения в JPA это хорошо, но JPA просто фундаментально из-за своего дизайна, который уже заложен в корне, такой какой есть. Поэтому кардинальных изменений ждать не стоит, как минимум в том числе из-за огромного пользовательского легаси кода, который не переживет огромных изменений в новой версии JPA. А поэтому, если хочется действительно лучшего опыта, нужно брать совсем другой продукт.
Поэтому, если кто не сталкивался с Ebean, советую изучить, для себя я решил, что полностью перешёл на Ebean, больше ни за что в JPA:
Немного странно начинать обсуждение EBean VS JPA в статье про Jakarta Data, ну да ладно.
Сначала хотел написать длинный комментарий по каждому пункту (может и напишу), а пока зайду сразу с козырей.
Глянул по диагонали документацию EBean, не увидел, подскажите, пожалуйста: его автор и пользователи в курсе про Read/Write Skew?
(я по фене ботать не умею, так, чисто для сохранения стилистики)
Ebean говорит, братан, загружай че хочешь, я справлюсь, если надо я несколько запросов сделаю, мне не в падлу:
Ebean will never generate a SQL cartesian product
Невер, говорит, не в падлу, говорит... А если потом пойдут предъявы за Read Skew, кто отвечать за базар неконсистентность данных будет?!
Это, естественно, не отменяет, такого же вопроса к абсолютно любому SQL-фреймворку, мой поинт в том, что невозможно с порога заявить, что такой проблемы больше нет.
И то же самое ещё раз
Его величество
LazyInitializationException
... В Ebean такая проблема отсутствует.
Т.е. OSIV (Open Session In View) тоже, выходит, "поощеряется" как в Spring Data JPA? А с Read Skew-то что?! Судя по ролику, который я глянул, транзакции былы разные.
LazyInitializationException
же явно говорит, что "Алё, гараж! Вы что-то не то делаете, транзакция закрыта ваще-то уже!"
На мой взгляд, Jakarta Data не решает корневые проблемы JPA, поэтому, если есть возможность, проще отказаться от JPA и поглядеть альтернативы, в том числе Ebean, поэтому я о нём и завёл речь. Особенно если интересоваться Jakarta Data в контексте выбора инструмента для нового проекта с нуля. В конечном итоге нас всех интересует удобный доступ к данным.
Я не говорил, что Ebean пытается или решает проблему Read/Write Skew. В контексте LazyInitializationException, под отсутствием проблемы я имел ввиду отсутствие выброса этого исключения - вместо него Ebean возвращает null для незагруженных полей (при отключенной lazy загрузке). Это удобнее - не вызывает проблем при маппинге в web DTO или при вызове toString (сгенерированным, например, через Lombok). Подобное поведение есть и в EclipseLink, например.
Если lazy загрузку включить (чего я не приветствую), поля будут "скрытно" загружаться, открывая сессию (подключение к бд) на лету, вместо её постоянного удержания, в отличие от классического OSIV. EclipseLink такое тоже умеет.
Hibernate же может подгружать поля "скрытно" (hibernate.enable_lazy_load_no_trans=true), но возвращать null вместо ошибки, нет. Соглашусь, иногда LazyInitializationException полезен. Но иногда это палки в колёса, когда разработчик знает что делает, а Hibernate кидается исключением.
В контексте SQL cartesian product, если я правильно понял, вы переживаете, что вместо одного запроса Ebean может выполнить несколько запросов рискуя получить read skew? Но ведь даже в рамках одного запроса в read committed (дефолт в sql server, например) по стандарту не гарантируется отсутствие read skew (т.е. могут быть прочитаны и старые и новые версии строк в результате).
В postgresql да, read committed использует снепшоты для каждого запроса, что гарантирует согласованность результата на уровне одного запроса, но это уже детали реализации конкретно postgresql, а по стандарту же, read skew возможен на всех уровнях ниже serializable.
Да и потом, в рамках бизнес логики, зачастую загружается несколько разных сущностей, приводя к вызову нескольких запросов так или иначе.
В Hibernate, один из workarounds (кидал ссылку: https://stackoverflow.com/a/30093606/2816631) всё так же использует несколько запросов для cartesian product. Разница в том, что Ebean это умеет автоматически, а Hibernate нет.
Ebean не поощряет OSIV. В Ebean вообще нет сессий в классическом понимании, про это ключевое отличие от JPA я упоминал и прикладывал ссылку (https://ebean.io/architecture/compare-jpa).
Исправляю ошибку 6-го пункта моего предыдущего комментария - в Hibernate можно сделать
entity.setCompany(...)
без загрузки company поля, это лишьget
нельзя. Это вообщем-то подтверждает, что безошибочно логически рассуждать о Hibernate/JPA зачастую трудно из-за витиеватости. В случае sessionless Ebean это проще.
Давайте разберёмся с определениями.
Entity может быть размазана по нескольким таблицам.
Если её читать несколькими запросами (и в это время другой процесс конкурентно её обновляет), получается Read Skew -- Entity может быть неконсистентна, бизнес-инварианты могут быть нарушены.
Чтобы избежать этого есть два подхода (и у каждого, конечно, своя "цена")
устанавливать уровень изоляции транзакции в REPEATABLE_READ или выше
на уровне READ_COMMITTED читать одним запросом -- `select ... from ... join ... join` -- это cartesian product
Если в Ebean заявляется, что
Ebean will never generate a SQL cartesian product
... то, если я верно понимаю, второй опции у разработчика вообще нет?
Что тогда, например, с Oracle предлагается делать? Включать SERIALIZABLE? Смириться с Read Skew? Не использовать Ebean?
Есть вообще в его документации какие-то рассуждения на эту тему?
Да и потом, в рамках бизнес логики, зачастую загружается несколько разных сущностей, приводя к вызову нескольких запросов так или иначе.
Несколько запросов для разных сущностей -- это нормально, это не Read Skew. Read Skew -- это именно о неконсистентной одной сущности.
Теперь о LazyInitializationException (LIE).
OSIV и hibernate.enable_lazy_load_no_trans=true -- это грязные хаки, давайте их не разбирать. Правильно -- в рамках repeatable_read транзакции entity консистентна, можно обращаться к любому lazy-полю, за пределами транзакции к энтити обращаться не желательно, о чём, в том числе, сигнализирует LIE.
Я не совсем понял, что предлагает Ebean. По ссылке https://ebean.io/architecture/compare-jpa написано
Partially populated beans always expected
Это что вообще имеется в виду?
И вы пишете
вместо него Ebean возвращает null для незагруженных полей
Можете какой-то практический пример привести, пожалуйста, для чего это и почему это хорошо?
И, главное, как в таком случае отличить реальный null
, т.е у поля нет значения, и когда оно не загружено?
Вы похоже подразумеваете, что read committed всегда идёт со snapshots, но ни в стандарте, ни в sql server, например, это не так, я уже упоминал.
Read skew может проявляться и в рамках одного запроса тоже. Read skew, исходя из официальной (если можно так выразиться) трактовки (https://habr.com/ru/articles/705332/#a5a-read-skew), это, если своими словами, чтение закомиченных данных, но совокупно неконсистентных из-за частично видимого апдейта другой транзакции. В рамках скольких запросов это будет проявляться зависит от реализации конкретной бд.
Поэтому, для предотвращения read skew:
да, в общем случае это repeatable read, или snapshot в нужной вариации (смотря где нужно отсутствие read skew: на уровне запроса или всей транзакции)
но нет, read committed на одном запросе не достаточно, а точнее зависит от реализации бд. Повторюсь, в стандартном read committed (как в sql server) не используются снапшоты (как в postgresql), поэтому вышеописанный read skew возможен даже при чтении сущностного графа одним запросом.
в рамках repeatable_read транзакции entity консистентна, можно обращаться к любому lazy-полю
^ опять же, в postgresql да (там снепшот на всю транзакцию), но не в sql server.
Я упоминал про нужду serializable для избежание read skew - это конечно лишнее, просто я держал в уме отсутствие фантомного чтения в том числе (чего repeatable read не гарантирует даже на одном запросе - примеры: https://habr.com/ru/articles/662407/), так как мы с вами вели речь о чтении консистентных данных как таковом.
Понятно, что некоторые бд, тот же postgresql, предоставляют read committed вместе со snapshots, что даёт удобные гарантии в рамках одного запроса. Но в общем случае (а ORM это general purpose инструмент), никто не захочет грузить cartesian product одним запросом. Более того, JPA не гарантирует сколько запросов будет выполнено в ходе операции чтения, и реализации вольны применять оптимизации, чему есть примеры в том же Hibernate и сегодня. Такое поведение не присуще именно Ebean. Поэтому строить решения исходя из жестких суждений о количестве исполняемых запросов под капотом не особо надежно, потому что ответ у ORM будет "сколько-то". Ну а Ebean тут можно похвалить, он предоставляет хотя-бы какую-то гарантию (что cartesian product не случится).
Если уж на то пошло, даже сейчас JPA не всегда даёт вам опцию исполнения cartesian product за один запрос - JPA не умеет фильтровать OneToMany коллекции в JPQL. С фильтрацией одной OneToMany коллекции можно выкрутиться делая SELECT дочерних сущностей (и фильтруя их) вместо родительских, а затем заниматься пляской с реконструкцией результата обратно в список родительских сущностей, но в случае нескольких OneToMany, вам придется сделать несколько отдельных запросов (по одному на дочернюю коллекцию). Ebean же позволяет это легко сделать ;) https://ebean.io/docs/query/filterMany.
Повторюсь, зачастую бизнес логика не ограничивается загрузкой лишь одной сущности, поэтому не вижу смысла упарываться в консистентное чтение именно cartesian product`а. А если очень нужно делать это одним запросом, можно открыть тикет в Ebean. Все остальные плюсы Ebean никуда при этом не деваются, так что не вижу ничего криминального в текущем поведении Ebean в отношении cartesian product, все в рамках ORM, и удобно дополняется filterMany возможностью упомянутой выше.
Про отсутствие LazyInitializationException я уже говорил, что это может быть делом вкуса, и приводил примеры где это удобно - не вызывает проблем при маппинге в web DTO или при вызове toString сгенерированным через Lombok (он вызывает deep toString, пытающийся прочесть все поля). Мне null импонирует меньшим coupling к самому существованию lazy loading и persistence слою или провайдеру вообще. А как жить без возможности отличия null незагруженного поля от реального null - ну так же, как jooq, mybatis итд, не катастрофа. Опять же, можно создать тикет в Ebean.
Вы похоже подразумеваете, что read committed всегда идёт со snapshots, но ни в стандарте, ни в sql server, например, это не так, я уже упоминал.
Приписывать собеседнику утверждение, а потом его опровергать -- это не очень честный приём.
Read skew может проявляться и в рамках одного запроса тоже
Это как?!
из-за частично видимого апдейта другой транзакции
Что такое "частично видимый апдейт другой транзакции"?! Это как?
Только не надо ссылок, пожалуйста, напишите своими словами, можно в той нотации, на которую ссылаетесь.
вышеописанный read skew возможен даже при чтении сущностного графа одним запросом
Это как?!
что некоторые бд, тот же postgresql, предоставляют read committed вместе со snapshots
Нет, postgres не предоставляет.
Ну а Ebean тут можно похвалить, он предоставляет хотя-бы какую-то гарантию (что cartesian product не случится)
Ок, ещё раз напишу. Раз cartesian product не случится, значит запросов больше одного.
А что тогда, например, с Oracle предлагается делать? Включать SERIALIZABLE? Смириться с Read Skew? Не использовать Ebean? Открывать тикет?
JPA не умеет фильтровать OneToMany коллекции в JPQL... Ebean же позволяет это легко сделать ;) https://ebean.io/docs/query/filterMany.
В общем, вкупе с рассуждениями про Read Skew я делаю предположение, что ни автор EBean, ни вы как его пользователь, не видите смысла упарываться в консистентное чтение. Ну, т.е. частичная загрузка дочерних сущностей -- вас абсолютно не смущает, поскольку энтити (в вашем мире) -- это не объект с бизнес-инвариантами, а так... какая-то структура для чтения данных из базы. Точка зрения понятна, переубеждать смысла не вижу, спасибо за дискуссию.
Приписывать собеседнику утверждение, а потом его опровергать -- это не очень честный приём.
Извините, что был столь высокого мнения о вас и посмел предположить, что вы знаете разницу между стандартным read committed и read committed со снепшотами, и ведёте речь о втором, а оказалось нет. Ну и я "предположил", а не утверждал? А вообще, давайте без язвы, вроде повода не давал?
Это как?!
Не получилось погуглить? Ну помогу. Из документации sql server:
Поведение READ COMMITTED зависит от настройки аргумента базы данных READ_COMMITTED_SNAPSHOT.
Если параметр READ_COMMITTED_SNAPSHOT находится в состоянии OFF (по умолчанию в SQL Server), ... Блокировка строки освобождается перед обработкой следующей строки. ...
Если параметр READ_COMMITTED_SNAPSHOT находится в состоянии ON (значение по умолчанию в Базе данных SQL Azure), ядро СУБД использует управление версиями строк для предоставления каждой инструкции согласованного на уровне транзакций моментального снимка данных в том виде, который он имел на момент начала выполнения инструкции. ...
То есть, в sql server в read committed, по умолчанию, при A join B, может случиться:
T1: прочитала строку A, отпустила блокировку строки
T2: пришла, обновила A и B, закомитилась, ушла
T1: прочитала строку B, отпустила блокировку
T1: соединила строку A (старую, уже прочитанную на первом шаге) со строкой B (уже содержащей новое значение)
В результате получили неконсистентную пару A и B, которая в таком виде совокупно никогда не существовала базе. Семантика read committed не нарушена, потому что оба A и B значения значатся как закоммиченные. Осмелюсь приложить ссылку на пример: https://learn.microsoft.com/en-us/answers/questions/264991/sql-server-isolation-behavior-during-count(*)-in-r Так работает read committed по стандарту. Потому и существует настройка READ_COMMITTED_SNAPSHOT для доп гарантий.
Воспроизвести легко. В первой транзакции запускаем джойн:
create table a (id int identity primary key, datavalue int);
create table b (id int identity primary key, datavalue int);
insert into a (datavalue) select 1 from generate_series(1, 1); -- одна
insert into b (datavalue) select 1 from generate_series(1, 1000000); -- много
select * from a cross join b
Одновременно, во второй апдейт обоих таблиц:
begin transaction;
update a set datavalue = 2
update b set datavalue = 2 where id = 1000000
commit;
Вторая транзакция завершится успешно до окончания первой. Дождавшись первую транзакцию, в результате увидим на последней строке следующее: a.id: 1 , a.datavalue: 1, b.id: 1000000, b.datavalue: 2. Видим старое a.datavalue и новое b.datavalue одновременно, несмотря на то, что вторая сессия обновила обе таблицы в одной транзакции успешно. Частично видимый апдейт. Планировщик может конечно и по другому сработать, смотря как повезёт.
Если переложить на объекты, по итогу имеем сущность с полем datavalue == 1, а в дочерней коллекции лежит объект с datavalue == 2. При этом совокупно в таком виде вместе они никогда не существовали в бд, так что мы получили неконсистентный результат за один запрос.
Ссылку на похожий пример, встречающийся в том числе в Repeatable Read, я приводил ранее.
Нет, postgres не предоставляет.
Из документации postgresql:
13.2.1. Уровень изоляции Read Committed
... В транзакции, работающей на этом уровне, запрос SELECT (без предложения FOR UPDATE/SHARE) видит только те данные, которые были зафиксированы до начала запроса; он никогда не увидит незафиксированных данных или изменений, внесённых параллельными транзакциями в процессе выполнения запроса. По сути запрос SELECT видит снимок базы данных в момент начала выполнения запроса.
Документация вполне ясно говорит про использование snapshots (на уровне запроса) в read committed? За подробностями предлагаю погуглить.
не видите смысла упарываться в консистентное чтение
Ну если вам по прежнему не так важно, что чтением одной сущности бизнес логика далеко не ограничивается и, так называемые бизнес-инварианты, важны для всего working set тоже, а потому множественным запросы неизбежны, где JPA проявляет себя не лучше, и также вы похоже предпочли проигнорировать мой пример где несколько запросов в JPA при cartesian product тоже неизбежны и делаете вид, будто это присуще только Ebean, и видите что-то отрицательное в том, что в Ebean, будучи general purpose ORM (как и другие), был сделан удобный трейдофф в пользу избегания взрыва памяти приложения из-за огромных cartesian product, и предпочитаете зацикливаться на чтении cartesian product одним запросом во что бы то ни стало (ну и на Oracle еще), будто это определяющий кейс использования ORM инструмента, или будто строго негативно характеризующий его в целом в сравнении с JPA (а о цельном сравнении я речь и заводил), то да, мне больше нечего сказать. Все факты и примеры я доходчиво изложил, не вижу пользы для сообщества в дальнейшем обсуждении.
А вообще, давайте без язвы, вроде повода не давал?
Мы оба даём друг другу поводы, высказывая утверждения, кажущиеся полной глупостью собеседнику, на которые тот с удовольствием посылает в гугл. Так что без язвительности в нашем деле никак, увы.
посмел предположить, что вы знаете разницу между стандартным read committed и read committed со снепшотами
А я и не знал. Я вообще не понял, о каких снепшотах вы пишете, мне показалось, что вы read committed и repeatable read путаете, поэтому да, я действительно подразумевал, что read committed всегда идёт с ними. И, конечно, в Postgres это так. Что сказать -- спасибо, что просветили.
но ни в стандарте, ни в sql server, например, это не так
Да, вы правы, и для меня это натурально открытие после надцати лет разработки. В своё оправдание могу сказать, что, кажется, MSSQL не так популярен в Java мире, как остальные три БД, я с ним -- так получилось -- ни разу не сталкивался.
Read skew может проявляться и в рамках одного запроса тоже
Я, пожалуй, возьму вашу ссылку и проверю коллег спором на деньги), уверен, многие ошибутся, а что -- в формулировке не будет сказано, на какой БД проверяем.
Единственно, мне сходу сложно представить, как read committed без snapshots в MSSQL можно вообще использовать, если там даже самый обычный `select ... from table` может неконсистентые данные возвращать?
Ок, с транзакциями разобрались, спасибо ещё раз, возвращаемся к EBean.
Ну если вам по прежнему не так важно, что чтением одной сущности бизнес логика далеко не ограничивается
Нет, я пишу прямо противоположное, мне важно, чтобы даже одна сущность была прочитана в консистентном состоянии.
Поэтому меня и смутил подход EBean, возникло ощущение, что с транзакциями и множественными запросами обращаются достаточно свободно, дочерние энтити можно загрузить частично, где-то вообще может быть null... т.е. где целостность-то?
Т.е. с JPA я могу быть уверен, что
@Transactional // очень желательно REPEATABLE_READ, конечно
void someMethod() {
var entity = ... // откуда-то загружена или взята из другой энтити
entity.doSomething();
// из-за Spring Data JPA многие, увы, привыкли добавлять repo.save(), но он не нужен
}
.. метод doSomething()
произведёт перевод сущности из одного консистетного состояния в другое.
А вот с EBean это неочевидно, хотя бы из-за https://ebean.io/docs/query/filterMany и вообще
Partially populated beans always expected
... т.е., если я правильно понял, за целостностью сущности предлагается следить самостоятельно, зато без декартова произведения?
Как по мне -- сомнительное преимущество.
Всё дальше мы от ООП.
Без бутылки не разобраться.
Сначала долго ведут в сторону stateless
Заметьте, что update — это отдельная операция, а репозитории Jakarta Data всегда stateless. В них нет persistence контекста.
Потом вспоминают про stateful
Jakarta Data должна сама по себе поддерживать stateful persistence контексты, а это по сути означает новый API.
По сути это означает ещё одну имплементацию JPA к уже имеющимся (Hibernate, EclipseLink -- с этимя двумя работал, OpenJPA -- c этим нет)
Это абсолютно разные, фундаментально разные модели программирования. Невозможно рационально и эффективно создать абстракцию поверх этих двух моделей, а если вы попытаетесь, вы только создадите все разновидности путаницы в голове пользователя.
Святая правда.
---
Всякие улучшайзинги по чтению из БД уже есть. И с типобезопасностью. Да, CriteriaAPI действительно не очень, поэтому есть Spring Data JPA, в нём Query by Example, а ещё QueryDSL (мой фаворит для сложных запросов) и, наверняка, много ещё, чего я не знаю.
Подозреваю, успех derived методов Spring Data JPA связан с тем, что во большинстве случаев их и достаточно. А на многословное имя, которое не влазит в экран (реализацию, по сути) можно и нужно сделать default метод -- бизнес-алиас -- в репозитории.
А в что с отображением изменения состояния энтити в БД?
Если говорить об операциях обновления данных, они, как правило, тривиальны.
Это точно?
Вот и Entity выглядят как настоящие, и @ManyToMany
вроде бы есть, а вот (специально напишу без инкапсуляции, чтобы было наглядно)
book.getAuthors().get(0).setFirstName("Misha");
book.getAuthors().add(new Author("John", "Doe"));
library.upsert(book)
... работает?
Jakarta Data и Persistence: Инструменты, которые меняют подход к работе с данными