Автор оригинала: Ali Dehghani.
1. Обзор
Сеанс на запрос-это транзакционный шаблон, связывающий жизненные циклы сеанса сохранения и запроса вместе. Неудивительно, что Spring поставляется с собственной реализацией этого шаблона под названием OpenSessionInViewInterceptor , чтобы облегчить работу с ленивыми ассоциациями и, следовательно, повысить производительность разработчиков.
В этом уроке, во-первых, мы узнаем, как перехватчик работает внутри, а затем мы увидим, как этот спорный шаблон может быть обоюдоострым мечом для наших приложений!
2. Представление открытой сессии с учетом
Чтобы лучше понять роль открытого сеанса в представлении (OSIV), предположим, что у нас есть входящий запрос:
- Spring открывает новый сеанс гибернации в начале запроса. Эти Сеансы не обязательно подключены к базе данных. Каждый раз, когда приложению требуется сеанс
- , он будет повторно использовать уже существующий. В конце запроса тот же перехватчик закрывает сеанс
- .
На первый взгляд, возможно, имеет смысл включить эту функцию. В конце концов, фреймворк обрабатывает создание и завершение сеанса, поэтому разработчики не заботятся об этих, казалось бы, низкоуровневых деталях. Это, в свою очередь, повышает производительность разработчиков.
Однако иногда OSIV может вызвать незначительные проблемы с производительностью в производстве . Обычно такие проблемы очень трудно диагностировать.
2.1. Пружинный Ботинок
По умолчанию OSIV активен в приложениях Spring Boot . Несмотря на это, начиная с Spring Boot 2.0, он предупреждает нас о том, что он включен при запуске приложения, если мы не настроили его явно:
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
В любом случае, мы можем отключить OSIV, используя свойство spring.jpa.open-in-view configuration:
spring.jpa.open-in-view=false
2.2. Шаблон или Анти-Шаблон?
Всегда были смешанные реакции на OSIV. Главным аргументом лагеря pros IV является производительность разработчиков, особенно при работе с ленивыми ассоциациями .
С другой стороны, проблемы с производительностью базы данных являются основным аргументом кампании против OSIV. Позже мы подробно рассмотрим оба аргумента.
3. Ленивый герой Инициализации
Поскольку OSIV связывает жизненный цикл Сеанса с каждым запросом, Hibernate может разрешать ленивые ассоциации даже после возврата из явной @Транзакционной службы .
Чтобы лучше понять это, предположим, что мы моделируем наших пользователей и их разрешения безопасности:
@Entity @Table(name = "users") public class User { @Id @GeneratedValue private Long id; private String username; @ElementCollection private Setpermissions; // getters and setters }
Подобно другим отношениям “один ко многим” и “многие ко многим”, свойство permissions является ленивой коллекцией.
Затем, в нашей реализации уровня обслуживания, давайте явно обозначим нашу транзакционную границу с помощью @Транзакционный :
@Service public class SimpleUserService implements UserService { private final UserRepository userRepository; public SimpleUserService(UserRepository userRepository) { this.userRepository = userRepository; } @Override @Transactional(readOnly = true) public OptionalfindOne(String username) { return userRepository.findByUsername(username); } }
3.1. Ожидание
Вот что мы ожидаем, когда наш код вызовет метод find One :
- Сначала прокси-сервер Spring перехватывает вызов и получает текущую транзакцию или создает ее, если таковой не существует.
- Затем он делегирует вызов метода нашей реализации.
- Наконец, прокси-сервер фиксирует транзакцию и, следовательно, закрывает базовую Сессия В конце концов, нам нужно только это Сессия в нашем сервисном слое.
В реализации метода findOne мы не инициализировали коллекцию разрешений . Следовательно, мы не сможем использовать разрешения после возврата метода. Если мы сделаем итерацию по этому свойству , мы должны получить исключение LazyInitializationException.
3.2. Добро пожаловать в Реальный мир
Давайте напишем простой контроллер REST, чтобы узнать, можем ли мы использовать свойство permissions :
@RestController @RequestMapping("/users") public class UserController { private final UserService userService; public UserController(UserService userService) { this.userService = userService; } @GetMapping("/{username}") public ResponseEntity> findOne(@PathVariable String username) { return userService .findOne(username) .map(DetailedUserDto::fromEntity) .map(ResponseEntity::ok) .orElse(ResponseEntity.notFound().build()); } }
Здесь мы перебираем разрешения во время преобразования сущности в DTO. Поскольку мы ожидаем, что преобразование завершится ошибкой с LazyInitializationException, следующий тест не должен пройти:
@SpringBootTest @AutoConfigureMockMvc @ActiveProfiles("test") class UserControllerIntegrationTest { @Autowired private UserRepository userRepository; @Autowired private MockMvc mockMvc; @BeforeEach void setUp() { User user = new User(); user.setUsername("root"); user.setPermissions(new HashSet<>(Arrays.asList("PERM_READ", "PERM_WRITE"))); userRepository.save(user); } @Test void givenTheUserExists_WhenOsivIsEnabled_ThenLazyInitWorksEverywhere() throws Exception { mockMvc.perform(get("/users/root")) .andExpect(status().isOk()) .andExpect(jsonPath("$.username").value("root")) .andExpect(jsonPath("$.permissions", containsInAnyOrder("PERM_READ", "PERM_WRITE"))); } }
Однако этот тест не вызывает никаких исключений, и он проходит.
Потому что OSIV создает Сессия в начале запроса транзакционный прокси-сервер использует текущий доступный Сессия вместо того, чтобы создавать совершенно новый .
Таким образом, несмотря на то, что мы могли бы ожидать, мы на самом деле можем использовать свойство permissions даже за пределами явного @Transactional . Более того, такого рода ленивые ассоциации могут быть извлечены в любом месте текущей области запроса.
3.3. О Производительности разработчиков
Если бы OSIV не был включен, нам пришлось бы вручную инициализировать все необходимые ленивые ассоциации в транзакционном контексте . Самый элементарный (и обычно неправильный) способ-использовать метод Hibernate.initialize() :
@Override @Transactional(readOnly = true) public OptionalfindOne(String username) { Optional user = userRepository.findByUsername(username); user.ifPresent(u -> Hibernate.initialize(u.getPermissions())); return user; }
К настоящему времени влияние OSIV на производительность разработчиков очевидно. Однако это не всегда связано с производительностью разработчиков.
4. Производительность.
Предположим, нам нужно расширить нашу простую пользовательскую службу до вызова другой удаленной службы после извлечения пользователя из базы данных :
@Override public OptionalfindOne(String username) { Optional user = userRepository.findByUsername(username); if (user.isPresent()) { // remote call } return user; }
Здесь мы удаляем аннотацию @Transactional , так как мы явно не хотим сохранять подключенный Сеанс во время ожидания удаленной службы.
4.1. Избегайте смешанной iOS
Давайте уточним, что произойдет, если мы не удалим аннотацию @Transactional . Предположим, что новая удаленная служба реагирует немного медленнее, чем обычно:
- Сначала прокси-сервер Spring получает текущий Сеанс или создает новый. В любом случае, этот Сеанс еще не подключен. То есть он не использует никакого соединения из пула.
- Как только мы выполняем запрос на поиск пользователя, Сеанс подключается и заимствует Соединение из пула.
- Если весь метод является транзакционным, то метод продолжает вызывать медленную удаленную службу, сохраняя при этом заимствованное Соединение .
Представьте, что в течение этого периода мы получаем серию вызовов метода findOne . Затем, через некоторое время, все Соединения могут ждать ответа от этого вызова API. Поэтому у нас скоро могут закончиться соединения с базой данных.
Смешивание базы данных с другими типами iOS в транзакционном контексте-это плохой запах, и мы должны избегать его любой ценой.
В любом случае, поскольку мы удалили аннотацию @Transactional из нашего сервиса, мы ожидаем, что будем в безопасности .
4.2. Исчерпание пула соединений
Когда OSIV активен , всегда есть Сеанс в текущей области запроса , даже если мы удалим @Transactional . Хотя этот Сеанс изначально не подключен, после нашего первого ввода-вывода базы данных он подключается и остается таковым до конца запроса.
Таким образом, наша невинная на вид и недавно оптимизированная реализация сервиса-это рецепт катастрофы в присутствии OSIV:
@Override public OptionalfindOne(String username) { Optional user = userRepository.findByUsername(username); if (user.isPresent()) { // remote call } return user; }
Вот что происходит, когда OSIV включен:
- В начале запроса соответствующий фильтр создает новый Сеанс .
- Когда мы вызываем метод findByUsername , этот Сеанс заимствует Соединение из пула.
- Сеанс остается подключенным до конца запроса.
Несмотря на то, что мы ожидаем, что наш сервисный код не исчерпает пул соединений, простое присутствие OSIV потенциально может сделать все приложение невосприимчивым.
Что еще хуже, первопричина проблемы (медленная удаленная служба) и симптом (пул подключений к базе данных) не связаны . Из-за этой небольшой корреляции такие проблемы с производительностью трудно диагностировать в производственных средах.
4.3. Ненужные запросы
К сожалению, исчерпание пула соединений-не единственная проблема производительности, связанная с OSIV.
Поскольку Сессия открыта в течение всего жизненного цикла запроса, некоторые переходы по свойствам могут вызвать еще несколько нежелательных запросов вне контекста транзакции . Это даже может закончиться проблемой выбора n+1 , и худшая новость заключается в том, что мы можем не заметить этого до производства.
Добавляя оскорбление к травме, Сессия выполняет все эти дополнительные запросы в режиме автоматической фиксации . В режиме автоматической фиксации каждая инструкция SQL обрабатывается как транзакция и автоматически фиксируется сразу после ее выполнения. Это, в свою очередь, оказывает большое давление на базу данных.
5. Выбирайте С Умом
Является ли OSIV шаблоном или анти-шаблоном, не имеет значения. Самое главное здесь-это реальность, в которой мы живем.
Если мы разрабатываем простой сервис CRUD, возможно , имеет смысл использовать OSIV , поскольку мы никогда не столкнемся с этими проблемами производительности.
С другой стороны, если мы обнаруживаем, что вызываем много удаленных служб или так много происходит за пределами наших транзакционных контекстов, настоятельно рекомендуется полностью отключить OSIV.
Если вы сомневаетесь, начните без OSIV, так как мы можем легко включить его позже. С другой стороны, отключение уже включенного OSIV может быть громоздким, так как нам может потребоваться обработать много LazyInitializationExceptions.
Суть в том, что мы должны знать о компромиссах при использовании или игнорировании OSIV.
6. Альтернативы
Если мы отключим OS IV, то мы должны каким-то образом предотвратить потенциальное Lazyinitializationexception при работе с ленивыми ассоциациями. Среди нескольких подходов к борьбе с ленивыми ассоциациями мы перечислим здесь два из них.
6.1. Графики сущностей
При определении методов запроса в Spring Data JPA мы можем аннотировать метод запроса с помощью @EntityGraph , чтобы охотно получить некоторую часть сущности :
public interface UserRepository extends JpaRepository{ @EntityGraph(attributePaths = "permissions") Optional findByUsername(String username); }
Здесь мы определяем специальный граф сущностей для быстрой загрузки атрибута permissions , хотя по умолчанию это ленивая коллекция.
Если нам нужно вернуть несколько проекций из одного и того же запроса, мы должны определить несколько запросов с различными конфигурациями графа сущностей:
public interface UserRepository extends JpaRepository{ @EntityGraph(attributePaths = "permissions") Optional findDetailedByUsername(String username); Optional findSummaryByUsername(String username); }
6.2. Предостережения При Использовании Hibernate.initialize()
Можно возразить, что вместо использования графов сущностей мы можем использовать пресловутый Hibernate.initialize() для извлечения ленивых ассоциаций везде, где нам это нужно:
@Override @Transactional(readOnly = true) public OptionalfindOne(String username) { Optional user = userRepository.findByUsername(username); user.ifPresent(u -> Hibernate.initialize(u.getPermissions())); return user; }
Они могут быть умны в этом, а также предложить вызвать метод getPermissions () , чтобы запустить процесс выборки:
Optionaluser = userRepository.findByUsername(username); user.ifPresent(u -> { Set permissions = u.getPermissions(); System.out.println("Permissions loaded: " + permissions.size()); });
Оба подхода не рекомендуются , так как они требуют (по крайней мере) одного дополнительного запроса , в дополнение к исходному, для извлечения ленивой ассоциации. То есть Hibernate генерирует следующие запросы для получения пользователей и их разрешений:
> select u.id, u.username from users u where u.username=? > select p.user_id, p.permissions from user_permissions p where p.user_id=?
Хотя большинство баз данных довольно хорошо справляются с выполнением второго запроса, мы должны избегать этого дополнительного сетевого обхода.
С другой стороны, если мы используем графики сущностей или даже объединения выборки , Hibernate будет извлекать все необходимые данные всего одним запросом:
> select u.id, u.username, p.user_id, p.permissions from users u left outer join user_permissions p on u.id=p.user_id where u.username=?
7. Заключение
В этой статье мы обратили наше внимание на довольно спорную особенность Spring и нескольких других корпоративных фреймворков: Открытая сессия в поле зрения. Во-первых, мы познакомились с этой моделью как концептуально, так и с точки зрения реализации. Затем мы проанализировали его с точки зрения производительности и производительности.
Как обычно, пример кода доступен на GitHub .