По поводу страшилки 20 тыщ руб за "ядра 4, памяти 16, а SSD - 400".
Прямо сейчас, эта конфига в Яндекс Облаке - 12 700 руб: CPU (4) - 3 701 руб RAM (16) - 3 856 руб SSD (400) - 5 145,12 руб причем половина съедена диском, а не памятью.
По памяти выходит 240 руб / 1 ГБ. У бизнеса нет денег потратить лишний гигабайт за 240 руб, но есть деньги на микросервисы, кубер и танцы с native?
Вместо 400 SSD для микросервиса можно меньше. Если речь про отдельный виртуальный хост, то 20 ГБ убунта, 1 ГБ docker образ spring boot приложения (а с оптимизациями в разы меньше), например. Пускай у нас постоянно лежат десятки версий образов, плюс базовый софт и базовые логи, итд. Как будто 200 ГБ было бы тоже достаточно. Это то, что "эффективный менеджер" может сделать уже сегодня в один клик? Если это кубер, суть та же, хватит нескольких десятков ГБ на хранение разных версий образа - остальное пространство будет занято вне зависимости от native vs non-native.
"Кубер перезапускает приложения и двигает по кластеру." - нам в любом случае нужно время на graceful shutdown для избежания потери текущих запросов, а значит есть время запустить новый инстанс.
По поводу динамического скейлинга - тушить то приложения динамически можно, и высвободить в native случае 100 МБ, например, но такой объем не всегда означает, что саму ноду (за что платятся деньги) можно выключить тоже.
В любом случае, мне интересно к чему придет GraalVM в итоге.
Приписывать собеседнику утверждение, а потом его опровергать -- это не очень честный приём.
Извините, что был столь высокого мнения о вас и посмел предположить, что вы знаете разницу между стандартным 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 всегда идёт со 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.
На мой взгляд, 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 это проще.
Спасибо, интересно, шаг в правильном направлении. Имена методов в 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)
Куча других плюсов в Ebean... хотел короткий комментарий, но боль не даёт.
Я это к тому, что улучшения в JPA это хорошо, но JPA просто фундаментально из-за своего дизайна, который уже заложен в корне, такой какой есть. Поэтому кардинальных изменений ждать не стоит, как минимум в том числе из-за огромного пользовательского легаси кода, который не переживет огромных изменений в новой версии JPA. А поэтому, если хочется действительно лучшего опыта, нужно брать совсем другой продукт.
Поэтому, если кто не сталкивался с Ebean, советую изучить, для себя я решил, что полностью перешёл на Ebean, больше ни за что в JPA:
1. Индивидуальные запросы, как таковые, в Hibernate нормальные, можете включить дебаг и проверить. Проблема в том как Hibernate ими пользуется, и его весь остальной функционал. Hibernate (и JPA в целом) ужасная реализация ORM.
2. Вместо них, советую посмотреть Ebean ORM, выглядит намного лучше.
3. В любом случае, почти любой ORM нормально справится с подтяжкой ManyToOne графа, и это удобно. Все что сложнее, есть наивные запросы, что не значит, что не нужно использовать ORM для простых случаев. Вот только JPA не очень здесь способствует, и в частности не любит дружить с графами объектов, которые были сформированы извне, руками разработчика (что обычно происходит в случае ручного маппинга результатов нативного сложного запроса). Ebean опять же здесь смотрится лучше.
4. По поводу реактивных драйверов. Известная статья HikariCP по поводу размеров connection пула к базе даёт понять, что не так уж и много одновременных запросов мы можем себе позволить, а значит и подключений (потоков) тоже. Тогда в целом реактивность, которая в теории даёт кучу подключений за дёшево, особо и не вперлась. А если же все таки ваша база способна выдержать сотни запросов, то это уже стоит вам столько, что наверняка можно себе позволить пару сотен потоков на бекенд приложении. В том числе, бекенды обычно в кластере, и возможности базы распределяются между ними всеми, так что пул каждого пропорциально нужно будет уменьшить.
5. R2DBC фу, ни ORM ни то ни сё. Слишком мало может.
Не делайте таких выводов по Web Framework Benchmarks. Там сетап всех приложений абсолютно разный, поддерживается самими комьюнити. Хотя все и участвуют в одном рейтинге, за идентичностью никто не следит.
Бенчмарки дело такое, нужно очень чётко посмотреть исходник, понять что именно измеряется и о чем говорят результаты, а потом признать, что все таки не понял, и ещё раз смотреть, и так раз 10.
По тенической части, это наверное полезно, если стало работать быстрее, пользователям конечно лучше.
Но выводы по метрикам в конце - мизерные проценты на уровне погрешности, и врядли учитывают естественный прирост пользователей со временем (без ваших улучшений), сезонность, настроение населения итп миллион причин. Я бы не стал делать выводы по таким результатам.
Не прям уж маркетинговый. Тут юзер понимает как можно сообщить ресторану если что-то не понравилось - оставить негативный отзыв (который бургер кингу может и не нужен), почувствовать важность, повлиять на ресторан, или наборот, похвалить, помочь оценить ресторан для других юзеров (а значит и себя самого).
Продвижение Project Loom, это здорово. Будут интересны результаты Spring MVC, когда появится "коробочная" версия, без танцев с бубном.
В тестах Spring MVC, надеюсь, количество tomcat потоков было увеличено с дефолтных 100?
Советую сделать тесты с jetty - изменение одной строки, по прежнему коробочный MVC, а производительность намного выше (по моим наблюдениям).
Хочется отметить ещё раз важность понимания что именно тестируется. Тесты WebMvc Jdbi это наглядно демонстрируют. Может получиться так, что не так далеко нужно уйти от коробочной версии, чтобы получит схожий буст производительности. Ещё пример - Spring Data и нативный R2DBC имеют разницу в примерно 1.5 раза.
Все таки важно тестировать на разных машинах. В частности, сетевые запросы к бд, вроде бы, и не идут через local loopback, из-за использования докера, но подозреваю, что все таки, это намного быстрее реального сетевого запроса. В реальности вся видимая разница может исчезнуть.
Важно тестировать на нескольких CPU. Например, R2DBC прям до недавнего времени имел огромную деградацию, проявляющуюся на многоядерности.
Про деньги - как бы да, дешевле. Но нужно помнить и про затраты на танцы с новым фреймворком. Его дружбе со всей другой экосистемой итп. Посчитайте сколько стоит один разраб в год, занимающийся этим.
Как по мне, WebServer.builder() такая же "магия" как и @RestController. Как ни крути, любой фреймворк, его поведения итд нужно учить и понимать. Не избежать этого, хоть с аннотациями, что без.
Согласен, Java быстрая. Нужны просто прямые руки. А если прям 200% уверены, что ботлнек не в бд, то есть Vertx с Vertx Sql Client.
Помните, переписав свой проект с нуля, зачастую получится быстрее даже на старом фреймворке! :)
А вам не кажется дикостью оценивать человека по количеству написанных им юнит тестов? Разные тесты требуют разное время на понимание тестируемого компонента, решение трудностей при написании теста из-за того как этот компонент написан.
Может быть вы еще и по объему кода человека оцениваете?
Как вы заботитесь о своей производительности (прибыльности) понятно. Расскажите как вы заботитесь о росте своих сотрудников, включая soft и hard skills.
По поводу страшилки 20 тыщ руб за "ядра 4, памяти 16, а SSD - 400".
Прямо сейчас, эта конфига в Яндекс Облаке - 12 700 руб:
CPU (4) - 3 701 руб
RAM (16) - 3 856 руб
SSD (400) - 5 145,12 руб
причем половина съедена диском, а не памятью.
По памяти выходит 240 руб / 1 ГБ. У бизнеса нет денег потратить лишний гигабайт за 240 руб, но есть деньги на микросервисы, кубер и танцы с native?
Вместо 400 SSD для микросервиса можно меньше. Если речь про отдельный виртуальный хост, то 20 ГБ убунта, 1 ГБ docker образ spring boot приложения (а с оптимизациями в разы меньше), например. Пускай у нас постоянно лежат десятки версий образов, плюс базовый софт и базовые логи, итд. Как будто 200 ГБ было бы тоже достаточно. Это то, что "эффективный менеджер" может сделать уже сегодня в один клик? Если это кубер, суть та же, хватит нескольких десятков ГБ на хранение разных версий образа - остальное пространство будет занято вне зависимости от native vs non-native.
"Кубер перезапускает приложения и двигает по кластеру." - нам в любом случае нужно время на graceful shutdown для избежания потери текущих запросов, а значит есть время запустить новый инстанс.
По поводу динамического скейлинга - тушить то приложения динамически можно, и высвободить в native случае 100 МБ, например, но такой объем не всегда означает, что саму ноду (за что платятся деньги) можно выключить тоже.
В любом случае, мне интересно к чему придет GraalVM в итоге.
Извините, что был столь высокого мнения о вас и посмел предположить, что вы знаете разницу между стандартным read committed и read committed со снепшотами, и ведёте речь о втором, а оказалось нет. Ну и я "предположил", а не утверждал? А вообще, давайте без язвы, вроде повода не давал?
Не получилось погуглить? Ну помогу. Из документации sql server:
То есть, в 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 для доп гарантий.
Воспроизвести легко. В первой транзакции запускаем джойн:
Одновременно, во второй апдейт обоих таблиц:
Вторая транзакция завершится успешно до окончания первой. Дождавшись первую транзакцию, в результате увидим на последней строке следующее: a.id: 1 , a.datavalue: 1, b.id: 1000000, b.datavalue: 2. Видим старое a.datavalue и новое b.datavalue одновременно, несмотря на то, что вторая сессия обновила обе таблицы в одной транзакции успешно. Частично видимый апдейт. Планировщик может конечно и по другому сработать, смотря как повезёт.
Если переложить на объекты, по итогу имеем сущность с полем datavalue == 1, а в дочерней коллекции лежит объект с datavalue == 2. При этом совокупно в таком виде вместе они никогда не существовали в бд, так что мы получили неконсистентный результат за один запрос.
Ссылку на похожий пример, встречающийся в том числе в Repeatable Read, я приводил ранее.
Из документации postgresql:
Документация вполне ясно говорит про использование snapshots (на уровне запроса) в read committed? За подробностями предлагаю погуглить.
Ну если вам по прежнему не так важно, что чтением одной сущности бизнес логика далеко не ограничивается и, так называемые бизнес-инварианты, важны для всего working set тоже, а потому множественным запросы неизбежны, где JPA проявляет себя не лучше, и также вы похоже предпочли проигнорировать мой пример где несколько запросов в JPA при cartesian product тоже неизбежны и делаете вид, будто это присуще только Ebean, и видите что-то отрицательное в том, что в Ebean, будучи general purpose ORM (как и другие), был сделан удобный трейдофф в пользу избегания взрыва памяти приложения из-за огромных cartesian product, и предпочитаете зацикливаться на чтении cartesian product одним запросом во что бы то ни стало (ну и на Oracle еще), будто это определяющий кейс использования ORM инструмента, или будто строго негативно характеризующий его в целом в сравнении с JPA (а о цельном сравнении я речь и заводил), то да, мне больше нечего сказать. Все факты и примеры я доходчиво изложил, не вижу пользы для сообщества в дальнейшем обсуждении.
Вы похоже подразумеваете, что 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 возможен даже при чтении сущностного графа одним запросом.
^ опять же, в 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.
На мой взгляд, 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 это проще.Спасибо, интересно, шаг в правильном направлении. Имена методов в Spring Data могут монструозно разрастаться, после чего придется делать прыжок изменений, чтобы перейти хотя-бы к
@Query
, а значит переписать весь запрос заново, что опасно. В Jakarta Data же, как я понял, имена методов не функциональны как таковые, что закладывает более надежную базу для расширения в будущем. Ну и проверка на этапе компиляции это плюс, хоть и окольными путями.Но все же, Ebean круче по всем этим параметрам пока что, не говоря уже об остальном. Если вдруг кто при виде Jakarta Data подумал "щас заживем", лучше посмотрите на Ebean.
Ну это же просто прекрасно:
Как легко и прозрачно можно обратиться ко вложенному полю (
customer.billingAddress.city
), типобезопасность, читабельные предикаты (например, вместо убожества Criteria API_builder.equal(_entity.get(Book_.title), title)
было бы просто.title.equalTo(title)
).Поверх этого, Entity Graph, использование которого в JPA до сих пор боль как будто это что-то инородное, в Ebean оно programmatic и легко:
Не говоря уже о том, что 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 ты как разработчик не достоин сделать такой простой запрос:потому что 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:
а адепты 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!) нельзя было написать такой простой запрос:
ведь, шок, JOIN вернет дублированный post столько раз, сколько там comments. И ты был обязан делать:
ведь Hibernate не мог сделать это за тебя. https://stackoverflow.com/a/53406102/2816631
Куча других плюсов в Ebean... хотел короткий комментарий, но боль не даёт.
Я это к тому, что улучшения в JPA это хорошо, но JPA просто фундаментально из-за своего дизайна, который уже заложен в корне, такой какой есть. Поэтому кардинальных изменений ждать не стоит, как минимум в том числе из-за огромного пользовательского легаси кода, который не переживет огромных изменений в новой версии JPA. А поэтому, если хочется действительно лучшего опыта, нужно брать совсем другой продукт.
Поэтому, если кто не сталкивался с Ebean, советую изучить, для себя я решил, что полностью перешёл на Ebean, больше ни за что в JPA:
https://ebean.io/architecture/compare-hibernate
https://ebean.io/architecture/compare-jpa
https://ebean.io/docs/query/where
https://stackoverflow.com/a/66405439/2816631
Сори за некропостинг, но можно узнать что за 90 дней? Дедик в селектеле на заказ за 5 дней соберут.
1. Индивидуальные запросы, как таковые, в Hibernate нормальные, можете включить дебаг и проверить. Проблема в том как Hibernate ими пользуется, и его весь остальной функционал. Hibernate (и JPA в целом) ужасная реализация ORM.
2. Вместо них, советую посмотреть Ebean ORM, выглядит намного лучше.
3. В любом случае, почти любой ORM нормально справится с подтяжкой ManyToOne графа, и это удобно. Все что сложнее, есть наивные запросы, что не значит, что не нужно использовать ORM для простых случаев. Вот только JPA не очень здесь способствует, и в частности не любит дружить с графами объектов, которые были сформированы извне, руками разработчика (что обычно происходит в случае ручного маппинга результатов нативного сложного запроса). Ebean опять же здесь смотрится лучше.
4. По поводу реактивных драйверов. Известная статья HikariCP по поводу размеров connection пула к базе даёт понять, что не так уж и много одновременных запросов мы можем себе позволить, а значит и подключений (потоков) тоже. Тогда в целом реактивность, которая в теории даёт кучу подключений за дёшево, особо и не вперлась. А если же все таки ваша база способна выдержать сотни запросов, то это уже стоит вам столько, что наверняка можно себе позволить пару сотен потоков на бекенд приложении. В том числе, бекенды обычно в кластере, и возможности базы распределяются между ними всеми, так что пул каждого пропорциально нужно будет уменьшить.
5. R2DBC фу, ни ORM ни то ни сё. Слишком мало может.
6. Ждём продакшен Loom.
Не делайте таких выводов по Web Framework Benchmarks. Там сетап всех приложений абсолютно разный, поддерживается самими комьюнити. Хотя все и участвуют в одном рейтинге, за идентичностью никто не следит.
Бенчмарки дело такое, нужно очень чётко посмотреть исходник, понять что именно измеряется и о чем говорят результаты, а потом признать, что все таки не понял, и ещё раз смотреть, и так раз 10.
По тенической части, это наверное полезно, если стало работать быстрее, пользователям конечно лучше.
Но выводы по метрикам в конце - мизерные проценты на уровне погрешности, и врядли учитывают естественный прирост пользователей со временем (без ваших улучшений), сезонность, настроение населения итп миллион причин. Я бы не стал делать выводы по таким результатам.
Не прям уж маркетинговый. Тут юзер понимает как можно сообщить ресторану если что-то не понравилось - оставить негативный отзыв (который бургер кингу может и не нужен), почувствовать важность, повлиять на ресторан, или наборот, похвалить, помочь оценить ресторан для других юзеров (а значит и себя самого).
Продвижение Project Loom, это здорово. Будут интересны результаты Spring MVC, когда появится "коробочная" версия, без танцев с бубном.
В тестах Spring MVC, надеюсь, количество tomcat потоков было увеличено с дефолтных 100?
Советую сделать тесты с jetty - изменение одной строки, по прежнему коробочный MVC, а производительность намного выше (по моим наблюдениям).
Хочется отметить ещё раз важность понимания что именно тестируется. Тесты WebMvc Jdbi это наглядно демонстрируют. Может получиться так, что не так далеко нужно уйти от коробочной версии, чтобы получит схожий буст производительности. Ещё пример - Spring Data и нативный R2DBC имеют разницу в примерно 1.5 раза.
Все таки важно тестировать на разных машинах. В частности, сетевые запросы к бд, вроде бы, и не идут через local loopback, из-за использования докера, но подозреваю, что все таки, это намного быстрее реального сетевого запроса. В реальности вся видимая разница может исчезнуть.
Важно тестировать на нескольких CPU. Например, R2DBC прям до недавнего времени имел огромную деградацию, проявляющуюся на многоядерности.
Про деньги - как бы да, дешевле. Но нужно помнить и про затраты на танцы с новым фреймворком. Его дружбе со всей другой экосистемой итп. Посчитайте сколько стоит один разраб в год, занимающийся этим.
Как по мне,
WebServer.builder()
такая же "магия" как и@RestController
. Как ни крути, любой фреймворк, его поведения итд нужно учить и понимать. Не избежать этого, хоть с аннотациями, что без.Согласен, Java быстрая. Нужны просто прямые руки. А если прям 200% уверены, что ботлнек не в бд, то есть Vertx с Vertx Sql Client.
Помните, переписав свой проект с нуля, зачастую получится быстрее даже на старом фреймворке! :)
Есть же Pingdom.
Может быть вы еще и по объему кода человека оцениваете?
Как вы заботитесь о своей производительности (прибыльности) понятно. Расскажите как вы заботитесь о росте своих сотрудников, включая soft и hard skills.