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

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

ERD

Entity定义

------------- 第一个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;
    ...
}

数据

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语句都没有从JEE container中发出来。而且,更要命的是,没!有!报!错!

我做了一个替换方案,用JPQL语句直接删除B(id:2),结果成功了。呵呵,到此可不算完结,不然我也不用大费周章的把这件事情记录下来。在删除
B(id:2)之后,我又尝试保存对A所做的变更,这么一保存,又出问题了。JPA报错,说是B(id:2)找不到,我晕。这又是什么情
况,B(id:2)明明已经被我删掉了,怎么在persist
A的时候JPA却要去检查一个已经被删掉的object?我确信在用JPQL删掉了B(id:2)后,我手动从A.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)之前,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.bs.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定义,)

解决方案

我这里有两种解决方案:

1、以更新A为起始点,剔除B(id:2)后,persist A。由于A上设置的Cascade,在更新A的同时,JPA会级联删除B(id:2)

2、可以强行删除B(id:2),但在对A(id:1)进行任何操作前,先去fecth一下A(id:1),也就是强行刷新一下JPA的缓存。

我个人推荐第一种方案。

时间: 2024-10-27 13:57:48

浅析JPA中EntityManager无法remove entity的问题的相关文章

EntityManager无法remove entity的问题

今天遇到一个奇怪的事情,利用EntityManager.remove(entity)方法删除一个entity时,删不掉,也不报错.后来经过多方查证,解决了这个问题. ERD Entity定义 ------------- 第一个Entity A --------------- @Entity public class A {     @Id     private Long id;          @Column(nullable = false, unique = true, length =

4、JPA table主键生成策略(在JPA中table策略是首推!!!)

用 table 来生成主键详解 它是在不影响性能情况下,通用性最强的 JPA 主键生成器.这种方法生成主键的策略可以适用于任何数据库,不必担心不同数据库不兼容造成的问题. initialValue不起作用? Hibernate 从 3.2.3 之后引入了两个新的主键生成器 TableGenerator 和 SequenceStyleGenerator.为了保持与旧版本的兼容,这两个新主键生成器在默认情况下不会被启用,而不启用新 TableGenerator 的 Hibernate 在提供 JPA

比较JPA的EntityManager接口与Hibernate的Session接口

本文对JPA的EntityManager接口与Hibernate的Session接口进行了比较,本文参考<精通JPA与Hibernate:Java对象持久化技术详解>第3版,2020年出版,作者:孙卫琴. EntityManager接口与Session接口的许多功能很相似,以下表1-1对这两个接口的方法做了对比.表1-1 对比EntityManager接口与Session接口 从以上表可以看出,EntityManager接口中的大部分方法在Sesson接口中都有对应的方法.EntityMana

Spring Data JPA 中使用Update Query更新实体类问题

在jpa中使用@Modifying 虽然事务已经能够更新,但是在循环更新的时候,执行modify语句后的查询的实体仍然是没有更新的. 执行完modifying query, EntityManager可能会包含过时的数据,因为EntityManager不会自动清除实体.只有添加clearAutomatically属性,EntityManager才会自动清除实体对象. @Modifying(clearAutomatically = true)

jpa中的一对多级联删除

jpa中如果要级联删除一方对应的多方的记录,必须在多对建立与一方之间的多对一关系,否则级联删除不了,如下: public class ChannelEntity implements java.io.Serializable { ....         private List<ChannelDayLimitEntity> chanelDayLimits; @OneToMany(mappedBy="channel", cascade=CascadeType.REMOVE,

Spring data JPA中使用Specifications动态构建查询

有时我们在查询某个实体的时候,给定的条件是不固定的,这是我们就需要动态 构建相应的查询语句,在JPA2.0中我们可以通过Criteria接口查询,JPA criteria查询.相比JPQL,其优势是类型安全,更加的面向对象.而在Spring data JPA中相应的接口是JpaSpecificationExecutor,这个接口基本是围绕着Specification接口来定义的. Specification接口中只定义了如下一个方法: Predicate toPredicate(Root<T>

浅析JDK中ServiceLoader的源码

前提 紧接着上一篇<通过源码浅析JDK中的资源加载>,ServiceLoader是SPI(Service Provider Interface)中的服务类加载的核心类,也就是,这篇文章先介绍ServiceLoader的使用方式,再分析它的源码. ServiceLoader的使用 这里先列举一个经典的例子,MySQL的Java驱动就是通过ServiceLoader加载的,先引入mysql-connector-java的依赖: <dependency> <groupId>m

浅析python中_name_=&#39;_main_&#39;

刚接触到python时,对代码中的_name_='_main_'比较疑惑,本文对其的讲解借鉴了其他博客讲述(见参考资料),希望和大家共同学习. Make a script both importable and executable 首先先看一个例子 1 #module.py 2 def main(): 3 print "we are in %s"%__name__ 4 if __name__ == '__main__': 5 main() 在这段函数中,定义main函数,当py文件被

EJB3 EntityBean中EntityManager的管理类型

EJB中EntityManager的管理方式有两种:Container-managed EntityManager和Application-managed EntityManager 即容器管理的EntityManager和应用管理的EntityManager 在EJB中,EntityManager所进行的持久化的方式与Hibernate的方式是不同的. 1.在Hibernate的同一个事务中,通过getCurrentSession获取的session对象均为同一个,保存于threadlocal