浅析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


相关文章
|
5月前
|
Java API 数据库
Java一分钟之-JPA注解:@Entity, @Table, @Id等
【6月更文挑战第14天】Java Persistence API (JPA) 是Java开发中的ORM框架,通过注解简化数据访问层。本文介绍了三个核心注解:`@Entity`标识实体类,`@Table`自定义表名,`@Id`定义主键。易错点包括忘记添加`@Entity`、未正确设置主键。建议使用`@GeneratedValue`和`@Column`细化主键策略和字段映射。正确理解和应用这些注解能提高开发效率和代码质量。
249 3
|
3月前
|
Java 数据库连接 数据库
save() 和 saveOrUpdate() 方法有什么区别?
【8月更文挑战第21天】
147 0
|
3月前
|
存储 SQL Java
|
6月前
|
Java 数据库连接
Hibernate query.list()
在Hibernate中,用hql语句查询实体类,采用List方法的返回结果为一个List,该List中封装的对象分为以下三种情况。
|
6月前
|
SQL 缓存 Java
JPA - EntityManager详解
JPA - EntityManager详解
224 0
org.apache.ibatis.builder.IncompleteElementException: Could not find result map com.dao.IndexDao.Use
org.apache.ibatis.builder.IncompleteElementException: Could not find result map com.dao.IndexDao.Use
99 0
|
Java 数据库连接
Table(name=“xx“)与Entity(name=“xx“) 问题
Table(name=“xx“)与Entity(name=“xx“) 问题
|
Java 数据库连接 API
@Entity 里面的 JPA 注解
关于注解Entity的JPA实现方式
|
测试技术
SpringDataJPA之JpaRepository和JpaSpecificationExecutor接口
JpaRepository 接口是我们开发时使用的最多的接口。其特点是可以帮助我们将其他接口的方法的返回值做适配处理。可以使得我们在开发时更方便的使用这些方法。