На прошлой неделе в блоге сообщества Spring АйО вышла статья-перевод про интересный кейс падения производительности при переходе на Hibernate 6.5. Оказалось, что выражения вида publisherId in :ids
при пустом ids
приводит к серьезной деградации производительности. Баг вскоре был пофикшен, однако, не дает покоя вопрос, почему так произошло? Ниже приводим историю появления и незамедлительного решения этой проблемы, от лица Гэвина Кинга, создателя Hibernate.
Сам тред
На прошлой неделе возникла интересная проблема с JPQL выражениями вида:
null in ()
Вы можете спросить, почему кому-либо вообще нужно писать что-то подобное? Необходимость в таком коде может появиться из параметризованного выражения вида
book.publisher.name in (:names)
в случае, если publisher опционален.
В чем вопрос: к чему должен приводиться запрос null in ()
, к false или к null/unknown? Ответ не очевиден, и зависит от того, какой смысл мы вкладываем в null:
неизвестное значение, или
пропущенное значение.
Освещая эту проблему, обычно забывают про фундаментальное различие неизвестного и пропущенного значений.
Рассмотрим наш пример c book.publisher:
если он null, потому что мы не знаем, кто издатель, то выражение должно приводиться к false
но если у книги нет издателя (пока нет), то выражение должно приводиться к null/unknown
Эти варианты принципиально разные!
Исторически, Hibernate берет первый вариант, рассматривая null in ()
как false
, транслируя
book.publisher.name in (:names)
в 1=0
, то есть false
, когда names - пустой список.
Работая с JPA 3.2, я заметил, что это, строго говоря, расходится с моим самым тщательным прочтением всех версий спецификации JPA вплоть до 3.1, в которых утверждалось, что такие выражения должны приводиться к null/unknown, если левая часть равна null.
Более того, я действительно считаю, что это самый правильный вариант! В SQL null чаще обозначает именно отсутствующее/несуществующее значение, а не неизвестное. Хотя, справедливости ради, второй вариант тоже допустим.
Итак, поскольку Hibernate 6 должен реализовывать JPA 3.1, я изменил обработку foo in ()
, чтобы она приводилась к null
, когда foo
равно null
. Это изменение было внесено в Hibernate 6.5 как исправление ошибки для обеспечения совместимости с JPA.
В репозитории JPA на GitHub состоялось длительное обсуждение по этому поводу, и вот комментарий, который объясняет мой конечный вариант трансляции JPQL в SQL:
Я нашел вариант трансляции
x in ()
, который работает на всех основных базах данных:(1 = case x is not null then 0 end)
.
Теперь мы фактически "подкорректировали/уточнили" формулировку в JPA 3.2 таким образом, что она допускает противоположное толкование, то есть при очень тщательном прочтении позволяет то, что исторически делал Hibernate, а именно трактовать 'null in ()' как false.
В четверг мы получили сообщение о проблеме от Джесси Джексона, в котором он сообщил, что изменение приводит к регрессии производительности для некоторых пользователей на некоторых базах данных:
https://hibernate.atlassian.net/browse/HHH-18235
После очень долгого обсуждения мы решили откатить это изменение. Что и было сделано в пятницу.
Это означает, что версии H6.6 и H6.5.3 вернутся к техническому несоответствию для JPA <= 3.1, но зато регрессия производительности будет устранена.
Для Hibernate 7, который нацелен на JPA 3.2, вопрос соответствия спецификации был решен благодаря изменениям, внесенным в саму спецификацию.
Эта ситуация была несколько досадной и довольно неловкой лично для меня.
Но с другой стороны, я хотел показать вам, что мы отнеслись к этому серьезно и исправили проблему чрезвычайно быстро. Вся очень долгая беседа и исправление ошибки произошли в течение 24 часов с момента поступления сообщения о проблеме.
Присоединяйтесь к русскоязычному сообществу разработчиков на Spring Boot в телеграм - Spring АйО, чтобы быть в курсе последних новостей из мира разработки на Spring Boot и всего, что с ним связано.
Ждем всех, присоединяйтесь!