在Spring 3.1中使用JPA Lazy Loading(延时加载)
Posted on: 2013-08-09, Last modified: 2015-07-31, View: 9651

ORM框架给我们带来的不仅仅是对象映射,很多产品带给我们带来了一些非常实用的解决方案。比如延时加载。延时加载就是对象的某些关联在持久层加载对象时并没有真正的从持久库中加载,而是载入一个代理,当真正的使用到这些关联的属性时,如调用get方法时,代理会从持久库中加载。整个过程不需要我们来实现,因为大部分ORM都有,所以它也成了JPA规范的一部分。关于更多的JPA延时加载的资料,这里就不多说了,本文主要讲的是在Spring中使上的问题。

问题:
上面描述了延时加载的过程,这个过程看起来好像没有什么问题,但在我们在某些应用尤其是大部分Web应用时,事情就不是看起来这么简单了。以大部分Web应用为例,我们常常会使用多层架构(如常见的MVC)来设计我们的应用,这种架构下视图层和持久层是两个不同的层次,一般的一个持久会话的作用域就是一个持久层方法,比如我们在持久层打开一个会话加载了对象然后关闭会话,这时当我们在模型层延时加载对象的延时关联时就会出现会话已关闭的错误,如采用Hibernate实现时会报:org.hibernate.LazyInitializationException

1
2
3
Caused by: org.hibernate.LazyInitializationException: could not initialize proxy - no Session
    at org.hibernate.proxy.AbstractLazyInitializer.initialize(AbstractLazyInitializer.java:149)
    ...

如果你也出现了这样的错误,很显然,我们只要保持会话在视图层依然是打开的就可以了,但这时你需要注意,会话最终还是要关闭的,否则你的资源会被耗尽。实现它有很多种方法,这里一个通用的解决方案就是,在视图层前在过滤器或拦截器级别打开与关闭,这也是Spring提供了一种实现。

Spring3.0之前的一个典型实现

在Spring3.0之前,我们使用Spring提供的OpenEntityManagerInViewFilter或OpenEntityManagerInViewInterceptor在视图层控制会话,以OpenEntityManagerInViewFilter为例,我们可以把它配置在web.xml里

1
2
3
4
5
6
7
8
9
<!-- JPA OPEN ENTITY MANAGER -->
<filter>
    <filter-name>jpa-open-entity-manager</filter-name>
    <filter-class>org.springframework.orm.jpa.support.OpenEntityManagerInViewFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>jpa-open-entity-manager</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

默认它会加载一个配置在Spring中id为entityManagerFactory的JPA EntityManagerFactory Bean产生EntityManager,当然你也可以通过entityManagerFactoryBeanName参数指定其它bean。
在Spring里一个典型的entityManagerFactory配置可能是这样:

1
2
3
4
5
6
<!-- ENTITY MANAGER FACTORY -->
<bean id="entityManagerFactory"
    class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
    <property name="persistenceUnitManager" ref="persistenceUnitManager" />
    <property name="persistenceUnitName" value="your-persistence-unit-name" />
</bean>

然后在持久层可以通过JpaTemplate来进行持久化操作,如果没有声明额外的事务,或是在Spring中对它进行事务控制,它会优先使用OpenEntityManagerInViewFilter中的EntityManager

1
2
3
4
<!-- JPA TEMPLATE -->
<bean id="jpaTemplate" class="org.springframework.orm.jpa.JpaTemplate">
    <property name="entityManagerFactory" ref="entityManagerFactory" />
</bean>

这里还有一个问题,就是JpaTemplate并不支持JPA2.0的很多规范,当然你可以通过execute方法,直接使用EntityManager。

替换JpaTemplate

这样的工程当然没有什么问题,但是:

  • JpaTemplate不支持JPA2.0规范
  • 持久层增加了与Spring的耦合度

也许正是由于以上的原因,当你升级到Spring 3.1的时候,JpaTemplate成了deprecated 那么一个可行的JpaTemplate的替换方法就是放弃JpaTemplate,只使用EntityManager。但这时的问题就是怎样使用OpenEntityManagerInViewFilter中产生的EntityManager。
分析OpenEntityManagerInViewFilter及JpaTemplate源码,你可以发现在OpenEntityManagerInViewFilter中的doFilterInternal方法内创建了EntityManager并封装为EntityManagerHolder通过TransactionSynchronizationManager绑定到线程中(Thread.threadLocals)

1
2
EntityManager em = createEntityManager(emf);
TransactionSynchronizationManager.bindResource(emf, new EntityManagerHolder(em));

