[译文]JPA的实施模式:延迟加载

原文:JPA implementation patterns: Lazy loading

作者:Vincent Partington

出处:http://blog.xebia.com/2009/04/27/jpa-implementation-patterns-lazy-loading/

 

在之前三篇关于JPA实施系列的博客中,我涵盖了保存实体、检索实体和删除实体等基本操作,在本篇博客中我会接着沿用不同的视角来探讨如何延迟加载实体这一主题,以及这一做法会如何影响你的应用。

任何使用了Hibernate一段时间的人,都有可能已经见过了一两个LazyInitializationException异常的出现,通常会尾随着“failed to lazily initialize a collection of role: com.xebia.jpaip.order.Order.orderLines, no session or session was closed”或者“could not initialize proxy - no Session”这一类的信息,即使这些信息可能会使Hibernate的新用户感到困惑,但它们还是比OpenJPA在这些情况下(至少是在使用运行时字节码增强的时候)抛出NullPointerExceptions异常要好得多。

既然JPA允许只要一访问哪怕仅是一个实体,就可以在无需加载整个数据库的情况下,为数据库建立起完整的关系模型,那么要充分发挥JPA的潜力就必须要理解延迟加载是如何工作的。

因为这一原因,所以说JPA 1.0规范没有深入地讨论这一主题而仅仅是用大致同于以下的几句话来进行描述是很令人遗憾的:

即时策略(EAGER strategy)是持久性提供程序(persistence provider)运行时方面的一个需求,即数据必须被及时抓取(eagerly fetched),而对于持久性提供程序运行时来说,延迟策略(LAZY strategy)则是一个提示(hint),示意数据在首次被访问时,其应该被延迟加载。这样的实现是允许的,即允许及时加载已经指定了延迟策略提示的数据。

 

截至撰写本文时止,JPA 2.0规范的拟议最后草案并未在这一部分中添加任何内容,现在我们能做的就是阅读JPA提供程序(JPA provider)的文档并做一些实验。

 

延迟加载何时会发生

 

@Basic@OneToMany@ManyToOne@OneToOne@ManyToMany等所有的注解都有一个被称为fetch的可选参数,如果该参数被设置为FetchType.LAZY的话,那么对于JPA提供程序来说,它会被解释成一个提示,示意可以延迟该域的加载直至其第一次被访问:

Ÿ           @Basic注解情况下是属性的值

Ÿ           @ManyToOne或者@OneToOne注解情况下是引用,或者

Ÿ           @OneToMany或者@ManyToMany注解情况下是集合

 

在缺省情况下,即时加载属性的值而延迟加载集合,如果你之前用过纯Hibernate的话,那么以下情况是与你的预期相反的,即在缺省情况下,引用是被即时加载的。

在讨论如何使用延迟加载之前,让我们先来看看JPA提供程序有可能会如何来实现延迟加载。

 

构建时字节码编入、运行时字节码编入和运行时代理

 

为了使得延迟加载有效,JPA提供程序需要变变魔术,把一些并未在某个地方存在的对象显现出来,使它们显得就像在那里似的,JPA提供程序可以通过一些不同的方法来达到这一目的,其中最常用的方法是:

