浅析JPA中EntityManager无法remove entity的问题

简介:

JPA对于维护双边关系操作其实已经有明确说明,应该从parent一端来维护关系。

今天遇到一个奇怪的事情,利用EntityManager.remove(entity)方法删除一个entity时,删不掉,也不报错。后来经过多方查证,解决了这个问题。


ERD

wKioL1cY4PXDaMjhAAA3p6ZllUk448.png


Entity定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
------------- 第一个Entity A ---------------
@Entity
public  class  A {
     @Id
     private  Long id;
     
     @Column (nullable =  false , unique =  true , length =  60 )
     private  String internalKey;
     
     @OneToMany (mappedBy =  "b" , cascade = CascadeType.ALL, orphanRemoval =  true )
     private  List<B> bs =  new  ArrayList<>();
     ...
}
------------- 第二个Entity B ---------------
@Entity
public  class  B {
     @Id
     private  Long id;
     
     @ManyToOne
     @JoinColumn (name =  "A_internalKey" , referencedColumnName =  "internalKey" )
     private  A a;
     ...
}


数据

1
2
3
4
5
6
7
8
9
10
Table A:
id       internalKey
-------- -------------
1        a1
 
Table B:
id       A_internalKey
-------- -------------
1        a1
2        a1


问题

按照多年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的约束要求。

接下来就是因素二:缓存与实际数据库不一致

看上面的那段标红的内容。是不是想到了什么?在删除B(id:2)之前,A(id:1)带着对B(id:1)B(id:2)的引用一直待在缓存里。当B(id:2)被我用JPQL强行删除之后,并没有任何代码去更新缓存里的A(id:1),所以A(id:1)上应该还有B(id:2)的引用。接下来,要persist A(id:1)的改动。虽然我后来手动做了A(id:1).getBs().remove(B(id:2))操作(从bs集合中剔除了B(id:2)的引用),但很遗憾,A(id:1)已经处于detached状态(即游离状态,姑且把已经处于游离状态的A(id:1)叫做a(id:1))。对一个已经处于游离状态的object进行的改动,不会映射到对应的Entity上,换句话说,不论我怎样操作a(id:1),在JPA缓存中的A(id:1)不会被更新。而且,戏剧性的一幕发生了,当我尝试着去persist一个游离对象a(id:1)时,JPA通过a(id:1).equals(A(id:1))的比较,认为a(id:1) == A(id:1),因为两个对象的id一样,hashcode一样,所以JPA从缓存中找到A(id:1),试图persist,接下来的事情也就不用我说了,JPA报错,并提示我找不到B(id:2)。(什么?为什么会去找B(id:2)?哦,那是因为A上的Cascade定义CascadeType.ALL的缘故。详情请参考Cascade相关信息)


解决方案

我这里有两种解决方案:

方案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的缓存。


个人推荐第一种方案。


参考资料:

1. ObjectDB website(http://www.objectdb.com/java/jpa/getting/started)

2. EJB3 in Action (ISBN 1-933988-34-7)


本文转自 rickqin 51CTO博客,原文链接:http://blog.51cto.com/rickqin/1766494


相关文章
|
8天前
|
Java 数据库连接
错误org.hibernate.AnnotationException: No identifier specified for entity
请根据你的实际情况,将实体类中的字段和注解进行适当的调整,以确保每个实体类都有一个明确定义的标识符(主键)。 买CN2云服务器,免备案服务器,高防服务器,就选蓝易云。百度搜索:蓝易云
7 0
|
4月前
|
SQL 缓存 Java
JPA - EntityManager详解
JPA - EntityManager详解
49 0
|
Java 数据库连接 API
@Entity 里面的 JPA 注解
关于注解Entity的JPA实现方式
How to update BOL entity property value via ABAP code
How to update BOL entity property value via ABAP code
How to update BOL entity property value via ABAP code