[Из песочницы] Open Session In View в Spring Boot: Скрытая угроза

habr.png

Все здесь правы, каждый по-своему, и, следовательно, все здесь не правы.
«Сказка о Тройке» (А. и Б. Стругацкие)

Если вы используете Spring Data JPA, то после обновления на Spring Boot 2 при старте приложения можете заметить в логе новое предупреждение:


spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning.

В этой статье попытаюсь объяснить, что это значит, кто виноват и что делать.

Для поднятия полноценного приложения на Spring Boot требуется всего лишь одна аннотация @SpringBootApplication. Для того, чтобы это было возможным, фреймворк использует большое количество автоконфигураций и настроек по умолчанию. Более того, для работы «из коробки» разработчикам Spring Boot пришлось выбрать некие концепции разработки приложений из нескольких альтернативных вариантов для каждой, чтобы пользователю не нужно было выбирать их явно. С одной стороны это хорошо для быстрого старта и легкой разработки, но с другой — через некоторое время может оказаться, что некая используемая по умолчанию концепция/парадигма/настройка не подходит для проекта, а для отказа от нее придется многое переделать. Одной из таких концепций является режим Open Session In View (OSIV), включенный в Spring Boot по умолчанию.

В этом режиме сессия Hibernate держится открытой все время обработки HTTP-запроса, включая этап создания представления (View) — JSON-ресурса или HTML-страницы. Это делает возможным ленивую загрузку данных в слое представления после коммита транзакции в слое бизнес-логики. Например, мы запрашиваем из БД сущность Article. Статья должна быть отображена вместе с комментариями. OSIV позволяет при рендеринге HTML просто вызвать метод сущности getComments(), и комментарии будут загружены отдельным запросом. При отключенном режиме OSIV мы получим LazyInitializationException, так как сессия уже закрыта, и сущность Article больше не управляется Hibernate. Большинство разработчиков на Hibernate сталкивались с LazyInitializationException; OSIV позволяет его избежать, подгружая данные по мере необходимости на любом этапе обработки HTTP-запроса.

OSIV в Spring Boot реализован с помощью класса-перехватчика web-запросов OpenEntityManagerInViewInterceptor. В отличие от чистого Spring, здесь он включен по умолчанию.

OSIV считается антипаттерном. Лучше всего его вредные стороны в своей статье объяснил Vlad Mihalcea, один из разработчиков Hibernate: The Open Session In View Anti-Pattern. Основные тезисы:


  • Запросы к БД без транзакции работают в режиме авто-коммита, сильно ее нагружая.
  • Отсутствует разделение ответственности, любой слой приложения может генерировать SQL-запросы, что затрудняет тестирование.
  • Возникает проблема n+1, когда каждая связанная с сущностью коллекция будет загружена отдельным запросом.
  • Долгие соединения с БД опять же увеличивают нагрузку на нее и уменьшают пропускную способность.

Это довольно неприятные проблемы режима OSIV и, казалось бы, сильные аргументы за то, чтобы его не использовать. Однако в запросе на его отключение по умолчанию разработчики Spring Boot также привели веские доводы, почему OSIV должен быть включен для новых проектов:


  • Обратная совместимость. Существующие приложения при апгрейде на Spring Boot 2 могли бы столкнуться с ошибками и багами из-за отключенного OSIV. Для избежания этого нужно всего лишь установить единственное значение spring.jpa.open-in-view, но и это затруднит переход на новую версию.
  • Для Spring Boot важна простота использования для новичков и быстрый старт. Если отключить OSIV, новичкам может быть непонятно, почему не работает такая интуитивно ожидаемая вещь, как получение коллекции связанных элементов при обращении к методу сущности. Вместо этого пользователь получит LazyInitializationException, это замедлит его путь к работающему приложению.
  • OSIV позволяет увеличить простоту кода, удобство и скорость разработки.
  • Трудно найти простые примеры, как должно быть построено приложение без OSIV.
  • Без OSIV слой бизнес-логики должен знать, каким образом данные будут представлены в UI, то есть какие DTO ему понадобятся или какие связанные данные должны быть загружены вместе с корневой сущностью. Это опять же отсутствие разделения ответственности.

