原文:JPA implementation patterns: Saving (detached) Entities
作者:Vincent Partington
出处:http://blog.xebia.com/2009/03/23/jpa-implementation-patterns-saving-detached-entities/
我们以数据访问对象模式作为探索JPA实施模式的开始,接着讨论了如何管理双向的关联,本周我们谈一谈这个乍一看仿佛微不足道的问题:如何保存实体。
在JPA中保存实体很简单,对吧?只要把想持久的对象传递给EntityManager.persist就可以了,在遇到可怕的“游离的实体被传给了persist方法”这一信息,或者是在用不同于Hibernate EntityManager的JPA提供程序的时候遇到类似的信息之前,这一做法似乎都很有效。
那么信息中提到的游离实体指的是什么呢?一个游离的实体(也叫做游离的对象)是指一个与持久性存储中的一个实体有着相同的ID,但已不再是持久性上下文(EntityManager会话范围)的组成部分的对象。导致这一现象的两个最常见的原因是:
用来检索该对象的EntityManager已经被关闭。
从应用的外部获得该对象,例如,作为提交表单的一部分、Hessian一类的远程协议,或者是通过BlazeDS AMF频道从Flex客户端获得等等。
关于持久的约定(见JPA 1.0规范的3.2.1节)明确指出,当被传入的对象是一个游离对象时,抛出EntityExistsException异常,而在持久性上下文被刷新或者事务被提交时,抛出任何其他的PersistenceException异常。需要注意的是,在一个事务内部两次持久同一个对象并不会产生问题,第二次调用仅是会被忽略掉,但该持久操作可能会被级联到自第一次调用以来所添加的实体的所有关联上。除了后者这一考虑外,对于已经被持久的对象来说,不需要调用EntityManager.persist,因为任何修改都会在做刷新或者提交的时候被自动保存。
saveOrUpdate和merge的比较
那些使用原始的Hibernate来工作的人可能已经非常习惯于使用Session.saveOrUpdate方法来保存实体了,saveOrUpdate方法会弄清楚对象是新的还是在这之前已经被保存过的,在第一种情况下实体被保存,在后一种情况下实体被更新。
在从Hibernate切换到JPA时,许多人都沮丧地发现这一方法不见了,而最接近的替代看起来好像是EntityManager.merge方法,但实际上彼此之间存在着很大的区别,而这些区别则带来了很重要的影响。Session.saveOrUpdate方法以及它的表亲Session.update,把传入的实体依附(attach)到持久性上下文中,而EntityManager.merge方法则是把传入对象的内容拷贝到拥有相同标识符的持久实体中,然后返回一个到该持久实体的引用,而传入的对象并未被依附到持久性上下文中。
这意味着在调用了EntityManager.merge之后,我们必须使用从该方法中返回的实体引用来代替最初传入的对象,这不同于简单地就一个对象调用EntityManager.persist(如前所述甚至是多次调用)来持久它,然后继续使用原来的对象的这样一种做法。即使是在更新的时候,Hibernate的Session.saveOrUpdate的确也像EntityManger.persist(更确切地说是Session.save)一样使用了这种好的做法,但是它有一个比较大的缺点,就是如果我们正在尝试更新一个实体,比如说重新依附(reattach)该实体,而此时另一个与它有着相同的ID的实体已经存在于持久性上下文中的话,则NonUniqueObjectException异常会被抛出。而且,要弄清楚哪段代码持久(或合并或检索)了另一个实体比弄清楚为什么我们会得到“游离的实体被传给persist方法”这样的信息还要难些。
归纳
场景 |
EntityManger.persist |
EntityManager.merge |
SessionManager.saveOrUpdate |
传入的对象从未被持久过 |
1、 把对象作为新实体添加到持久性上下文中 2、 在刷新/提交时把新实体插入到数据库中 |
1、 拷贝内容到新实体中 2、 把新实体添加到持久性上下文中 3、 在刷新/提交时把新实体插入到数据库中 4、 返回新实体 |
1、 把对象作为新的实体添加到持久性上下文中 2、 在刷新/提交时把新实体插入到数据库中 |
对象之前已被持久,但没有被加载到该持久性上下文中 |
1、抛出EntityExistsException异常(或在刷新/提交时抛出PersistenceException异常) |
1、 加载已存在的实体 2、 拷贝对象的内容到被加载实体中 3、 在刷新/提交时更新被加载实体到数据库中 4、 返回被加载实体 |
1、 把对象添加到持久性上下文中 2、 在刷新/提交时把被加载实体更新到数据库中 |
对象之前已被持久且已经被加载到该持久性上下文中 |
1、抛出EntityExistsException异常(或在刷新/提交时抛出PersistenceException异常) |
1、 拷贝对象的状态到被加载实体中 2、 在刷新/提交时更新被加载实体到数据库中 3、 返回被加载实体 |
1、抛出NonUniqueObjectException异常 |
通过查看该表就可以开始理解,为什么saveOrUpdate方法不会成为JPA规范的一部分,以及为什么JSR的成员反而会选择merge方法。顺便说一下,你可以在Stevi Deter关于该主题的博客中发现其从不同的角度来讨论这一saveOrUpdate和merge之间的比较问题。
合并的问题
在继续我们的话题之前,我们需要讨论一下EntityManger.merge工作方式中存在的缺陷,那就是该工作方式很容易破环双向的关联。考虑一下使用本系列文章中的上一篇博客提到的Order和OrderLine类作为例子,如果是从web前端(或从Hessian客户端,或是flex应用等)接收到了一个已被更新的OrderLine对象的话,其order域可能被设置为null。如果该对象随后与已加载的实体合并的话,该实体的order域就会被设成null,但它不会被从其曾指向的Order的orderLines集合中去掉,从而破坏了Order的orderLines集合中的每个元素的order域都被设置成指回到该Order的这样一种固定做法。
在这种情况下,或是在其他简单地因EnttiyManager.merge拷贝对象的内容到被加载的实体中而导致问题的情况下,我们可以求助于自助合并模式(DIY merge pattern),通过调用EntityManger.find而不是调用EntityManager.merge来查找已存在的实体,然后由我们自己来拷贝内容,如果EntityManager.find返回null的话,我们可以决定是持久接收到的对象还是抛出异常。适用于Order类的这一模式可以这样来实现:
Order existingOrder = dao.findById(receivedOrder.getId());
if(existingOrder == null) {
dao.persist(receivedOrder);
} else {
existingOrder.setCustomerName(receivedOrder.getCustomerName());
existingOrder.setDate(receivedOrder.getDate());
}
模式
那么,所有的这些情况是否会让我们无从下手呢?我会坚持这样的一些经验法则:
当且只有当创建新的实体时(最好是在这种情况下),才调用EntityManager.persist来持久它,当我们把领域的访问对象视作集合时,这样做最合理。我把这种做法称作新增持久模式(persist-on-new pattern)。
在更新现有的实体时,不调用任何的EntityManager方法,JPA提供程序会在刷新或者提交时自动地更新数据库。
在从应用的外部接收到已存在的简单实体(没有引用其他实体的实体)的更新版本并希望保存新的内容时,调用EntityManager.merge把这些内容拷贝到持久性上下文中。鉴于合并的工作方式,如果不能确定对象是否已经被持久过的话,也可以这样做。
当我们在合并的过程中需要更多的控制权时,使用自助合并模式(DIY merge pattern)。
我希望这篇博客为你提供了一些如何保存实体以及如何处理游离实体的指引,在讨论数据传输对象(Data Transfer Object)的时候我们会重新提到游离实体,不过下周我们会先处理一些常见的实体检索模式,在此期间欢迎你提出意见,你会用到哪些JPA模式呢?