在JpaTemplate使用时通过TransactionSynchronizationManager取回(使用EntityManagerFactoryUtils.getTransactionalEntityManager)

1
2
3
EntityManagerHolder emHolder =
    (EntityManagerHolder) TransactionSynchronizationManager.getResource(emf);
emHolder.getEntityManager();

所以你可以在你的持久层这样使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class DaoSupport {
    private EntityManager entityManager;
    public EntityManager getEntityManager() {
        try {
            if (null == this.entityManager || !this.entityManager.isOpen()) {
                EntityManager em = EntityManagerFactoryUtils
                        .getTransactionalEntityManager(this.entityManagerFactory);
                if (null == em || !em.isOpen()) {
                    em = this.entityManagerFactory.createEntityManager();
                }
                this.entityManager = em;
            }
        } catch (Exception e) {
        }
        return this.entityManager;
    }
}

这样,你就可以不用JpaTemplate又使用了OpenEntityManagerInViewFilter中的EntityManager重要的是还支持Spring中的事务管理。

使用标准JPA注解(Spring 3.1)

用第二种方法的确让我们脱离了JpaTemplate,但它仍然不是我们想要的结果,因为:

  • 代码不够优雅,不是JPA的规范
  • 持久层仍然与Spring耦合

其实在Spring3.1中已经支持了JPA的注解,你可以通过PersistenceContext注解使用EntityManager。
首先在Spring增加注解的事务支持(如果你使用Spring的事务控制的话),以XML配置为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?xml version="1.0" encoding="UTF-8"?>
 
 
 
 
 
 
    <!-- TRANSACTION MANAGER -->
    <bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
        <property name="entityManagerFactory" ref="entityManagerFactory" />
        <!-- <property name="dataSource" ref="dataSource" /> -->
    </bean>
    <tx:annotation-driven transaction-manager="transactionManager" />
</beans>

然后上面的DaoSupport示例可以写成这样:

1
2
3
4
5
6
7
8
9
10
public class DaoSupport {
    @PersistenceContext
    private EntityManager entityManager;
    public EntityManager getEntityManager() {
        return this.entityManager;
    }
    public void setEntityManager(EntityManager entityManager) {
        this.entityManager = entityManager;
    }
}

这样你的代码会清爽很多。当你调用getEntityManager时,Spring的代理就会为你返回事务中的EntityManager,当然包括了OpenEntityManagerInViewFilter中的EntityManager

 

附录:

在旧的hibernate版本中常有的是三种实现:

1把lazy设成false。这个是最简单的办法,个人认为也是比较笨的方法。因为这是在用效率作为代价。

2使用OpenSessionInViewFilter。这种方法是将session交给servlet filter来管理,每当一个请求来之后就会开

启一个session,只有当响应结束后才会关闭。如下:

复制代码
1         <filter-name>hibernateFilter</filter-name> 
2              <filter-class>  org.springframework.orm.hibernate3.support.OpenSessionInViewFilter </filter-class> 
3         </filter 
4         <filter-mapping> 
5              <filter-name>hibernateFilter</filter-name> 
6              <url-pattern>/*</url-pattern> 
7         </filter-mapping> 
复制代码

 

 

       上面的配置文件时在web.xml中配置的。

 

3将hibernate的抓起策略改为join。也就是是left join fetch或inner join fetch语法。就是在<many-to-one../>中配

置lazy="false" fetch="join"即可。如:

1     <many-to-one name="worker" lazy="false" fetch="join" class="com.paixie.domain.Worker">
2 
3              <column name="positionId"></column>
4 
5         </many-to-one>

--------------------------------------------------------------------------------------------------------

第1种实现比较简单,但是会丢失性能。而且在我的项目中因为有两级oneToMany的映射关系,会报出“cannot simultaneously fetch multiple bags ”这样的异常,当然简单的一层映射时好用

第2种方法,在新的spring的jpa实现中,会带来资源文件丢失的问题,应该是经处理后的请求不合法造成。不知道会不会是JSF的原因。但是使用上面的解决方案,filter class改用org.springframework.orm.jpa.support.OpenEntityManagerInViewFilter就能解决问题,lazyloading有效,页面也能正常访问

From: http://ettear.info/press/2011/12/27/spring-3-1-jpa-lazy-loading/
Go
Friend Links:
Bill Site
https://item.taobao.com/item.htm?spm=a21an.7676007.1998473182.296.lAvrE2&id=45399580446