大概模拟下表结构情况:

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;
...
}

遇到问题

正常看来,这样的关联关系,直接删除B表是可行的,A表上不存在任何对B表的外键引用,所以可以直接删除B表上的数据。
但是使用spring data jpa提供的CrudRepository中的delete方法无法删除,既没有sql输出,也没有任何异常提示,而且接下来的代码会继续执行。
尝试直接用jpql 或者 直接用 jdbcTemplate 来执行删除,是可以直接删掉的。 但是删除后,再去修改A对象,JPA就会报错B(id=xxx)找不到,使用sql已经将B(id=xxx)删除了,但去找A的时候还是去查询他。

问题分析

CrudRepository默认会使用SimpleJpaRepository的实现.直接是调用的jpa EntityManager:

1
2
3
4
5
@Transactional
public void delete(T entity) {
Assert.notNull(entity, "The entity must not be null!");
em.remove(em.contains(entity) ? entity : em.merge(entity));
}

查阅jpa的一些技术文档,大致了解到,于entity的几种状态以及O/R Mapping框架中的缓存有关系。一般,之所以产生这种不一致问题可能与受管对象的状态、生命周期或是访问范围等有关。那么,代入JPA中考虑,对应的应该是Entity的生命周期或访问机制()缓存机制).

首先,问题的最关键之处,A和B的双向一对多的关系。
问题可以这样理解,在删除B对象之前,我首先去查了一个A,A的List中包含的有需要删除的B这个对象。就像操作系统中打开占用一样,打开一个文件后就没办法去删除他的了。需要删除的B对象一直被A占用着,JPA不会去干掉他。
A应该同时保有List中的元素,Jpa的remove出于某种保护,并不会让你把被引用的B删掉。
当然,如果要强制删除,可以直接用jpqlsql来强制删除,但是这样就会导致缓存于实际数据库不一致。

缓存于数据库不一致

在删除B之前,A带着对List<B>的引用一直呆在缓存中,当被引用的某个B被强制删除后,并没有任何的代码可以去更新缓存中的A引用的List<B>
接下来要persist A的改动,虽然后面手动做了remove B,但是A已经处于detached状态,对一个已经处于游离状态的object进行的改动,不会映射到对应的Entity上,换句话说,不论我怎样操作这个游离的对象,在JPA缓存中的A不会被更新.

而且尝试着去persist游离的对象时,Jpa通过游离的A和缓存中的A做比较,认为这两个对象的idhashcode一样,所以jpa从缓存中找到了A,尝试再persist一次,jpa直接报错,并且找不到被删除的B.

解决方案

  1. 以更新A为起始点,取出A的List后 去除你要删除的对象,persist A,由于A上设置的Cascade,在更新A的同时,Jpa会级联删除
  2. 强制删除B,但在对A进行任何操作前,都先去fetch一下A,也就是强制刷新一下jpa缓存.

参考资料