org.hibernate.Исключение LazyInitializationException: как правильно использовать функцию отложенной загрузки Hibernate

#java #hibernate

#java #спящий режим

Вопрос:

У меня возникли некоторые проблемы с загрузкой списка объектов из моей базы данных с использованием Hibernate и режима lazy = true. Надеюсь, что кто-нибудь сможет мне здесь помочь.

Здесь у меня есть простой класс под названием UserAccount, который выглядит следующим образом:

 public class UserAccount {
    long id;
    String username;
    List<MailAccount> mailAccounts = new Vector<MailAccount>();

    public UserAccount(){
        super();
    }

    public long getId(){
        return id;
    }

    public void setId(long id){
        this.id = id;
    }

    public String getUsername(){
        return username;
    }

    public void setUsername(String username){
        this.username = username;
    }

    public List<MailAccount> getMailAccounts() {
        if (mailAccounts == null) {
            mailAccounts = new Vector<MailAccount>();
        }
        return mailAccounts;
    }

    public void setMailAccounts(List<MailAccount> mailAccounts) {
        this.mailAccounts = mailAccounts;
    }
}
  

Я сопоставляю этот класс в Hibernate с помощью следующего файла сопоставления:

 <?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
    <class name="test.account.UserAccount" table="USERACCOUNT">

        <id name="id" type="long" access="field">
            <column name="USER_ACCOUNT_ID" />
            <generator class="native" />
        </id>

        <property name="username" />

        <bag name="mailAccounts" table="MAILACCOUNTS" lazy="true" inverse="true" cascade="all">
            <key column="USER_ACCOUNT_ID"></key>
            <one-to-many class="test.account.MailAccount" />
        </bag>

    </class>
</hibernate-mapping>
  

Как вы можете видеть, для параметра lazy установлено значение «true» в элементе сопоставления пакетов.

Сохранение данных в базу данных работает нормально:

Загрузка также выполняется путем вызова loadUserAccount(String username) (см. Код ниже):

 public class HibernateController implements DatabaseController {
    private Session                 session         = null;
    private final SessionFactory    sessionFactory  = buildSessionFactory();

    public HibernateController() {
        super();
    }

    private SessionFactory buildSessionFactory() {
        try {
            return new Configuration().configure().buildSessionFactory();
        } catch (Throwable ex) {
            System.err.println("Initial SessionFactory creation failed."   ex);
            throw new ExceptionInInitializerError(ex);
        }
    }

    public UserAccount loadUserAccount(String username) throws FailedDatabaseOperationException {
        UserAccount account = null;
        Session session = null;
        Transaction transaction = null;
        try {
            session = getSession();
            transaction = session.beginTransaction();
            Query query = session.createQuery("FROM UserAccount WHERE username = :uname").setParameter("uname", username));
            account = (UserAccount) query.uniqueResult();
            transaction.commit();
        } catch (Exception e) {
            transaction.rollback();
            throw new FailedDatabaseOperationException(e);
        } finally {
            if (session.isOpen()) {
                // session.close();
            }
        }

        return account;
    }

    private Session getSession() {
        if (session == null){
            session = getSessionFactory().getCurrentSession();
        }
        return session;
    }
}
  

Проблема просто в том, что когда я обращаюсь к элементам в списке «Почтовые аккаунты», я получаю следующее исключение:

org.hibernate.LazyInitializationException: не удалось лениво инициализировать коллекцию role: test.account.Учетная запись пользователя.Почтовые учетные записи, ни один сеанс или session не был закрыт

Я предполагаю, что причиной этого исключения является то, что сеанс был закрыт (не знаю, почему и как), и, следовательно, Hibernate не может загрузить список. Как вы можете видеть, я даже удалил session.close() вызов из loadUserAccount() метода, но сеанс по-прежнему, похоже, либо закрывается, либо заменяется другим экземпляром. Если я установил lazy=false , то все работает гладко, но это не то, что я хотел, потому что мне нужна функция загрузки данных «по требованию» из-за проблем с производительностью.

Итак, если я не могу быть уверен, что мой сеанс все еще действителен после того, как метод loadUserAccount(String username) завершен, какой смысл иметь эту функцию и как мне обойти это?

Спасибо за вашу помощь!

PS: Я новичок в Hibernate, поэтому, пожалуйста, извините мою тупость.

Обновление: Вот мой режим гибернации config.cfg.xml

 <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-configuration PUBLIC
        "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
        "http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
    <session-factory>
        <property name="hibernate.connection.driver_class">com.mysql.jdbc.Driver</property>
        <property name="hibernate.connection.password">foo</property>
        <property name="hibernate.connection.url">jdbc:mysql://localhost:3306/mytable</property>
        <property name="hibernate.connection.username">user</property>
        <property name="hibernate.dialect">org.hibernate.dialect.MySQLInnoDBDialect</property>

        <!-- Auto create tables -->
