
На прошлой неделе в блоге сообщества 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 и всего, что с ним связано.
Ждем всех, присоединяйтесь!
 
          