Основные причины "тормозящего" хибернейта -- это, конечно, не сам хибернейт, а разработчики вокруг него.
Основные проблемы:
Отсутствие индексов!
Если для @ManyToOne primary key забыть невозможно, то вот для
class Parent {
@OneToMany(mappedBy = "parent")
List<Child> children;
}
... parent_id в таблице children -- вполне. Проверьте!
Явно установленный EAGER для `@OneToMany(fetch = EAGER)`. То ли для борьбы с LazyInitializationException, то ли для "загрузки" данных, то ли ещё для чего-то. Так делать не надо, источник N+1 проблемы. Правильно: маппить энтити в responseDto.
Переопределённый `@ManyToOne(fetch = LAZY)`. То ли после прочтения этой статьи, то ли аналогичных. Тоже потенциальный источник N+1 проблемы. Можно менять только полном понимании всех трейдоффов.
Это была рубрика "вредные советы", ничего из перечисленного в статье делать не надо. В хибернейте разумные дефолты (OSIV не в счёт, это Spring Data JPA, а не сам хибер), что-то tweak-ать надо с полным пониманием.
Кто такой вообще этот Мацей Валковяк и почему его постят?
Поехали разбираться.
Поэтому Spring Data рассуждает следующим образом: “О, это сущность, которая, как утверждает callee, уже существует! Однако мне надо удостовериться, существует ли она на самом деле. Поэтому я сделаю SELECT в базу данных, чтобы убедиться, есть ли там эти данные.”
Чушь! Это не так.
Во-первых, Spring Data навязал репозиторный метод save() в том числе и для Spring Data JPA (для единообразия), оказав тем самым медвежью услугу. В абсолютном большинстве случаев нужны em.persist() для новых entities и "ничего" для изменения managed ones.
Хорошо бы понимать, для чего em.merge() (спойлер -- в нормальных кейсах он не нужен) и какой у него сайд-эффект при вручную установленных ID, вот эта самая лишняя загрузка.
Именно так и появился тот длинный SELECT, который поставил нас в тупик и существенно замедлил работу нашего приложения.
Скорость работы селекта определяется не его длиной)
Лишний селект, это, конечно, никогда не хорошо, но в данном конкретном случае -- join по primary key -- существенно замедлить работу не должен.
Альтернатива состоит в том, чтобы реализовать интерфейс Persistable.
Вот такого точно не надо, пожалуйста.
И @Version тоже добавляйте для Optimistic Lock, а не для каких-то других целей.
Смущает лишний select при "сохранении" энтити с предустановленными ID -- просто используйте em.persist()!
добавить аннотацию @Transactional
Ну, хорошо что про это всё таки вспомнили, хорошо бы с этого начать, а заодно понимать, как оно до этого работало.
При использовании getReferenceById(), если такой счет не существует, операция потерпит неудачу только после вызова save(). Однако, всеми этими проблемами можно управлять.
И как управлять-то?
Прежде чем "ускорять" приложение с помощь getReferenceById() хорошо бы понимать трейд-офф между ним и обычным findById(id).orElseThrow(...).
Обычный findById делает запрос в БД и кидает указанный экспешн, который -- обычная практика -- переоборачивается в 400 или 404.
getReferenceById() ничего этого не делает, id используется как есть в надежде, что есть foreign key. Парсить ForeignKeyConstraintViolationException тоже можно (а возможно и нужно в сильно конкуретной среде), но строить на нём BadRequest/ResourseNotFoundException несколько сложнее.
Этот код загружает данные, мапит их на DTO, и затем в контроллере это сериализуется в JSON. Не очень хороший код.
Почему это не очень хороший код?! Вот как раз правильно сделано!
> В дальнейшем для каждого элемента в списке найденых переводов будут выполняться еще по два запроса, так что у нас в конце концов появляется ситуация, когда чем больше результат выполнения первоначального запроса, тем больше запросов мы выполняем по сумме. Это называется проблема N+1.
Откуда тут N+1?! Видимо, автор запутался в своих тестах и получил N+1, как раз когда поставил `@ManyToOne(fetch = LAZY)`
Когда у нас есть отношения @ManyToOne или @ManyToMany, необходимо всегда менять FetchType на LAZY
Не нужно. Во-первых, @ManyToMany и так LAZY по дефолту.
А что касается исправления @ManyToOne на LAZY то, опять же, надо понимать трейд-офф.
C EAGER мы `left join`-ом загружаем данные всегда (как, собственно, автор и указал вначале)
from bank_transfer btl_0
left join account r1_0 on r1_0.id = btl_0.receiver_id
left join account s1_0 on s1_0.id = btl_0.sender_id
Они могут нам понадобиться для бизнес-логики/маппинга в ДТО, а могут и не понадобиться. Если у нас инкапсулированная модель, то в общем случае мы этого заранее не знаем, и знать не можем.
JOIN по первичному ключу, естественно, не бесплатен, но, будем считать, относительно дешёв.
В противном случае, с `@ManyToOne(fetch = LAZY)` на любом запросе коллекции энтити как раз-то и есть риск получать N+1 проблему. Обычно это существенно медленнее, и поэтому дефолтное значение как раз EAGER.
Короче говоря, дефолтные `@ManyToOne(fetch = EAGER)` и `@OneToMany(fetch = LAZY)` -- это нормально и менять их не надо.
Кроме того, SQL запросы лучше писать самим и указывать все нужные нам данные в явном виде, чтобы не загружать лишнего.
Обычно лучше не писать. Но вообще это философский спор между инкапсуляцией и быстродействием.
когда мы выполняем UPDATE, PostgreSQL в любом случае вставляет полностью новую колонку.
СТРОКУ, а не колонку!
Select не будет сделан, если у энтити есть идентификатор, генерируемый на стороне приложения, равный null
А, вижу, добавили. Может, поднимите наверх, где "проблема" обсуждается и спойлер уберите, чтобы сразу в глаза бросалось?
@DynamicUpdate
Мы прогоняем тест и смотрим на UPDATE, который выполняется в базе данных. Почти каждый ORM, включая Hibernate, просто установят значения для каждой колонки, которая есть в этой сущности, даже если значение для этой колонки не поменялось.
Самое время задуматься, чем руководствовались разработчики Хибернейта, и почему сделано именно так. Разобраться с `JDBC PreparedStatement` и кешем запросов.
По мелочи.
Бизнес-конструкторы -- это хорошо и правильно, пустой конструктор нужен хибернейту, но его лучше сделать protected, чтобы запретить вызывать из кода.
Я бы предложил для случаев, когда идёт микс из обращений к БД и внешним сервисам (читай -- медленное I/O), и мы переходим на ручное управление транзакциями, не стыдливо комментирировать аннотацию
Я тоже периодически так делаю, но мне постоянно высказывают, что это неправильно и что на самом деле надо мокать сетевые вызовы с помощью Wiremock.
Предлагаю компромисс.
Если по ресту ходит готовый клиент -- например, предоставленный сторонним сервисом или feign-клиент, сгенерированный по спеке -- то считаю вполне допустимым его мокать с помощью @MockBean (один раз в базовом тестовом классе, вестимо, чтобы не плодить контексты).
Если же мы используем что-то низкоуровненное -- всякие restTemplate, webClient и прочее, в общем пишем queryParams и pathVariables "руками", то лучше Wiremock. Хотя бы из-за того, что "given" будет написано в другом формате и более наглядно. Иначе получается масло масляное -- вызываем restTemplate с аргументами... и проверяем, что вызываем с правильными аргументами? Нехорошо.
Ещё лучше, конечно, если сторонний эндпоинт выдаст stub-ы в рамках Contract Testing-а (Spring Cloud Contract), но на практике я такого счастья пока не встречал.
А вообще, давайте без язвы, вроде повода не давал?
Мы оба даём друг другу поводы, высказывая утверждения, кажущиеся полной глупостью собеседнику, на которые тот с удовольствием посылает в гугл. Так что без язвительности в нашем деле никак, увы.
посмел предположить, что вы знаете разницу между стандартным 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() произведёт перевод сущности из одного консистетного состояния в другое.
В общем, вкупе с рассуждениями про Read Skew я делаю предположение, что ни автор 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 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 же явно говорит, что "Алё, гараж! Вы что-то не то делаете, транзакция закрыта ваще-то уже!"
Заметьте, что 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 вроде бы есть, а вот (специально напишу без инкапсуляции, чтобы было наглядно)
Ок, короткая шпаргалка, что такое JPA, зачем и как с ним работать.
JPA -- это Java Persistence API, фреймворк, который позволяет мапить Java объекты на реляционную базу данных.
Что это значит на практике? Допустим, у нас есть такой класс (и объекты -- экземпляры этого класса)
public class Order {
private Map<Item, Integer> itemsAndTheirAmounts = new HashMap<>();
private Set<PromoCode> promoCodes = new HashSet<>();
private Instant createdAt = Instant.now();
private Money totalPrice = Money.of(0, Monetary.getCurrency("RUB"));
}
... в Item и PromoCode, в свою очередь, наверчено ещё что-то и т.д.
Пока мы работаем с ними в оперативной памяти -- всё ок, у но как только начинаем работать с внешним миром, нужно их как-то различать, для этого добавляется ID.
Entity = доменный класс + ID
Поскольку оперативная память всё ещё ограничена и энергозависима, нужно сохранять (persist-ить) эти объекты где-то, например, в реляционной БД.
Далее, есть такой подход как ООП -- объектно-ориентированное программирование. Одной из его идей является инкапсуляция -- это когда поля закрыты для изменений напрямую, но есть методы, которые гарантируют бизнес-целостность объекта.
В нашем примере это могут быть addItem(...), applyPromoCode(...) и т.д., которые изменяют состояние объекта, в том числе и пересчитывая totalPrice, благодаря чему (и юнит-тестам на них, конечно) мы можем быть уверены, что объект всегда консистентен.
Ещё раз, если у нас есть addItem(...) и removeItem(...) с соответсвующими проверками, есть гарантия, что количество товаров положительное, а итоговая цена пересчитана правильно.
Далее, все изменения состояния объекта хорошо бы отобразить обратно в БД. Этим и занимается JPA, что-то вроде
class Service {
@Transactional
void someMethod(...) {
var entity = repo.findById(id);
entity.update(...);
// нет, repo.save(entity) здесь не нужен
}
}
Единственный ли это подход? Конечно же нет.
Ещё можно вызывать разные SQL команды, а можно вообще ничего не перегонять на бекэнд, а всё делать сразу в БД с помощью хранимых процедур. У каждого подхода есть свои преимущества и недостатки.
К преимуществам JPA можно отнести, что бизнес-логика пишется на высокоуровневом языке Java, проверяется unit-тестами, а DML операции будут произведены фреймворком.
Ну, а недостатком считается факт, что в частных случаях с помощью SQL можно добиться большей производительности.
Однако, тут есть диллема. Допустим, мы хотим увеличить зарплату (правильнее, конечно, говорить ставку) сотрудников некоторого отдела на 10%. Какой подход лучше
... т.е. "выгрузить данные из БД, изменить, записать обратно по одному" или bulk update
update emloyees set salary = salary * 1.1 where department_id = :department_id;
...?
Очевидно, что вторая команда быстрее.
Но кто сказал, что метод increaseSalaryByPercent(...) настолько прост? Там могут быть (сейчас или добавятся потом) проверки на граничные значения, правила округления и т.д.
Кроме того, написав sql update мы создали вторую точку изменения salary, теперь придётся всегда об этом помнить и держать их в согласованном состоянии.
Всё рассуждения выше были про изменение состояния объектов, JPA в первую очередь об этом.
Кроме этого обычное занятие приложения это отображать их состояние AKA "читатьданные из базы".
С объектной точки зрения корректный путь здесь -- это загружать объекты из базы и преобразовывать их в DTO.
Однако, очевидно, реляционные отношения и SQL предоставляют больше возможности и производительности. Если необходимо этим можно и нужно пользоваться, плата за это -- как в примере выше с bulk update -- поддержание ДВУХ подходов в синхронизированном состоянии.
P.S. Spring PetClinic -- довольно плохой пример использования JPA. Так делать не надо.
Почитайте, плиз, про ООП и инкапсуляцию, подумайте, как сочетаются с ней билдер и сеттеры. К сожалению, это непопулярный взгляд, многие коллеги не видят никаких проблем в её нарушении, чай не проблема потом баги поправить, ещё и ещё.
А, кстати, я правильно предполагаю, что entityManager.createStoredProcedureQuery вне транзакции не работает? Т.е. границы транзакции всё таки были где-то "выше"?
Но транзакция не закрывалась, потому что наружу выходило checked exception EDeliveryException?
И проблема решилась не столько добавлением @Transactional к createInboxMessage методу — это какбэ принципиально не верно — задавать границы транзакции в слое репозитория, а потому что перестали checked exception бросать
Опять же, с Lazy имеем кучу проблем, таких как n+1
Любая Lazy-загрузка -- это потенциальная N+1 проблема. Но одновременно и очевидный прирост производительности. `fetch = FetchType.EAGER` вы же над коллекциями не ставите, верно?
и работа за пределами транзакций
Я не вполне понимаю, что вы имеете в виду, хотя и догадываюсь. Как вы работаете с lazy-коллекциями "за пределами транзакций"? C lazy-скалярами всё то же самое.
пример для h2
БД значения не имеет, lazy-loading делает хибернейт.
без танцев с бубнами
Byte enhancement -- это валидный инструмент, а не танцы с бубном. Поскольку String -- final class, а не интерфейс, по-другому, увы, никак, хотя ещё ленивую загрузку LOB-a можно сымитировать с наследуемым классом (но там свои компромиссы), если byte enhancement почему-то смущает.
Отдельный комментарий для "зачем нужен это ваш JPA/Hibernate, одни только проблемы, лучше возьмите JOOQ/Spring JDBC/учите чистый SQL".
Hibernate нужен вот для этого, соберусь с силами -- разверну в статью.
Теперь правильные советы.
Основные причины "тормозящего" хибернейта -- это, конечно, не сам хибернейт, а разработчики вокруг него.
Основные проблемы:
Отсутствие индексов!
Если для
@ManyToOne
primary key забыть невозможно, то вот для...
parent_id
в таблицеchildren
-- вполне. Проверьте!Явно установленный EAGER для `@OneToMany(fetch = EAGER)`. То ли для борьбы с
LazyInitializationException
, то ли для "загрузки" данных, то ли ещё для чего-то. Так делать не надо, источник N+1 проблемы. Правильно: маппить энтити в responseDto.Переопределённый `@ManyToOne(fetch = LAZY)`. То ли после прочтения этой статьи, то ли аналогичных. Тоже потенциальный источник N+1 проблемы. Можно менять только полном понимании всех трейдоффов.
Ох, парни, что ж вы такое переводите!
Это была рубрика "вредные советы", ничего из перечисленного в статье делать не надо. В хибернейте разумные дефолты (OSIV не в счёт, это Spring Data JPA, а не сам хибер), что-то tweak-ать надо с полным пониманием.
Кто такой вообще этот Мацей Валковяк и почему его постят?Поехали разбираться.
Чушь!Это не так.Во-первых, Spring Data навязал репозиторный метод
save()
в том числе и для Spring Data JPA (для единообразия), оказав тем самым медвежью услугу. В абсолютном большинстве случаев нужныem.persist()
для новых entities и "ничего" для изменения managed ones.Хорошо бы понимать, для чего
em.merge()
(спойлер -- в нормальных кейсах он не нужен) и какой у него сайд-эффект при вручную установленных ID, вот эта самая лишняя загрузка.Скорость работы селекта определяется не его длиной)
Лишний селект, это, конечно, никогда не хорошо, но в данном конкретном случае -- join по primary key -- существенно замедлить работу не должен.
Вот такого точно не надо, пожалуйста.
И
@Version
тоже добавляйте для Optimistic Lock, а не для каких-то других целей.Смущает лишний select при "сохранении" энтити с предустановленными ID -- просто используйте
em.persist()
!Ну, хорошо что про это всё таки вспомнили, хорошо бы с этого начать, а заодно понимать, как оно до этого работало.
И как управлять-то?
Прежде чем "ускорять" приложение с помощь
getReferenceById()
хорошо бы понимать трейд-офф между ним и обычнымfindById(id).orElseThrow(...)
.Обычный
findById
делает запрос в БД и кидает указанный экспешн, который -- обычная практика -- переоборачивается в 400 или 404.getReferenceById()
ничего этого не делает,id
используется как есть в надежде, что есть foreign key. ПарситьForeignKeyConstraintViolationException
тоже можно (а возможно и нужно в сильно конкуретной среде), но строить на нёмBadRequest/ResourseNotFoundException
несколько сложнее.Почему это не очень хороший код?! Вот как раз правильно сделано!
Откуда тут N+1?! Видимо, автор запутался в своих тестах и получил N+1, как раз когда поставил `@ManyToOne(fetch = LAZY)`
Не нужно.
Во-первых,
@ManyToMany
и такLAZY
по дефолту.А что касается исправления
@ManyToOne
наLAZY
то, опять же, надо понимать трейд-офф.C
EAGER
мы `left join`-ом загружаем данные всегда (как, собственно, автор и указал вначале)Они могут нам понадобиться для бизнес-логики/маппинга в ДТО, а могут и не понадобиться. Если у нас инкапсулированная модель, то в общем случае мы этого заранее не знаем, и знать не можем.
JOIN по первичному ключу, естественно, не бесплатен, но, будем считать, относительно дешёв.
В противном случае, с `@ManyToOne(fetch = LAZY)` на любом запросе коллекции энтити как раз-то и есть риск получать N+1 проблему. Обычно это существенно медленнее, и поэтому дефолтное значение как раз
EAGER
.Короче говоря, дефолтные `@ManyToOne(fetch = EAGER)` и `@OneToMany(fetch = LAZY)` -- это нормально и менять их не надо.
Обычно лучше не писать. Но вообще это философский спор между инкапсуляцией и быстродействием.
СТРОКУ, а не колонку!
А, вижу, добавили. Может, поднимите наверх, где "проблема" обсуждается и спойлер уберите, чтобы сразу в глаза бросалось?
Самое время задуматься, чем руководствовались разработчики Хибернейта, и почему сделано именно так. Разобраться с `JDBC PreparedStatement` и кешем запросов.
По мелочи.
Бизнес-конструкторы -- это хорошо и правильно, пустой конструктор нужен хибернейту, но его лучше сделать
protected
, чтобы запретить вызывать из кода.Я бы предложил для случаев, когда идёт микс из обращений к БД и внешним сервисам (читай -- медленное I/O), и мы переходим на ручное управление транзакциями, не стыдливо комментирировать аннотацию
... а смело вешать
чтобы немного подстраховаться от потенциальных проблем.
Я тоже периодически так делаю, но мне постоянно высказывают, что это неправильно и что на самом деле надо мокать сетевые вызовы с помощью Wiremock.
Предлагаю компромисс.
Если по ресту ходит готовый клиент -- например, предоставленный сторонним сервисом или feign-клиент, сгенерированный по спеке -- то считаю вполне допустимым его мокать с помощью
@MockBean
(один раз в базовом тестовом классе, вестимо, чтобы не плодить контексты).Если же мы используем что-то низкоуровненное -- всякие
restTemplate
,webClient
и прочее, в общем пишемqueryParams
иpathVariables
"руками", то лучше Wiremock. Хотя бы из-за того, что "given" будет написано в другом формате и более наглядно. Иначе получается масло масляное -- вызываемrestTemplate
с аргументами... и проверяем, что вызываем с правильными аргументами? Нехорошо.Ещё лучше, конечно, если сторонний эндпоинт выдаст stub-ы в рамках Contract Testing-а (Spring Cloud Contract), но на практике я такого счастья пока не встречал.
Мы оба даём друг другу поводы, высказывая утверждения, кажущиеся полной глупостью собеседнику, на которые тот с удовольствием посылает в гугл. Так что без язвительности в нашем деле никак, увы.
А я и не знал. Я вообще не понял, о каких снепшотах вы пишете, мне показалось, что вы read committed и repeatable read путаете, поэтому да, я действительно подразумевал, что read committed всегда идёт с ними. И, конечно, в Postgres это так. Что сказать -- спасибо, что просветили.
Да, вы правы, и для меня это натурально открытие после надцати лет разработки. В своё оправдание могу сказать, что, кажется, MSSQL не так популярен в Java мире, как остальные три БД, я с ним -- так получилось -- ни разу не сталкивался.
Я, пожалуй, возьму вашу ссылку и проверю коллег спором на деньги), уверен, многие ошибутся, а что -- в формулировке не будет сказано, на какой БД проверяем.
Единственно, мне сходу сложно представить, как read committed без snapshots в MSSQL можно вообще использовать, если там даже самый обычный `select ... from table` может неконсистентые данные возвращать?
Ок, с транзакциями разобрались, спасибо ещё раз, возвращаемся к EBean.
Нет, я пишу прямо противоположное, мне важно, чтобы даже одна сущность была прочитана в консистентном состоянии.
Поэтому меня и смутил подход EBean, возникло ощущение, что с транзакциями и множественными запросами обращаются достаточно свободно, дочерние энтити можно загрузить частично, где-то вообще может быть null... т.е. где целостность-то?
Т.е. с JPA я могу быть уверен, что
.. метод
doSomething()
произведёт перевод сущности из одного консистетного состояния в другое.А вот с EBean это неочевидно, хотя бы из-за https://ebean.io/docs/query/filterMany и вообще
... т.е., если я правильно понял, за целостностью сущности предлагается следить самостоятельно, зато без декартова произведения?
Как по мне -- сомнительное преимущество.
Приписывать собеседнику утверждение, а потом его опровергать -- это не очень честный приём.
Это как?!
Что такое "частично видимый апдейт другой транзакции"?! Это как?
Только не надо ссылок, пожалуйста, напишите своими словами, можно в той нотации, на которую ссылаетесь.
Это как?!
Нет, postgres не предоставляет.
Ок, ещё раз напишу. Раз cartesian product не случится, значит запросов больше одного.
А что тогда, например, с Oracle предлагается делать? Включать SERIALIZABLE? Смириться с Read Skew? Не использовать Ebean? Открывать тикет?
В общем, вкупе с рассуждениями про Read Skew я делаю предположение, что ни автор EBean, ни вы как его пользователь, не видите смысла упарываться в консистентное чтение. Ну, т.е. частичная загрузка дочерних сущностей -- вас абсолютно не смущает, поскольку энтити (в вашем мире) -- это не объект с бизнес-инвариантами, а так... какая-то структура для чтения данных из базы. Точка зрения понятна, переубеждать смысла не вижу, спасибо за дискуссию.
Давайте разберёмся с определениями.
Entity может быть размазана по нескольким таблицам.
Если её читать несколькими запросами (и в это время другой процесс конкурентно её обновляет), получается Read Skew -- Entity может быть неконсистентна, бизнес-инварианты могут быть нарушены.
Чтобы избежать этого есть два подхода (и у каждого, конечно, своя "цена")
устанавливать уровень изоляции транзакции в REPEATABLE_READ или выше
на уровне READ_COMMITTED читать одним запросом -- `select ... from ... join ... join` -- это cartesian product
Если в Ebean заявляется, что
... то, если я верно понимаю, второй опции у разработчика вообще нет?
Что тогда, например, с 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 написано
Это что вообще имеется в виду?
И вы пишете
Можете какой-то практический пример привести, пожалуйста, для чего это и почему это хорошо?
И, главное, как в таком случае отличить реальный
null
, т.е у поля нет значения, и когда оно не загружено?Немного странно начинать обсуждение EBean VS JPA в статье про Jakarta Data, ну да ладно.
Сначала хотел написать длинный комментарий по каждому пункту (может и напишу), а пока зайду сразу с козырей.
Глянул по диагонали документацию EBean, не увидел, подскажите, пожалуйста: его автор и пользователи в курсе про Read/Write Skew?
(я по фене ботать не умею, так, чисто для сохранения стилистики)
Невер, говорит, не в падлу, говорит... А если потом пойдут предъявы за Read Skew, кто отвечать за
базарнеконсистентность данных будет?!Это, естественно, не отменяет, такого же вопроса к абсолютно любому SQL-фреймворку, мой поинт в том, что невозможно с порога заявить, что такой проблемы больше нет.
И то же самое ещё раз
Т.е. OSIV (Open Session In View) тоже, выходит, "поощеряется" как в Spring Data JPA? А с Read Skew-то что?! Судя по ролику, который я глянул, транзакции былы разные.
LazyInitializationException
же явно говорит, что "Алё, гараж! Вы что-то не то делаете, транзакция закрыта ваще-то уже!"Всё дальше мы от ООП.Без бутылки не разобраться.
Сначала долго ведут в сторону 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
вроде бы есть, а вот (специально напишу без инкапсуляции, чтобы было наглядно)... работает?
Ок, короткая шпаргалка, что такое JPA, зачем и как с ним работать.
JPA -- это Java Persistence API, фреймворк, который позволяет мапить Java объекты на реляционную базу данных.
Что это значит на практике? Допустим, у нас есть такой класс (и объекты -- экземпляры этого класса)
... в
Item
иPromoCode
, в свою очередь, наверчено ещё что-то и т.д.Пока мы работаем с ними в оперативной памяти -- всё ок, у но как только начинаем работать с внешним миром, нужно их как-то различать, для этого добавляется ID.
Поскольку оперативная память всё ещё ограничена и энергозависима, нужно сохранять (persist-ить) эти объекты где-то, например, в реляционной БД.
Далее, есть такой подход как ООП -- объектно-ориентированное программирование. Одной из его идей является инкапсуляция -- это когда поля закрыты для изменений напрямую, но есть методы, которые гарантируют бизнес-целостность объекта.
В нашем примере это могут быть
addItem(...)
,applyPromoCode(...)
и т.д., которые изменяют состояние объекта, в том числе и пересчитываяtotalPrice
, благодаря чему (и юнит-тестам на них, конечно) мы можем быть уверены, что объект всегда консистентен.Ещё раз, если у нас есть
addItem(...)
иremoveItem(...)
с соответсвующими проверками, есть гарантия, что количество товаров положительное, а итоговая цена пересчитана правильно.Далее, все изменения состояния объекта хорошо бы отобразить обратно в БД. Этим и занимается JPA, что-то вроде
Единственный ли это подход? Конечно же нет.
Ещё можно вызывать разные SQL команды, а можно вообще ничего не перегонять на бекэнд, а всё делать сразу в БД с помощью хранимых процедур. У каждого подхода есть свои преимущества и недостатки.
К преимуществам JPA можно отнести, что бизнес-логика пишется на высокоуровневом языке Java, проверяется unit-тестами, а DML операции будут произведены фреймворком.
Ну, а недостатком считается факт, что в частных случаях с помощью SQL можно добиться большей производительности.
Однако, тут есть диллема. Допустим, мы хотим увеличить зарплату (правильнее, конечно, говорить ставку) сотрудников некоторого отдела на 10%. Какой подход лучше
... т.е. "выгрузить данные из БД, изменить, записать обратно по одному" или bulk update
...?
Очевидно, что вторая команда быстрее.
Но кто сказал, что метод
increaseSalaryByPercent(...)
настолько прост? Там могут быть (сейчас или добавятся потом) проверки на граничные значения, правила округления и т.д.Кроме того, написав sql update мы создали вторую точку изменения
salary
, теперь придётся всегда об этом помнить и держать их в согласованном состоянии.Всё рассуждения выше были про изменение состояния объектов, JPA в первую очередь об этом.
Кроме этого обычное занятие приложения это отображать их состояние AKA "читать данные из базы".
С объектной точки зрения корректный путь здесь -- это загружать объекты из базы и преобразовывать их в DTO.
Однако, очевидно, реляционные отношения и SQL предоставляют больше возможности и производительности. Если необходимо этим можно и нужно пользоваться, плата за это -- как в примере выше с bulk update -- поддержание ДВУХ подходов в синхронизированном состоянии.
P.S. Spring PetClinic -- довольно плохой пример использования JPA. Так делать не надо.
Почитайте, плиз, про ООП и инкапсуляцию, подумайте, как сочетаются с ней билдер и сеттеры. К сожалению, это непопулярный взгляд, многие коллеги не видят никаких проблем в её нарушении, чай не проблема потом баги поправить, ещё и ещё.
Чуток "причесал" вашу реализацию.
Но вообще лично я придерживаюсь подхода, который мне кажется более строгим:
entities не должны покидать границы транзакций
тогда и
equals()/hashCode()
переопредлять не нужноНу и посколько статья про Lombok:
как вы указываете,
toString()
можно только с явно указанными полями@Builder
считаю непременимым для Entity вообще, нужен явный конструктор безid
, чтобы гарантировать бизнес-целостностьк
@NoArgsConstructor
лучше добавитьaccess=AccessLevel.PROTECTED
, JPA достаточно, а разработчикам он не нужен по причине выше@AllArgsConstructor
скорее не нужен, потому как он включаетid
, а он обычно генерируетсяsequence
@Setter
тоже не нужен всё по той же причине необходимости бизнес-целостности@Data
отпадает, уже объяснено вышеИтого от Lombok-a остаются только `@ToString(onlyExplicitlyIncluded = true)` c явно указанными полями.
И
@Getter
. Геттеры нужны для а) конвертации entity в DTO, б) для тестов. В бизнес коде их быть не должно.Vlad Mihalchea предлагает способ, как писать
select new PostWithAuthorFlatDto
без указания полного имени класса.Крайне интересно. Поделитесь техническими подробностями, пожалуйста.
Можете показать код, воспроизводящий баг? Который в баг-репорт прикладывали?
И вот эти комментарии
и
... они в каких конкретно проектах/файлах/строчках?
Предполагаю, что вы ещё в контексте проблемы, раз
Вы можете подтвердить баг тестовым проектом, демонстрирующем проблему? Отправили баг-репорт в Spring?
Атрибут
transactionManager
в `@Transactional` конечно же использовали?А, кстати, я правильно предполагаю, что
entityManager.createStoredProcedureQuery
вне транзакции не работает? Т.е. границы транзакции всё таки были где-то "выше"?Но транзакция не закрывалась, потому что наружу выходило checked exception
EDeliveryException
?И проблема решилась не столько добавлением @Transactional к
createInboxMessage
методу — это какбэ принципиально не верно — задавать границы транзакции в слое репозитория, а потому что перестали checked exception бросать...?
Глушить эксепшены тоже, кстати, нехорошая практика.
В общем, вопросы есть к изложенному в статье.
Вопрос не по теме (по теме напишу отдельный комментарий) -- вы как новый счёт клиенту добавляете?
или
Это не так в случае lazy-скаляров. Когда они у вас заработают, посмотрите, как ведёт себя обновление.
Любая Lazy-загрузка -- это потенциальная N+1 проблема. Но одновременно и очевидный прирост производительности. `fetch = FetchType.EAGER` вы же над коллекциями не ставите, верно?
Я не вполне понимаю, что вы имеете в виду, хотя и догадываюсь. Как вы работаете с lazy-коллекциями "за пределами транзакций"? C lazy-скалярами всё то же самое.
БД значения не имеет, lazy-loading делает хибернейт.
Byte enhancement -- это валидный инструмент, а не танцы с бубном. Поскольку
String
-- final class, а не интерфейс, по-другому, увы, никак, хотя ещё ленивую загрузку LOB-a можно сымитировать с наследуемым классом (но там свои компромиссы), если byte enhancement почему-то смущает.Это пока нет) А внимательно перечитаете мой самый первый комментарий, погуглите -- уверен, заработает. Хинт: IDEA компилирует сама, byte enhancement не делает, поэтому запускайте тест напрямую с maven/gradle, вот "баг" на эту тему https://youtrack.jetbrains.com/issue/IDEA-159903/Hibernate-bytecode-instrumentation-code-is-being-overridden-by-IDEA