<!--        <property name="hbm2ddl.auto">create</property>-->

        <!-- Enable Hibernate's automatic session context management -->
        <property name="current_session_context_class">thread</property>

        <!-- Mappings -->
        <mapping resource="test/account/SmampiAccount.hbm.xml"/>
        <mapping resource="test/account/MailAccount.hbm.xml"/>
    </session-factory>
</hibernate-configuration>
  

Ответ №1:

Отложенная загрузка, работающая или нет, не имеет никакого отношения к границам транзакции. Для этого требуется только, чтобы сеанс был открыт.

Однако время открытия сеанса зависит от того, как вы на самом деле настроили SessionFactory, о чем вы нам не сказали! За тем, что SessionFactory.getCurrentSession() на самом деле делает, происходит настройка! Если вы оставляете ее с версией по умолчанию ThreadLocalSessionContext и ничего не делаете для управления жизненным циклом, это действительно по умолчанию закрывает сеанс при фиксации. (Отсюда распространенное представление о том, что расширение границ транзакции является «исправлением» для исключения отложенной загрузки.)

Если вы управляете своим собственным жизненным циклом сеанса с помощью sessionFactory.openSession() и session.close() , вы сможете нормально выполнять отложенную загрузку в рамках жизненного цикла сеанса, за пределами границ транзакции. В качестве альтернативы вы можете предоставить подкласс ThreadLocalSessionContext , который управляет жизненным циклом сеанса с желаемыми вами границами. Существуют также легко доступные альтернативы, такие как фильтр OpenSessionInView, который можно использовать в веб-приложениях для привязки жизненного цикла сеанса к жизненному циклу веб-запроса.

редактировать: Вы также можете, конечно, просто инициализировать список внутри транзакции, если это работает для вас. Я просто думаю, что это приводит к действительно неуклюжим API, когда вам нужна либо новая сигнатура метода, либо какой-то параметр ‘flag’ для каждого уровня гидратации вашего объекта. dao.getUser() dao.getUserWithMailAccounts() dao.getuserwithmailaccounts иhistoricalids () и так далее.

редактирование 2: Вы можете найти это полезным для разных подходов к тому, как долго сеанс остается открытым / взаимосвязь между областью сеанса и областью транзакции. (в частности, идея сеанса для каждого запроса с отсоединенными объектами по сравнению с сеансом для каждого разговора.)

Это зависит от ваших требований и архитектуры, насколько масштабным на самом деле является диалог.

Комментарии:

1. Я думаю, что открытие и закрытие сеанса вручную может быть вариантом. Однако было бы разумно оставить сеанс открытым в течение всего срока службы приложения и закрыть его только после завершения работы приложения?

2. Обновление: Замена factory.getCurrentSession() на factory.openSession() , никогда не закрывая сеанс, работает, но я все еще задаюсь вопросом, является ли это хорошей практикой.

3. Это был бы необычный подход, но он мог бы быть жизнеспособным для настольного приложения. Сеансы не являются потокобезопасными, поэтому вам нужно быть осторожным при синхронизации доступа от асинхронных рабочих. Сложно сказать, ничего не зная о вашем приложении! Вероятно, существует меньший объем жизненного цикла сеанса, который был бы разумным. Помните, что при выходе из сеанса исключения вы должны получить новое. Таким образом, требуется некоторое управление, даже если вы сохраняете ту же ссылку. И вы, вероятно, также не хотите, чтобы ваш кэш L1 рос без ограничений.

4. Я создаю многопоточное веб-приложение. Не знал, что сеанс не является потокобезопасным. Это еще немного усложнит задачу. В любом случае, большое спасибо за ваш совет. Я ценю это!

5. «В качестве альтернативы вы можете предоставить подкласс ThreadLocalSessionContext, который управляет жизненным циклом сеанса с желаемыми границами… «Не могли бы вы, пожалуйста, пояснить, как это сделать.. Спасибо

Ответ №2:

Причиной, по которой вы получаете исключение, может быть то, что транзакция, в которую вы загружаете данные, закрыта (и сеанс с ней), т. Е. вы работаете вне сеанса. Отложенная загрузка особенно полезна при работе с объектами в одном сеансе (или между сеансами при правильном использовании кэша второго уровня).

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

Комментарии:

1. Меня бы действительно заинтересовал тот параметр конфигурации, который вы упомянули в своем последнем предложении 🙂

2. К сожалению, у меня больше нет доступа к коду, который мы использовали, но, возможно, эта статья сможет вам помочь: theserverside.com/news/1363571/Remote-Lazy-Loading-in-Hibernate (мы использовали для загрузки отложенных ассоциаций в удаленном клиенте).

Ответ №3:

Вам нужно обернуть весь ваш процесс в транзакцию.

Итак, вместо запуска и фиксации транзакции в loadUserAccount, сделайте это вне этого.

Например:

 public void processAccount()
{
    getSession().beginTransaction();
    UserAccount userAccount = loadUserAccount("User");
    Vector accts = userAccount.getMailAccounts();  //This here is lazy-loaded -- DB requests will happen here
    getSession().getTransaction().commit();
}
  

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

Комментарии:

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

2. Что это за приложение? Веб-приложение? Автономный?