构建时字节码编入(Build-time bytecode instrumentation实体类在被编译之后及打包运行前就被编入。这一做法的一个缺点是其需要修改构建过程以及(因此)不能总是保持与IDE的兼容。被编入的类与其非编入版本之间可能会是二进制不兼容的,这可能会导致Java序列化问题以及其他类似问题,不过我还没有获悉有人提到过这方面的问题。

运行时字节码编入(Run-time bytecode instrumentation实体类还可以在运行时而不是构建时被编入,这就需要使用JDK 1.5或以上版本的-javaagent选项来安装Java代理,运行在JDK 1.6或更高版本之下时则是使用类的重转化(class retransformation),如果正在使用更旧版本的JDK的话,或者需要用到一些专有的方法。因此,虽然这种方法不需要修改构建过程,但却是与使用的JDK密切相关的。

运行时代理(Run-time proxies在这种情况下,类不需要被编入,不过JPA提供程序返回的对象是实际实体的代理,这些代理可以是动态代理类、由CGLIB创建的代理或者是代理集合类。这种方法虽然要求的设置最少,但却是可供JPA实现者使用的方法中最缺少透明度的一个,因此需要完全了解他们。

 

Hibernate的基于运行时代理的延迟加载

 

虽然Hibernate支持通过构建时的字节码编入来启用个别属性的延迟加载,但是大多数的Hibernate用户都会使用运行时代理,这是是默认的方法,并且大多数情况下都非常有效,所以,让我们来探讨一下Hibernate的运行时代理。

Hibernate创建两种类型的代理:

1.         在通过延迟的多对一或者一对一关联来延迟加载实体,或者通过调用EntityManager.getReference来延迟加载实体时,Hibernate使用CGLIB来创建实体类的一个子类,该子类作为到真正实体的代理。代理中的任一方法首次被调用时,从数据库中加载实体,并且把方法调用传递给被加载的实体。我的同事Maarten Winkels去年曾在他的博客上讲述了这些Hibernate代理存在的隐患。

2.         在通过一对多或者多对多关联来延迟加载实体集合时,Hibernate返回诸如PersistentSet或者PersistentMap一类的实现了PersistentCollection接口的类的一个实例,集合第一次被访问的时候,它的成员会被加载,成员实体被当作平常的类来加载,因此之前提到的Hibernate代理的隐患与这部分无关。

 

为了感受一下这里发生的事情,你可能希望在调试器中跟进一些简单的JPA代码来查看Hibernate创建的对象,如果这一机制涉及很多的话,这种做法会增进你理解。J

 

OpenJPA的运行时代码编入

 

OpenJPA提供了许多增强方法(enhancement method),因为文档中就是这样称呼它的,其中我发现运行时的字节码编入是最容易设置的。

通过调试器的步进,你能够看到OpenJPA并未创建代理,作为代替,一些额外的域出现在每一个实体类中,它们有着类似pcStateManager或者pcDetachedState这样的名字。更重要的是,你可以看到被延迟加载的实体的域都被设置为0或者null,这样的话它的内容只有在其方法被调用的时候才加载,更确切地说,被配置为延迟加载的属性只有在其getter方法被调用时才加载。

了解这一点是非常重要的,即直接访问延迟加载实体的域(或者是代表延迟加载属性的域)并不会触发对这一实体(或者域)的加载,此外,当会话(session)不再是可用的时,OpenJPA不会像Hibernate那样抛出异常,而只是让值处于非初始化状态,这在以后会引发我之前提过的NullPointException异常。

 

OpenJPAHibernate相比较

 

你可能会注意到这两种方法之间的首要区别是被代理/编入的对象:

OpenJPA对所有实体进行了编入,这意味着它能够检测到你何时访问正在使用的实体的一个延迟的引用或者集合,这样它就会返回实际的实体或者实际实体的集合。你只有在使用EntityManager.getReference延迟加载实体时,或者已把属性配置成延迟加载时,才会得到(部分)为空的实体。

就延迟引用(或者是已使用EntityManager.getReference来延迟加载的实体)这一情况来说,Hibernate使用CGLIB来代理延迟对象本身,这会带来之前提到过的代理隐患。在使用延迟集合的时候,Hibernate的处理则像OpenJPA一样是透明的。最后一点是,Hibernate并不支持使用代理的延迟加载属性。

 

如果把OpenJPA的编入和Hibernate的运行时代理相比较一下你就会发现,OpenJPA所采用的方法更加的透明化,遗憾的是,这一优势却因为OpenJPA较少强健的错误处理而有所缩小。

 

模式

 

既然现在已经知道了如何配置延迟加载以及它的工作方式,那么我们如何才能正确地使用它呢?

第一步是检查所有的关联,查看哪些应该被延迟加载而哪些应该被即时加载。我的经验法则是开始先把所有的*对一关联都保留为即时的(这是缺省情况),他们在通常情况下的查询数目的合计无论如何都很难达到一个很大的数量,如果数量真的很大的话,我可以修改这些关联。然后我会检查所有的*对多关联,任何它们中的关联如果其指向的实体总是被访问因而总是被加载的话,我就会把他们配置成即时加载的,有时候我会使用Hibernate特有的@CollectionOfElements注解来映射这种“值类型”的实体。

第二步是最重要的,为了防止所有的LazyInitializationException或者NullPointerException异常,你需要确保所有对领域对象的访问在同一个事务内部发生。当领域对象在事务完成之后被访问时,持久性上下文不能再被访问以加载持久对象,因此会导致这些问题的出现。有两种方法可用来化解这一矛盾:

1.         最纯粹的方式是在服务之前放置一个服务门面(Service Facade)(如果你喜欢的话远程门面(Remote Facade)也行),并通过传输对象(即Data Transfer Object,又名DTO)只与服务门面的客户通信。门面负责把所有适当的值从领域对象拷贝到数据传输对象中,包括引用和集合的深度拷贝。应用的事务范围应包括服务门面这样的工作模式,即给门面设定@Transactional注解,或者为它指定一个适当的@TransactionAttribute

2.         如果你正在使用MVC框架来编写模型2Model 2web应用的话,另一种被广泛使用的办法是在视图模式中使用打开的EntityManager,在Spring中可配置一个Servlet过滤器或者Web MVC拦截器,这样当请求进来的时候,过滤器或者拦截器会打开实体管理器,并保持管理器的打开状态直到请求的处理完成。这意味着在控制器(controller)和视图(view)(JSP或者其他方式)中活动的是相同的事务。或者一些纯粹控会争辩说,这种做法会导致表现层依赖于领域对象,但对于简单的web应用来说,这是一种令人感兴趣的方式。

 

第三步是启用JPA提供程序的SQL日志功能来检查应用的一些用例。这对于了解实体被访问时哪些查询被执行很有启发作用。SQL日志还能够提供性能优化的输入,因此你能够重新审视在步骤一中所作的决定并对数据库进行调整。最终延迟加载都会与性能有关,所以不要忘记了本步骤。

 

我希望本篇博客给你带来了关于延迟加载如何工作以及如何在应用中使用它的一些见解,在下一篇博客中我会深入探讨DTO这一主题和服务门面模式。不过在结束之前,我要感谢来J-Spring 2009讨论这一主题的每个人,我非常开心!看起来确实有很多人都想知道如何有效地使用JPA,因为我收到了很多问题。遗憾的是这些问题用光了我的所有时间,下次我会更加注意举着时间卡的女孩,并带上我的手表,再次感谢你们的参与!

 

附:有人知道hibernate.org发生了什么事吗?一个多星期以来网站一直在显示消息说他们为了维护所以关闭网站。

 

你可能感兴趣的:(spring,应用服务器,Hibernate,配置管理,jpa)