原文:JPA implementation patterns: Removing entities
作者:Vincent Partington
出处:http://blog.xebia.com/2009/04/09/jpa-implementation-patterns-removing-entities/
过去几周以来我一直在谈论自己在编写JPA应用时发现的实施模式,上两篇博客分别涉及了保存实体和检索实体方面的内容,不过在真正完全实现实体的时候,我猜你是希望也能够对它们进行删除操作的,所以,删除就是本篇博客的主题。
就像检索实体一样,删除一个实体是很简单的,实际上,所有需要做的就是把实体传给EntityManager.remove方法(当然实际上是调用了DAO的一个删除方法,该方法转而调用EntityManager.remove),以便在事务提交的时候从数据库中删除该实体。通常情况下,这一切就是这么回事,不过,事情会因为你正在使用关联(无论它们是双向的与否)而变得更有意思起来。
删除作为关联的组成部分的实体
考虑一下之前我们讨论过的Order和OrderLine类这一例子,比方说,我们要从Order中删除OrderLine,我们以这种简单的方式来完成这件事:
orderLineDao.remove(lineToDelete);
这一代码是有问题的,当你告诉实体管理器删除该实体的时候,实体管理器并不会自动地从指向该实体的所有关联中删除它,就像JPA不会自动地管理双向关联一样,就例子中的情况而言,关联指的应该是OrderLine.order属性指向的Order对象中的orderLines集合。假如我要把这一描述写成一个测试失败的JUnit用例的话,那它应该是这样的:
OrderLine orderLineToRemove = orderLineDao.findById(someOrderLineId);
Order parentOrder = orderLineToRemove.getOrder();
int sizeBeforeRemoval = parentOrder.getOrderLines().size();
orderLineDao.remove(orderLineToRemove);
assertEquals(sizeBeforeRemoval - 1, parentOrder.getOrderLines().size());
影响
这一测试用例的失败有两处不易察觉也因此是比较恶劣的影响:
在删除OrderLine之后,任何使用Order对象的代码相反仍然会看到被删除的OrderLine,只有在提交该事务,然后开始一个新的事务并在新事务中重新加载Order之后,被删除的OrderLine才不会在Order.orderLines集中出现。在简单的情况下我们不会遇到这一问题,但在事情越来越复杂的时候,我们可能会被这些“僵尸”OrderLines的出现吓一跳。
当持久操作从Order对象级联到Order.orderLines关联并且其包含的Order对象没有在同一事务中被删除时,我们将会收到诸如“已删除的实体被传给了持久方法”一类的错误。不同于我们在之前的博客中谈到的“游离实体被传给了持久方法”这一错误,当前的这一错误是由Order对象拥有到已被删除的OrderLine对象的引用这一事实导致的,JPA提供程序在把持久性上下文中的实体刷新到数据库中时发现了该引用,造成其企图持久已经被删除的实体,从而导致了错误的出现。
简单的解决方案
为了解决这一问题,我们还必须从Order.orderlines集中删除OrderLine,这听起来非常熟悉……实际上,在管理双向关联时,我们也需要确保关联的两端保持一致的状态,这意味着我们可以重用在这方面用过的模式,通过把对orderLineToRemove.setOrder(null)的调用加入到测试中来使其成功运行:
OrderLine orderLineToRemove = orderLineDao.findById(someOrderLineId);
Order parentOrder = orderLineToRemove.getOrder();
int sizeBeforeRemoval = parentOrder.getOrderLines().size();
orderLineToRemove.setOrder(null);
orderLineDao.remove(orderLineToRemove);
assertEquals(sizeBeforeRemoval - 1, parentOrder.getOrderLines().size());
模式
不过类似这样的做法会使代码变得脆弱,因为其依赖于领域对象的用户调用正确的方法,DAO应该承担这一工作。一个非常棒的解决这一问题的方法是像这样子来使用@PreRemove这一实体生命周期的钩子:
@Entity
public class OrderLine {
[...]
@PreRemove
public void preRemove() {
setOrder(null);
}
}
现在我们只要调用OrderLineDao.remove()来除去不想要的OrderLine对象就可以了。
本篇博客最初是建议引入由DAO来调用的带有preRemove方法的HasPreRemove接口,不过Sakuraba建议说,@PreRemove注解正好是我们这里要用到的东西,再次感谢你,Sakuraba!
删除孤儿
但是你会问,如果我们只是要从Order.orderLines集中删除OrderLine的话,那事情会怎么样呢?
Order parentOrder = orderLineToRemove.getOrder();
parentOrder.removeOrderLine(orderLineToRemove);
OrderLine确实已是从Order.orderlines集中删除,并且不仅仅只是在该事务中,如果我们在新的事务中再次检索该Order对象的话,被删除的OrderLine也不会出现。但是如果我们查看一下数据库,就会看到该OrderLine还在,它只是把OrderLine.order字段设置成null罢了,我们在这里看到的是一个“已成孤儿的”集合元素,解决这一问题有两种方法:
如我们之前讨论的那样,显式地删除OrderLine对象。
如果正在使用Hibernate作为JPA提供程序的话,可以让Hibernate自动地删除这些孤儿,对你希望Hinernate这样做的@OneToMany注解来说,在其下面添加值为org.hibernate.annotations.CascadeType.DELETE_ORPHAN的org.hibernate.annotations.Cascade注解,可参阅Hinernate文档中的例子。
虽然第二种解决方案是供应商特定的,但确实有它的好处在,那就是代码不需要每次从集合中删除实体的时候都要调用DAO的删除方法,不过为了明确地表明你是在使用供应商特定的扩展,你应该使用完整的包名称(既然Java Persistence with Hibernate一书也这样建议)来引用这些注解:
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
@org.hibernate.annotations.Cascade(org.hibernate.annotations.CascadeType.DELETE_ORPHAN)
private Set<OrderLine> orderLines = new HashSet<OrderLine>();
需要注意的是,CascadeType.All并未包含DELETE_ORPHAN这一级联类型,这就是为什在这里的例子中要显式地设定它。
因此,我们可以得出结论,删除实体是件简单的工作,除非你正在处理关联,在这种情况下需要特别小心,要确保从所有指向实体的对象中删除该实体,并且同时要从数据库中删除它。我们下一篇博客中再见!在此期间,请在下面的评论部分中留下任何想说的话。
附:如果下周你会参加J-Spring的话,欢迎来参加我关于这一主题的谈论,时间是14:25到15:15(不过是在荷兰举行),或者可在举行会议的地方找我,一起来探讨一下JPA,我可能会在Xebia的展位附近出现。J