Обновление Hibernate 4 -> 5 приводит к LazyInitializationException

Я обновил проект с Spring 4.3.9.RELEASE + Hibernate 4.3.11.Final до Spring Boot 2.1.4.RELEASE и Hibernate 5.3.9.Final. Запросы все еще работают нормально, но я получаю LazyInitializationException с некоторыми @OneToMany членами класса.

Сначала я извлекаю объект, который имеет ссылку на список @OneToMany, из службы @Transaction. Коллекция возвращается в контроллер, а оттуда возвращается в Spring для сериализации в json. У контроллера есть @RestController, так что он знает, что делать.

В Spring 4.3.9.RELEASE + Hibernate 4.3.11.Final все было хорошо, хотя OpenEntityManagerInView не был включен в конфигурации и коллекция не загружалась в режиме EAGER. Но в Spring Boot 2.1.4.RELEASE и Hibernate 5.3.9.Final то же самое больше не работает. Я пытался включить OEMIV, установив spring.jpa.open-in-view=true, но даже это не работает или где-то переопределяется.

Если я включу режим загрузки EAGER для этой коллекции, все будет работать нормально.

пом.xml

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

@Entity
@JsonSerialize(using = TemplateSerializer.class)
public class Template implements Serializable {

    @Id
    @GeneratedValue
    private Long id;

    private String name;

    @ManyToOne
    private ObjFormat objFormat;

    @OneToOne
    @JoinColumn(name = "event_id")
    @OnDelete(action = OnDeleteAction.CASCADE)
    private Event event;

    @OneToMany
    @JoinColumn(name = "category_id")
    private List<Category> linkToCategories;

Проблема вызвана полем linkToCategories. Если я настрою @OneToMany(fetch = FetchType.EAGER), все будет работать нормально.

Конфигурация приложения:

    @Bean
    public LocalSessionFactoryBean sessionFactory(DataSource dataSource) throws ClassNotFoundException {
        LocalSessionFactoryBean localSessionFactoryBean = new LocalSessionFactoryBean();
        localSessionFactoryBean.setDataSource(dataSource);
              localSessionFactoryBean.setPackagesToScan("com.project.backend.model",
            "com.project.backend.hibernate.converters");
        return localSessionFactoryBean;
    }

    @Bean
    public HibernateTransactionManager transactionManager(SessionFactory sessionFactory) {
        return new HibernateTransactionManager(sessionFactory);
    }

Позднее редактирование: После долгих отладок разница между старой и новой функциональностью Hibernate заключается в HibernateTransactionManager. В методе doGetTransaction() в Hibernate 4 он находит объект SessionHolder при вызове

TransactionSynchronizationManager.getResource(getSessionFactory())

в то время как в Hibernate 5 это не так.

    SessionHolder sessionHolder =
            (SessionHolder) TransactionSynchronizationManager.getResource(getSessionFactory());
    if (sessionHolder != null) {
        if (logger.isDebugEnabled()) {
            logger.debug("Found thread-bound Session [" + sessionHolder.getSession() + "] for Hibernate transaction");
        }
        txObject.setSessionHolder(sessionHolder);
    }
    else if (this.hibernateManagedSession) {
        try {
            Session session = this.sessionFactory.getCurrentSession();
            if (logger.isDebugEnabled()) {
                logger.debug("Found Hibernate-managed Session [" + session + "] for Spring-managed transaction");
            }
            txObject.setExistingSession(session);
        }
        catch (HibernateException ex) {
            throw new DataAccessResourceFailureException(
                    "Could not obtain Hibernate-managed Session for Spring-managed transaction", ex);
        }
    }

В методе doBegin создается новый сеанс и устанавливается в txObject для каждого запроса.

