Hibernate/Spring: 懒加载失败 - 无会话或会话已关闭。

39

答案请往下滑至文章末尾...

基本问题已经被多次提出。我有一个简单的程序,包含两个POJOs:Event和User - 其中用户可以拥有多个事件。

@Entity
@Table
public class Event {
 private Long id;
 private String name;
 private User user;

 @Column
 @Id
 @GeneratedValue
 public Long getId() {return id;}
 public void setId(Long id) { this.id = id; }

 @Column
 public String getName() {return name;}
 public void setName(String name) {this.name = name;}

 @ManyToOne
 @JoinColumn(name="user_id")
 public User getUser() {return user;}
 public void setUser(User user) {this.user = user;}

}

用户:

@Entity
@Table
public class User {
 private Long id;
 private String name;
 private List<Event> events;

 @Column
 @Id
 @GeneratedValue
 public Long getId() { return id; }
 public void setId(Long id) { this.id = id; }

 @Column
 public String getName() { return name; }
 public void setName(String name) { this.name = name; }

 @OneToMany(mappedBy="user", fetch=FetchType.LAZY)
 public List<Event> getEvents() { return events; }
 public void setEvents(List<Event> events) { this.events = events; }

}

注意:这是一个示例项目。我真的想在这里使用Lazy fetching。
现在我们需要配置Spring和Hibernate,并有一个简单的basic-db.xml用于加载:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans 
           http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
           http://www.springframework.org/schema/aop 
           http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">
<bean id="myDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close" scope="thread"> <property name="driverClassName" value="com.mysql.jdbc.Driver" /> <property name="url" value="jdbc:mysql://192.168.1.34:3306/hibernateTest" /> <property name="username" value="root" /> <property name="password" value="" /> <aop:scoped-proxy/> </bean>
<bean class="org.springframework.beans.factory.config.CustomScopeConfigurer"> <property name="scopes"> <map> <entry key="thread"> <bean class="org.springframework.context.support.SimpleThreadScope" /> </entry> </map> </property> </bean>
<bean id="mySessionFactory" class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean" scope="thread"> <property name="dataSource" ref="myDataSource" /> <property name="annotatedClasses"> <list> <value>data.model.User</value> <value>data.model.Event</value> </list> </property> <property name="hibernateProperties"> <props> <prop key="hibernate.dialect">org.hibernate.dialect.MySQLDialect</prop> <prop key="hibernate.show_sql">true</prop> <prop key="hibernate.hbm2ddl.auto">create</prop> </props> </property> <aop:scoped-proxy/>
</bean>
<bean id="myUserDAO" class="data.dao.impl.UserDaoImpl"> <property name="sessionFactory" ref="mySessionFactory" /> </bean>
<bean id="myEventDAO" class="data.dao.impl.EventDaoImpl"> <property name="sessionFactory" ref="mySessionFactory" /> </bean>
</beans>
注意:我已经尝试使用CustomScopeConfigurer和SimpleThreadScope,但没有改变任何东西。
我有一个简单的dao-impl(只粘贴了userDao - EventDao基本相同 - 除了没有“listWith”函数): public class UserDaoImpl implements UserDao{ private HibernateTemplate hibernateTemplate;
public void setSessionFactory(SessionFactory sessionFactory) { this.hibernateTemplate = new HibernateTemplate(sessionFactory); }
@SuppressWarnings("unchecked") @Override public List listUser() { return hibernateTemplate.find("from User"); }
@Override public void saveUser(User user) { hibernateTemplate.saveOrUpdate(user); }
@Override public List listUserWithEvent() { List users = hibernateTemplate.find("from User"); for (User user : users) { System.out.println("LIST : " + user.getName() + ":"); user.getEvents().size(); } return users; } }
我遇到了org.hibernate.LazyInitializationException异常 - 在user.getEvents().size()这一行出现了“无法延迟初始化角色集合:data.model.User.events,没有会话或会话已关闭”。
最后,这是我使用的测试类:
公共类HibernateTest {
public static void main(String[] args) {
ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("basic-db.xml");
UserDao udao = (UserDao) ac.getBean("myUserDAO"); EventDao edao = (EventDao) ac.getBean("myEventDAO");
System.out.println("新用户..."); User user = new User(); user.setName("测试");
Event event1 = new Event(); event1.setName("生日1"); event1.setUser(user);
Event event2 = new Event(); event2.setName("生日2"); event2.setUser(user);
udao.saveUser(user); edao.saveEvent(event1); edao.saveEvent(event2);
List users = udao.listUserWithEvent(); System.out.println("用户的事件"); for (User u : users) {
System.out.println(u.getId() + ":" + u.getName() + " --"); for (Event e : u.getEvents()) { System.out.println("\t" + e.getId() + ":" + e.getName()); } }
((ConfigurableApplicationContext)ac).close(); }
}

