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

Ждем всех, присоединяйтесь!

Комментарии (0)