JPA对于维护双边关系操作其实已经有明确说明,应该从parent一端来维护关系。
今天遇到一个奇怪的事情,利用EntityManager.remove(entity)方法删除一个entity时,删不掉,也不报错。后来经过多方查证,解决了这个问题。
ERD
<a href="http://s3.51cto.com/wyfs02/M00/7F/49/wKioL1cY4PXDaMjhAAA3p6ZllUk448.png" target="_blank"></a>
Entity定义
<code>------------- 第一个Entity A ---------------</code>
<code>@Entity</code>
<code>public</code> <code>class</code> <code>A {</code>
<code> </code><code>@Id</code>
<code> </code><code>private</code> <code>Long id;</code>
<code> </code>
<code> </code><code>@Column</code><code>(nullable = </code><code>false</code><code>, unique = </code><code>true</code><code>, length = </code><code>60</code><code>)</code>
<code> </code><code>private</code> <code>String internalKey;</code>
<code> </code><code>@OneToMany</code><code>(mappedBy = </code><code>"b"</code><code>, cascade = CascadeType.ALL, orphanRemoval = </code><code>true</code><code>)</code>
<code> </code><code>private</code> <code>List<B> bs = </code><code>new</code> <code>ArrayList<>();</code>
<code> </code><code>...</code>
<code>}</code>
<code>------------- 第二个Entity B ---------------</code>
<code>public</code> <code>class</code> <code>B {</code>
<code> </code><code>@ManyToOne</code>
<code> </code><code>@JoinColumn</code><code>(name = </code><code>"A_internalKey"</code><code>, referencedColumnName = </code><code>"internalKey"</code><code>)</code>
<code> </code><code>private</code> <code>A a;</code>
数据
<code>Table A:</code>
<code>id internalKey</code>
<code>-------- -------------</code>
<code>1 a1</code>
<code>Table B:</code>
<code>id A_internalKey</code>
<code>2 a1</code>
问题
按照多年SQL脚本操作数据的经验,直接从B表中删除记录B(id:2)是可行的。A表上不存在任何对B表的外键引用,所以可以直接删除B表上的数据,数据库管理系统不会不开心。但是,使用JPA中EntityManager的remove(entity)方法来删除B(id:2)时,问题发生了。remove根本删不掉B(id:2)记录,连SQL语句都没有从程序中中发出来。而且,更要命的是,没!有!报!错!
我做了一个替换方案,用JPQL语句直接删除B(id:2),结果成功了。呵呵,到此可不算完结,不然我也不用大费周章的把这件事情记录下来。在删除B(id:2)之后,我尝试保存对A所做的变更,这么一保存,又出问题了。JPA报错,说是B(id:2)找不到,我晕。这又是什么情 况?B(id:2)明明已经被我删掉了,怎么在persist A的时候JPA却要去检查一个已经被删掉的object?我确信在用JPQL删掉了B(id:2)后,我手动从A(id:1).bs集合中剔除了B(id:2),为啥 这个B(id:2)阴魂不散呢?
分析
在翻阅了一些文档后,我隐约意识到,问题应该与entity的几种状态(尤其是detached状态)以及O/R Mapping框架中的缓存有关。说白了,就是程序哪里产生数据不一致了。一般,之所以产生这种不一致问题可能与受管对象的状态、生命周期或是访问范围等有关。那么,代入JPA中考虑,对应的应该是Entity的生命周期或访问机制(缓存机制)。
继续深究发现,这个issue是由于多个方面综合作用下产生的。
首先,问题的最关键之处:A与B的bidirectional OneToMany(双向一对多关系)。
这其实很好理解,就像Java中的垃圾回收机制一样,被用到的Object不会被GC。同理,被引用的child,也就是这个B(id:2),一直被A(id:1)引用着,JPA怎么会让你把他干掉?!前面未曾提及,在删除B(id:2)之前,A(id:1)被JPA读取过。当我试图删除B(id:2)时A(id:1)应该还在JPA的缓存里待着。根据Entity上的annotation标注,A(id:1)应该同时保有B(id:1)、B(id:2)的引用(就是那个List<B> bs集合中的两个元素)。JPA的remove出于某种机制(我猜的),并不会让你把被引用的B(id:2)删掉。
当然,如果你执意要删除,那么可以用EntityManager.createQuery("DELETE FROM B WHERE B.id=2").executeUpdate();来强行删除指定的数据库记录。因为createQuery().executeUpdate()会向DBMS发送指定的sql,如果有报错,异常会由DBMS通过底层JDBC报给JPA框架最终通过EntityManager冒出来。我就是用了这种方法强行把B(id:2)给干掉了。不报错,说明直接删除B(id:2)记录符合DBMS的约束要求。
接下来就是因素二:缓存与实际数据库不一致
解决方案
我这里有两种解决方案:
方案1:
以更新A(id:1)为起始点,剔除B(id:2)后,persist A(id:1)。由于A上设置的Cascade=CascadeType.ALL(或至少是个CascadeType.REMOVE),在persist A(id:1)的同时,JPA会级联删除B(id:2)
方案2:
用JPQL强行删除B(id:2),但在对A(id:1)进行任何操作前,先去fecth一下A(id:1)(要用find()方法,不能用getReference()方法),也就是强行刷新一下JPA的缓存。
个人推荐第一种方案。
参考资料:
2. EJB3 in Action (ISBN 1-933988-34-7)
本文转自 rickqin 51CTO博客,原文链接:http://blog.51cto.com/rickqin/1766494