这是异常:

1621 [main] 错误 org.hibernate.LazyInitializationException - 无法懒加载角色集合:data.model.User.events,没有会话或会话已关闭 org.hibernate.LazyInitializationException: 无法懒加载角色集合:data.model.User.events,没有会话或会话已关闭 在 org.hibernate.collection.AbstractPersistentCollection.throwLazyInitializationException(AbstractPersistentCollection.java:380) 在 org.hibernate.collection.AbstractPersistentCollection.throwLazyInitializationExceptionIfNotConnected(AbstractPersistentCollection.java:372) 在 org.hibernate.collection.AbstractPersistentCollection.readSize(AbstractPersistentCollection.java:119) 在 org.hibernate.collection.PersistentBag.size(PersistentBag.java:248) 在 data.dao.impl.UserDaoImpl.listUserWithEvent(UserDaoImpl.java:38) 在 HibernateTest.main(HibernateTest.java:44) 主线程中的异常"main" org.hibernate.LazyInitializationException: 无法懒加载角色集合:data.model.User.events,没有会话或会话已关闭 在 org.hibernate.collection.AbstractPersistentCollection.throwLazyInitializationException(AbstractPersistentCollection.java:380) 在 org.hibernate.collection.AbstractPersistentCollection.throwLazyInitializationExceptionIfNotConnected(AbstractPersistentCollection.java:372) 在 org.hibernate.collection.AbstractPersistentCollection.readSize(AbstractPersistentCollection.java:119) 在 org.hibernate.collection.PersistentBag.size(PersistentBag.java:248) 在 data.dao.impl.UserDaoImpl.listUserWithEvent(UserDaoImpl.java:38) 在 HibernateTest.main(HibernateTest.java:44)

尝试但未成功的方法:

  • 分配一个线程范围并使用beanfactory(我使用了“request”或“thread”-没有注意到任何区别):
  // 范围相关
  Scope threadScope = new SimpleThreadScope();
  ConfigurableListableBeanFactory beanFactory = ac.getBeanFactory();
  beanFactory.registerScope("request", threadScope);
  ac.refresh();
...
  • 通过从deo获取会话对象来设置事务:
...
  Transaction tx = ((UserDaoImpl)udao).getSession().beginTransaction();
  tx.begin();
  users = udao.listUserWithEvent();
...
  • 在listUserWithEvent()中获取事务
 public List listUserWithEvent() {
  SessionFactory sf = hibernateTemplate.getSessionFactory();
  Session s = sf.openSession();
  Transaction tx = s.beginTransaction();
  tx.begin();
List users = hibernateTemplate.find("from User"); for (User user : users) { System.out.println("LIST : " + user.getName() + ":"); user.getEvents().size(); } tx.commit(); return users; }

我现在真的没有更多的想法了。此外,使用listUser或listEvent也可以正常工作。

向前迈进:

多亏了Thierry,我觉得我又向前迈进了一步。我创建了MyTransaction类,并在其中完成了所有工作,从spring获取了所有内容。新的main方法如下:


 public static void main(String[] args) {
ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("basic-db.xml");
// 获取dao UserDao udao = (UserDao) ac.getBean("myUserDAO"); EventDao edao = (EventDao) ac.getBean("myEventDAO");
// 获取事务模板 TransactionTemplate transactionTemplate = (TransactionTemplate) ac.getBean("transactionTemplate");
MyTransaction mt = new MyTransaction(udao, edao); transactionTemplate.execute(mt);
((ConfigurableApplicationContext)ac).close(); }

不幸的是,现在在daoImpl中出现了空指针异常:user.getEvents().size();。