Итак, мы можем выделить два взгляда на эту проблему. С точки зрения архитектора баз данных (DBA) Open Session In View безусловно является неприемлемым, так как взаимодействие приложения с БД организовано неоптимальным образом и порождает повышенную нагрузку. Но мы часто используем менее оптимальные решения ради скорости, удобства разработки и простоты изучения инструментов — пишем на управляемых языках, используем текстовые форматы данных для сетевого взаимодействия и так далее. С позиции разработчика фреймворка для упрощения разработки и быстрого старта новичков OSIV позволяет уменьшить когнитивную нагрузку, количество понятий, необходимых для начала разработки приложения. Если разработчик выбрал JPA, он уже согласился на некоторое снижение производительности в обмен на удобство разработки. JPA помогает «подружить» объектную и реляционную модели данных. При работе в объектном стиле для получения связанных элементов мы просто обращаемся к методу сущности (пусть даже и в слое представления), это просто, логично и интуитивно ожидаемо, хоть эта простота и обманчива.

Для работы в режиме Open Session In View существует много примеров и туториалов, архитектура в целом понятна: слой сервисов запрашивает JPA-сущность, слой представления сериализует ее в JSON напрямую, через промежуточный DTO или использует данные из нее для рендеринга HTML-страницы.

Менее понятно, как работать без OSIV. На это жалуется один из разработчиков Spring в упомянутом запросе, дескать много криков про антипаттерн, но нет простых примеров, как без него жить. В этом варианте сущности могут использоваться только для записи, а для чтения — многочисленные DTO, отдельные для каждого набора данных в UI, которые маппятся прямо из БД. Или кастомные SQL-запросы с описанием join-ов необходимых связанных коллекций для конкретного web-запроса. То есть больше бойлерплейта и описание нужд UI в слое бизнес логики. С отключенным режимом OSIV абстракция JPA начинает протекать, приложение описывает больше технических деталей взаимодействия с БД.

Таким образом, разработка с OSIV проще. Но проблема в том, что если в будущем вы захотите от него отказаться, придется очень многое переделывать, концепцию работы с БД в проекте установкой одного свойства не изменить. Возможно, придется переделать всю архитектуру приложения. Однако отказ от OSIV может быть преждевременной оптимизацией, которая замедлит скорость разработки, что может быть критичным для стартапа, например. Вы можете использовать OSIV и оптимизировать запросы к БД только в самых медленных местах. Например, запросы коллекций сущностей больше всего страдают от проблемы n+1, когда каждая сущность тянет за собой несколько запросов связанных коллекций.

Итак, если хотите сделать правильно, нужно разрабатывать без OSIV. Но если важна скорость разработки и простота кода, можно использовать этот режим, смирившись с некоторой потерей производительности.

Главное, чтобы эта проблема производительности не превратилась через несколько лет в огромный технический долг. Вдвойне опасна ситуация, когда разработчики и не подозревают, что этот долг у них копится, потому что Spring Boot молча выбрал концепцию Open Session In View за них. Поэтому отлично, что в результате упомянутого запроса было принято решение выводить в лог предупреждение об используемом режиме, приведенное мной в начале статьи.

Надеюсь, что предупреждение и эта моя статья о нем помогут разработчикам принимать более информированное решение — использовать ли концепцию Open Session In View в приложении на Spring Boot. Я привел и обсудил основные аргументы за и против, также советую почитать исходное обсуждение. Эта проблема показывает, что большое количество автоконфигураций и умолчаний в Spring/Spring Boot может быть опасным для невнимательных разработчиков.

Используете ли вы OSIV в приложениях на Spring Boot? Если нет, то как организована архитектура взаимодействия слоя представления с БД? Какие приемы и/или библиотеки используются для этого?

© Habrahabr.ru