        if (txObject.getSessionHolder() == null || txObject.getSessionHolder().isSynchronizedWithTransaction()) {
            Interceptor entityInterceptor = getEntityInterceptor();
            Session newSession = (entityInterceptor != null ?
                    getSessionFactory().withOptions().interceptor(entityInterceptor).openSession() :
                    getSessionFactory().openSession());
            if (logger.isDebugEnabled()) {
                logger.debug("Opened new Session [" + newSession + "] for Hibernate transaction");
            }
            txObject.setSession(newSession);
        }

Мой опыт работы с Hibernate довольно мал, поэтому здесь я застрял. Возможно, дело в настройках, но я не могу найти.

Итак, вы обновляетесь с 4.3 (Spring) до 5.1 (2 основные версии), в спящем режиме вы переходите на 4 версии, переходите из приложения Spring в приложение Spring Boot. Не слишком ли много вы делаете за один раз? Вы используете спящий режим, но используете ли вы JPA или обычный спящий режим. Вы не показываете код, который не работает, и конфигурацию.

M. Deinum 09.05.2019 12:27

Это не первое обновление Spring Boot, так что ничего особенного. Hibernate 5.3.9.Final поставляется с Spring Boot, поэтому я не стал переходить на предыдущую версию. Я учту это, спасибо. Что касается кода, я не добавлял его, потому что его легко объяснить. Сейчас сделаю некоторые модификации.

Cristian 09.05.2019 12:34

Вы используете простой спящий режим, а НЕ jpa, поэтому включение OpenEntityManagerInView, конечно, не поможет. Судя по вашему вопросу, вы переходите от обычного приложения Spring к Spring Boot.

M. Deinum 09.05.2019 12:49

Да, это правильно, я перехожу на Spring Boot. Я делал это раньше, но у меня не был включен Hibernate.

Cristian 09.05.2019 12:50

Только что сделал тест с Hibernate 5.0.3.Final, результат тот же. Что касается конфигурации, я ничего не делал, кроме как переместил ее из файла конфигурации в код.

Cristian 09.05.2019 12:52

Вы переходите с hibernate4 на 5, убедитесь, что вы используете правильную инфраструктуру. Также, как уже упоминалось, вы используете Hibernate NOT простой JPA.

M. Deinum 09.05.2019 13:00

Что вы подразумеваете под правильной инфраструктурой? Все работает нормально, единственная проблема это ленивая загрузка.

Cristian 09.05.2019 14:10

Правильные классы Spring для Hibernate 5 и, как уже дважды упоминалось, материал OpenEntituyManager не поможет, поскольку вы используете простой спящий режим, а НЕ jpa. Вместо этого вам нужен OpenSessionInView. Однако я настоятельно рекомендую отказаться от простого Hibernate в пользу JPA. Кроме того, как уже говорилось, вы переходите от простого приложения Spring к Spring BOot, поэтому, вероятно, есть некоторые дополнительные фильтры в файле web.xml или WebApplicationInitializer, используемые для начальной загрузки приложения, которые вы не настроили/не скопировали.

M. Deinum 09.05.2019 14:11

Я попытаюсь применить это снова. Приложение отлично работает после перехода на Spring Boot. Запросы к базе данных также работают очень хорошо. В предыдущей версии использовался Hibernate, теперь по-прежнему Hibernate. Единственная проблема связана с коллекциями EAGER, но я не понимаю, как это работает с Hibernate 4.

Cristian 13.05.2019 09:20

Это не имеет ничего общего с спящей версией. Но с другими компонентами, настроенными в простом приложении Spring, а не в приложении Spring Boot. Как уже говорилось, я подозреваю, что в вашем приложении Spring были настроены OpenSessionInViewFilter или OpenSessionInViewInterceptor. Вы не перенесли это на Spring Boot, и, следовательно, это не удается.

M. Deinum 14.05.2019 09:41

В обновлениях Hibernate есть только одна константа: с каждым обновлением вы узнаете новые вещи, которые всегда делали неправильно. Старые версии Hibernate были довольно снисходительны, новые версии довольно строги.

Gimby 14.05.2019 10:17

Вы были правы, наконец мне удалось проверить, что OpenSessionInViewFilter был настроен и не настроен в Spring Boot.

Cristian 14.05.2019 14:33
1
12
610
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

@xxxToMany аннотации указывают, что по умолчанию используется тип выборки LAZY. Это означает, что вам нужно инициализировать коллекцию, на которую ссылается ваша сущность.

Например.

@Entity
public class Book {
    @OneToMany
    public List<Author> authors;    

}

Есть несколько способов решить эту проблему. Вы можете изменить аннотацию @OneToMany с помощью:

@OneToMany(FetcType=FetchType.EAGER)

Или создать метод, в котором вы будете инициализировать authors, например:

public void initializeAuthors(Book book) {

    Book b = em.find(Book.class, book.getId());
    List<Author> authors = new ArrayList<>(b.getAuthors());

    book.setAuthors(authors);

}

Если у вас есть @NamedQueries в ваших объектах, вы можете сделать это, добавив LEFT JOIN FETCH в свои коллекции.

Я знаю, что вы имеете в виду, но идентичный код отлично работает в Spring 4.3.9.RELEASE + Hibernate 4.3.11.Final.

Cristian 09.05.2019 12:25

Страстное извлечение коллекций oneToMany не совсем хорошая идея, есть веская причина, по которой они ленивы по умолчанию. Это вполне может вызвать бомбардировку запросов в зависимости от того, насколько велика коллекция. Однако объединение выборки является хорошим предложением по производительности.

Gimby 14.05.2019 10:15

На данный момент времени на переработку всего слоя репозитория недостаточно, поэтому приходится придерживаться того, что дано. В будущем оптимизация Hibernate и интеграция — следующий логический шаг.

Cristian 14.05.2019 14:40

@Gimby Согласитесь, тип выборки EAGER - не очень хорошая идея. Инициализация отношений, когда вам это нужно, гораздо лучше. Я описал это в своем ответе.

stakahop 14.05.2019 14:44
Ответ принят как подходящий

Как говорил М. Дейнум, конфигурация Spring 4.3.9.RELEASE + Hibernate 4.3.11.Final загружала OpenSessionInViewFilter, что объясняет, почему все запросы выполнялись успешно. После настройки того же фильтра в Spring Boot все возвращается на круги своя. Добавьте следующий bean-компонент для регистрации фильтра:

@Bean
public FilterRegistrationBean<OpenSessionInViewFilter> registerOpenSessionInViewFilterBean() {
    FilterRegistrationBean<OpenSessionInViewFilter> registrationBean = new FilterRegistrationBean<>();
    OpenSessionInViewFilter filter = new OpenSessionInViewFilter();
    registrationBean.setFilter(filter);
    return registrationBean;
}

Следующим шагом является замена простого Hibernate на JPA и OpenSessionInViewFilter на OpenEntityManagerInViewFilter.

Спасибо М. Дейнум.

Другие вопросы по теме