我知道它不应该为空(既不是从控制台输出,也不是从数据库布局)。以下是更多信息的控制台输出(我检查了user.getEvent() == null并打印了“EVENT is NULL”):

新用户... Hibernate: 插入到User (name)值(?) Hibernate: 插入到User (name)值(?) Hibernate: 插入到Event (name, user_id)值(?, ?) Hibernate: 插入到Event (name, user_id)值(?, ?) Hibernate: 插入到Event (name, user_id)值(?, ?) 用户列表: Hibernate: 选择user0_.id作为id0_,user0_.name作为name0_从用户user0_ 1:User1 2:User2 事件列表: Hibernate: 选择event0_.id作为id1_,event0_.name作为name1_,event0_.user_id作为user3_1_从事件event0_ 1:1的生日1:User1 2:1的生日2:User1 3:2的婚礼:User2 Hibernate: 选择user0_.id作为id0_,user0_.name作为name0_从用户user0_ 用户的事件 1:User1-- 事件为空 2:User2-- 事件为空

您可以从http://www.gargan.org/code/hibernate-test1.tgz获取示例项目(这是一个eclipse/maven项目)

解决方案(适用于控制台应用程序)

实际上,对于这个问题有两种解决方案 - 取决于您的环境:

对于控制台应用程序,您需要一个事务模板来捕获实际的数据库逻辑并处理事务:


public class UserGetTransaction implements TransactionCallback{
public List users;
protected ApplicationContext context;
public UserGetTransaction (ApplicationContext context) { this.context = context; }
@Override public Boolean doInTransaction(TransactionStatus arg0) { UserDao udao = (UserDao) ac.getBean("myUserDAO"); users = udao.listUserWithEvent(); return null; }
}

您可以通过以下方式使用它:


 TransactionTemplate transactionTemplate = (TransactionTemplate) context.getBean("transactionTemplate");
 UserGetTransaction mt = new UserGetTransaction(context);
 transactionTemplate.execute(mt);

为了使其工作,您需要在Spring中定义模板类(例如在basic-db.xml中):

<bean id="transactionTemplate" class="org.springframework.transaction.support.TransactionTemplate">
    <property name="transactionManager" ref="transactionManager"/>
</bean>

另一种(可能的)解决方案

谢谢 Andi

    PlatformTransactionManager transactionManager = (PlatformTransactionManager) applicationContext.getBean("transactionManager");
    DefaultTransactionAttribute transactionAttribute = new DefaultTransactionAttribute(TransactionDefinition.PROPAGATION_REQUIRED);

transactionAttribute.setIsolationLevel(TransactionDefinition.ISOLATION_SERIALIZABLE);
    TransactionStatus status = transactionManager.getTransaction(transactionAttribute);
    boolean success = false;
    try {
      new UserDataAccessCode().execute();
      success = true;
    } finally {
      if (success) {
        transactionManager.commit(status);
      } else {
        transactionManager.rollback(status);
      }
    }
解决方案(适用于Servlet)

Servlet并不是什么大问题。当你有一个Servlet时,你可以在函数开始时简单地启动和绑定事务,并在结束时解除绑定:

public void doGet(...) {
  SessionFactory sessionFactory = (SessionFactory) context.getBean("sessionFactory");
  Session session = SessionFactoryUtils.getSession(sessionFactory, true);
  TransactionSynchronizationManager.bindResource(sessionFactory, new SessionHolder(session));

// Your code....

  TransactionSynchronizationManager.unbindResource(sessionFactory);
}
7个回答

26

我认为你不应该使用Hibernate的会话事务方法,而是让Spring来处理。

将以下内容添加到您的Spring配置文件中:

<bean id="txManager" class="org.springframework.orm.hibernate3.HibernateTransactionManager">
    <property name="sessionFactory" ref="mySessionFactory" />
</bean>

<bean id="transactionTemplate" class="org.springframework.transaction.support.TransactionTemplate">
    <property name="transactionManager" ref="txManager"/>
</bean>

然后我会修改你的测试方法,使用Spring事务模板:

public static void main(String[] args) {
    // init here (getting dao and transaction template)

    transactionTemplate.execute(new TransactionCallback() {
        @Override
        public Object doInTransaction(TransactionStatus status) {
          // do your hibernate stuff in here : call save, list method, etc
        }
    }
}
作为一个附带说明,@OneToMany关联默认是惰性加载的,所以您不需要注释它为lazy。(@*ToMany默认是LAZY的,@*ToOne默认是EAGER的)
编辑:这里是从Hibernate角度发生的事情:
- 打开会话(并启动事务) - 保存用户并将其保留在会话中(将会话缓存视为实体哈希图,其中键是实体ID) - 保存事件并将其保留在会话中 - 再保存另一个事件并将其保留在会话中 - …其他保存操作也相同… - 然后加载所有用户(“from Users”查询) - 在此时,Hibernate看到它已经在会话中拥有该对象,因此丢弃它从请求中获取的对象并返回会话中的对象。 - 您会发现会话中的用户未初始化其事件集合,因此您得到null。 - ...
以下是一些增强代码的建议:
- 当集合排序不需要时,请在模型中使用Set而不是List来定义您的集合(private Set events,而不是private List events)。 - 在您的模型中,对您的集合进行类型标注,否则Hibernate无法确定要提取哪个实体(private Set<Event> events)。 - 当您设置双向关系的一侧并且希望在同一事务中使用关系的mappedBy侧时,请同时设置两侧。在下一个tx之前(当会话是从数据库状态的新视图时),Hibernate不会为您处理它。
因此,要解决上述问题,或者在一个事务中进行保存,然后在另一个事务中进行加载:
public static void main(String[] args) {
    // init here (getting dao and transaction template)
    transactionTemplate.execute(new TransactionCallback() {
        @Override
        public Object doInTransaction(TransactionStatus status) {
          // save here
        }
    }

    transactionTemplate.execute(new TransactionCallback() {
        @Override
        public Object doInTransaction(TransactionStatus status) {
          // list here
        }
    }
}

或者将两边都设置:

...
event1.setUser(user);
...
event2.setUser(user);
...
user.setEvents(Arrays.asList(event1,event2));
...

(还要记得解决上面提到的代码改进点,使用Set而非List,指定集合类型)


1
我尝试了你的解决方案,成功解决了懒加载问题,但是现在它完全不加载任何事件:user.getEvents() 产生了一个空指针异常(尽管我可以从数据库和第一次迭代中看到用户确实有相关联的事件)。可能是 hibernateTemplate.find 没有解析依赖关系吗? - Niko
我已经按照你的建议上传了我的测试项目,也许你可以找出问题所在。 - Niko

9

对于Web应用程序,也可以在web.xml中声明特殊的过滤器,它将执行会话-每个请求:

<filter>
    <filter-name>openSessionInViewFilter</filter-name>
    <filter-class>org.springframework.orm.hibernate3.support.OpenSessionInViewFilter</filter-class>
</filter>

<filter-mapping>
    <filter-name>openSessionInViewFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

之后,您可以在请求期间随时使用延迟加载来加载数据。


在我的应用程序中,这个过滤器也被声明了,但是会抛出异常。 - Harmeet Singh Taara

7

我在这里寻找关于类似问题的提示。我尝试了Thierry提到的解决方案,但它没有起作用。之后,我尝试了以下几行代码,它成功解决了问题:

SessionFactory sessionFactory = (SessionFactory) context.getBean("sessionFactory");
Session session = SessionFactoryUtils.getSession(sessionFactory, true);
TransactionSynchronizationManager.bindResource(sessionFactory, new SessionHolder(session));

实际上,我正在进行的是批处理过程,必须利用Spring现有的管理器/服务。在加载上下文并执行一些调用后,我发现了著名的问题“无法懒惰地初始化集合”。这三行代码为我解决了这个问题。


搞定了!尽管我的xml文件中写着<property name="sessionFactory" ref="wfSessionFactory"/>,但我必须使用getBean("wfSessionFactory")。此外,在结尾处可能需要一个“unbindResource”调用(请参见问题末尾的答案)。 - rogerdpack

3
问题在于您的dao正在使用一个Hibernate会话,但是user.getName的懒加载(我假设这就是它报错的地方)发生在该会话之外--要么根本没有会话,要么在另一个会话中。通常,我们在进行DAO调用之前打开一个Hibernate会话,并且直到完成所有懒加载后才关闭它。Web请求通常包装在一个大会话中,因此不会出现这些问题。
通常情况下,我们将dao和懒加载调用包装在SessionWrapper中。类似以下内容:
public class SessionWrapper {
    private SessionFactory sessionFactory;
    public void setSessionFactory(SessionFactory sessionFactory) {
        this.hibernateTemplate = new HibernateTemplate(sessionFactory);
    }
    public <T> T runLogic(Callable<T> logic) throws Exception {
        Session session = null;
        // if the session factory is already registered, don't do it again
        if (TransactionSynchronizationManager.getResource(sessionFactory) == null) {
            session = SessionFactoryUtils.getSession(sessionFactory, true);
            TransactionSynchronizationManager.bindResource(sessionFactory, new SessionHolder(session));
        }

        try {
            return logic.call();
        } finally {
            // if we didn't create the session don't unregister/release it
            if (session != null) {
                TransactionSynchronizationManager.unbindResource(sessionFactory);
                SessionFactoryUtils.releaseSession(session, sessionFactory);
            }
        }
    }
}

显然,SessionFactory是注入到您的dao中的同一个SessionFactory。


在您的情况下,您应该将整个listUserWithEvent主体包装在这个逻辑中。类似于:

public List listUserWithEvent() {
    return sessionWrapper.runLogic(new Callable<List>() {
        public List call() {
            List users = hibernateTemplate.find("from User");
            for (User user : users) {
                System.out.println("LIST : " + user.getName() + ":");
                user.getEvents().size();
            }
        }
    });
}

您需要将SessionWrapper实例注入到您的DAO中。


很遗憾,我真的不知道如何让它工作。你是说你的DAOImpl扩展了SessionWrapper吗?在我的例子中,谁会调用runLogic,Callable会是什么?也许当你扩展示例并将其适应我提供的示例代码时,情况会更清晰。此外,我的问题特定于非Web,因为我想在独立应用程序(我们的数据爬虫)中也使用相同的模型/ DAOs。 - Niko
抱歉让你感到困惑。我已添加了一个带有runLogic示例的部分。您不需要更改dao。它应该使用注入到SessionWrapper中的相同SessionFactory进行注入。然后将SessionWrapper注入到daos中。 - Gray
这就是Spring事务模板正在做的事情。(只是缺少打字:/) - Thierry
Spring事务模板也会开启数据库事务,对吧?我们不想在我们的会话包装器中这样做。 - Gray
你说的打开数据库 tx 是什么意思?它会打开一个到数据库的连接,就像从sessionFactory获取一个session一样。启用事务只是将连接上的autoCommit标志从on设置为off的事实。 - Thierry
取决于实现方式。在Hibernate中,我相信您也正在启动SavePoint,这会导致数据库必须保存状态,以便您可以回滚。对吗? - Gray

2

有趣!

我在一个@Controller的@RequestMapping处理方法中遇到了相同的问题。 简单的解决方法是在处理程序方法中添加@Transactional注释,以便在整个方法体执行期间保持会话处于打开状态。


1

最简单的实现方案:

在会话范围内[在使用 @Transactional 注释的 API 内部],执行以下操作:

如果 A 有一个懒加载的 List<B>,只需调用一个 API 来确保 List 已加载

那个 API 是什么?

List 类的 size(); API。

所以只需要:

Logger.log(a.getBList.size());

这个简单的调用记录大小确保在计算列表大小之前获取整个列表。现在你将不会收到异常!

0
在JBoss中对我们起作用的是从Java Code Geeks这个网站采取的解决方案#2。

Web.xml:

  <filter>
      <filter-name>ConnectionFilter</filter-name>
      <filter-class>web.ConnectionFilter</filter-class>
  </filter>
  <filter-mapping>
      <filter-name>ConnectionFilter</filter-name>
      <url-pattern>/faces/*</url-pattern>
  </filter-mapping>

连接过滤器:

import java.io.IOException;
import javax.annotation.Resource;
import javax.servlet.*;
import javax.transaction.UserTransaction;

public class ConnectionFilter implements Filter {
    @Override
    public void destroy() { }

    @Resource
    private UserTransaction utx;

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        try {
            utx.begin();
            chain.doFilter(request, response);
            utx.commit();
        } catch (Exception e) { }
    }

    @Override
    public void init(FilterConfig arg0) throws ServletException { }
}

可能 Spring 也能做到。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接