[译文]JPA的实施模式:双向关联与延迟加载之间的矛盾

来源:互联网 发布:tomcat nginx 编辑:程序博客网 时间:2024/05/16 17:53

原文:JPA implementation patterns: Bidirectional Associations vs. Lazy loading

作者:Vincent Partington

出处:http://blog.xebia.com/2009/05/25/jpa-implementation-patterns-bidirectional-associations-vs-lazy-loading/

 

两周之前,我在博客上讲述了服务门面和数据传输对象模式在JPA应用架构中的用法,本周我会移至一个较高的层面,从该角度来讨论一种非常有意思的相互作用,我发现的这一相互作用存在于双向关联的管理方式和延迟加载之间。那么,让我们现在就开始进入这一话题吧。

本篇博客假定你已熟悉我在这一系列的博客文章的首两篇中介绍的Order/OrderLine实例,如果你并未了解的话,那么请先回顾一下该例子。

考虑一下以下代码:

 

OrderLine orderLineToRemove = orderLineDao.findById(30);

orderLineToRemove.setOrder(null);

 

       此代码的目的是要解除OrderLine与其之前已关联的Order之间的关联,你可能会设想在删除orderLine对象之前先这样做(虽然也可以通过使用@PreRemove注解来让这一步骤自动完成),或者是在想把OrderLine依附到不同的Order实体时会这样做。

如果你运行这一代码的话,你会发现以下的这些实体将会被加载:

1.         id30OrderLine

2.         与该OrderLine相关联的Order,这会发生这是因为OrderLine.setOrder方法调用了Order.internalRemoveOrderLine方法来从其父Order对象中删除了OrderLine的缘故。

3.         所有其他与该Order相关联的OrderLineOrder.orderLines集在id30OrderLine被从其中删除时被加载。

 

这一过程可能要用到两个到三个查询,这取决于JPA提供程序(JPA provider),Hibernate使用三个查询;上面提到的每一行内容用到一个。OpenJPA只需要两个查询,因为它使用一个外连接(outer join)来一起加载OrderLineOrder

不过在这里令人关注的事情是所有的OrderLine实体都被加载了,如果有大量的OrderLine存在的话,这可真是一个代价非常高的操作,因为即使在你并没有对集合的真正内容感兴趣时,集合也被加载了,你为了保持双向关联的完好而正在付出高昂的代价。

到目前为止,我已经发现了三种不同的解决这一问题的方法:

1.         不要把关联设置成双向的;只保持从子对象到父对象的引用,在这种情况下,这将意味着去掉Order对象中的orderLines集。为了检索Order所拥有的OrderLine,你应该调用OrderLineDaofindOrderLinesByOrder方法。因为是检索所有的子对象,于是我们就遇到了这样的一个问题,因为有许多的子对象,这就需要编写更多特定的查询来查找所需的子对象的子集。这种方法的一个缺点是,这意味着如果不通过一个服务层的话,Order就不能够访问它的OrderLine(这是我们在随后的博客中将会解决的一个问题)。

2.         使用Hibernate特有的@LazyCollection注解来促成集合的“格外地延迟(extra lazily”加载,像这样:

 

@OneToMany(mappedBy = "order", cascade = CascadeType.PERSIST, fetch = FetchType.LAZY)

@org.hibernate.annotations.LazyCollection(org.hibernate.annotations.LazyCollectionOption.EXTRA)

public Set<OrderLine> orderLines = new HashSet<OrderLine>();

 

这一功能使得Hibernate能够处理非常大的集合,例如,在你需要集合的大小的时候,Hibernate不会加载集合中的所有元素,作为替代,它会执行一个SELECT COUNT(*) FROM……查询。不过更有意义的是:对集合的修改需要排队等候而不是直接得到作用,如果在集合被访问的时候存在任何悬而未决的修改的话,则在进行进一步的工作之前先要冲刷会话(session)。

对于size()方法来说,这一做法很有效,在你尝试在集合的元素上进行迭代的时候,该做法不会起作用(见这个已被提出两年有半的JIRA问题HHH-2087)。对集合大小的格外延迟加载至少还有两个未解决的bugHHH-1491HHH-3319,所有的这些都使我倾向于相信Hibernate的格外延迟加载功能是一个好的想法,但还未完全成熟(吧?)。

3.         Hibernate的把集合操作延迟直到真正需要它们执行的时候的这一机制激发所想到的,我对Order类进行了修改以便其能够完成类似的事情,首先,操作队列作为一个瞬态域被添加到了Order类中:

 

private transient Queue<Runnable> queuedOperations = new LinkedList<Runnable>();

 

然后修改internalAddOrderLineinternalRemoveOrderLine方法,以便它们不直接修改orderLines集,作为替代,它们会创建QueueOperation类的的适当子类的一个实例,使用将被添加或者删除的OrderLine对象来初始化该实例,然后把它放到queuesOperations队列中:

 

public void internalAddOrderLine(final OrderLine line) {

         queuedOperations.offer(new Runnable() {

                  public void run() { orderLines.add(line); }

         });

}

 

public void internalRemoveOrderLine(final OrderLine line) {

         queuedOperations.offer(new Runnable() {

                  public void run() { orderLines.remove(line); }

         });

}

 

最后修改getOrderLines方法,以便它在返回集合之前执行队列中的所有操作:

 

public Set<? extends OrderLine> getOrderLines() {

         executeQueuedOperations();

         return Collections.unmodifiableSet(orderLines);

}

 

private void executeQueuedOperations() {

         for (;;) {

                  Runnable op = queuedOperations.poll();

                  if (op == null)

                            break;

                  op.run();

         }

}

 

如果有更多的方法需要完全更新的集合的话,那么它们应该以类似的方式调用executeQueuedOperations方法。

这一做法的缺点是领域对象充斥着的 “连接管理代码”比我们在管理双向关联时已有的更多,把这一逻辑提取出来放到一个单独的类中的工作就作为一个练习留给读者。

 

当然这个问题并非只发生在你有双向关联的时候,它会在任何你操纵使用@OneToMany或者@ManyToMany映射的大型集合的时候浮现,双向关联仅仅是使得这一缘由变得不那么明显,因为你会认为你只不过是在操纵单个实体而已。

2009-5-31的补充:如果你对被映射的集合级联所有操作的话,那么就不应该使用前面描述的第3个方法,如果推迟了集合的修改操作的话,JPA提供程序将不会知晓这要添加或删除的元素,因此会把操作级联到错误的实体上去,这意味着任何被添加到已经设置了@CascadeType.PERSIST的集合中的实体将不会被持久,除非你对它们显式地调用EntityManager.persist。一个类似的说明是,当从HibernatePersistentCollection中确实地删除它们时,Hibernate特定的@org.hibernate.annotations.CascadeType.DELETE_ORPHAN注解将只删除孤儿子实体。

无论如何,现在你知道了是什么导致了性能的损失以及三种可能的解决方法,我很想知道你们是否遇到过这种问题,以及你们是如何解决该问题的。

原创粉丝点击