Contents

Hibernate implicit auto flush caused problems

hibernate auto-flush

背景:hibernate默认的auto-flush策略,会隐式的flush当前session中的改变,而提前产生了SQL,这是我们不希望的。

1、首先,所需的基础知识:hibernate三种对象hibernate-auto-flush-trigger-circumstances

2、其次,丰富hibernate的日志功能,提供我们足够的信息判断。资料

主要配置如:

1
2
3
4
logging:
  level:
    org.hibernate: TRACE
    org.hibernate.type.descriptor.sql.BasicBinder: TRACE

3、再现问题demo 有三个entity:Father Son Attachment 关系如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class Attachment {
  private String id;
}

public class Son {
  private String id;
  private String name;
  
  @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
  @JoinColumn(name = "son_id")
  private List<Attachment> attachmentList = new ArrayList<>();
}

public class Father {
  private String id;
  private String name;

  @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
  @JoinColumn(name = "father_id")
  private List<Attachment> attachmentList = new ArrayList<>();

  @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
  @JoinColumn(name = "father_id")
  private List<Son> sonList = new ArrayList<>();
}

重现测试:先将father关联的对象保存。如下 father有2个attachment,有2个son。其中第二个son有2个attachment。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@Before
public void setUp() {
  Father father = new Father();
  father.setAttachmentList(Arrays.asList(new Attachment(), new Attachment()));
  father.setSonList(
      Arrays.asList(
          Son.builder().build(),
          Son.builder()
              .attachmentList(Arrays.asList(new Attachment(), new Attachment()))
              .build()));
  fatherRepository.saveAndFlush(father);
  fatherId = father.getId();
  testEntityManager.clear();
}

测试1:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
@Test
public void clearFatherAttachmentAndThenFindAllAttachmentWillCauseImplicitAutoFlush() {
  Father father = fatherRepository.findById(fatherId).orElseThrow(AssertionError::new);
  father.setName("flush test");
  List<Attachment> attachmentList = father.getAttachmentList();
  String fatherSecondAttachmentId = attachmentList.get(0).getId();
  
  father.getAttachmentList().clear();
  
  attachmentRepository.findAllById(Collections.singletonList(fatherSecondAttachmentId));
  
  testEntityManager.clear();
  Father foundFather = fatherRepository.findById(fatherId).orElseThrow(AbstractMethodError::new);
  assertThat(foundFather.getName()).isEqualTo("flush test");
  assertThat(foundFather.getAttachmentList().size()).isEqualTo(0);
}

通过assertThat可以看出,father的name和attachment已经flush了。

而trigger auto-flush的原因是,当前的query与之前的entity修改 有重叠部分,对应测试部分:attachmentRepository.findAllById之前,father.getAttachmentList().clear()会导致update attachment set father_id = null where father_id = ?

对应到hibernate 的action也就是collectionUpdates action。所以,在执行attachment findAll时 hibernate为了保证数据一致性,会在findAll之前先flush一次。

下面会有对应的源码分析。

有的query不会trigger auto-flush的情况如下

测试2:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@Test
public void clearFatherAttachmentAndThenFindAllSonAttachmentWillNotCauseImplicitAutoFlush() {
  Father father = fatherRepository.findById(fatherId).orElseThrow(AssertionError::new);
  father.setName("flush test");
  
  father.getAttachmentList().clear();
  
  attachmentRepository.findById("test-id");
  
  testEntityManager.clear();
  Father foundFather = fatherRepository.findById(fatherId).orElseThrow(AbstractMethodError::new);
  assertThat(foundFather.getName()).isNull();
  assertThat(foundFather.getAttachmentList().size()).isEqualTo(2);
}

通过assertThat看出,father的修改没有flush。主要原因,findById对应到的是hibernate SessionImpl.java的find方法,其中并没有调用autoFlushIfRequired。所以也就不会有了implicit auto flush。下面进行源码分析。

完整注释版demo

源码分析

通过上面hibernate的文档,我们知道hibernate的auto-flush 是在 HQL/JPQL与当前session的action queue里的表有重叠时候,就会执行auto-flush。

但是测试2中 attachment.findById方法又不会触发auto-flush,可见HQL/JPQL的overlap auto-flush并不仅仅是判断有表重叠。

下面跟踪下源码主要在SessionImpl.java中。

1、先分析attachmentRepository.findAllById 触发的地方

这种情况下源码在,org/hibernate/internal/SessionImpl.java里的list方法,发现 autoFlushIfRequired( plan.getQuerySpaces() );

而且autoFlushIfRequired方法被 SessionImpl多处查询方法中调用到,后面会说。

进入autoFlushIfRequired里主要的listener.onAutoFlush( event ); 它调用的是 org/hibernate/event/internal/DefaultAutoFlushEventListener.java#onAutoFlush

这里简单展示DefaultAutoFlushEventListener.java#onAutoFlush方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public void onAutoFlush(AutoFlushEvent event) throws HibernateException {
	final EventSource source = event.getSession();
	try {
		source.getEventListenerManager().partialFlushStart();
		if ( flushMightBeNeeded(source) ) {
			// Need to get the number of collection removals before flushing to executions
			// (because flushing to executions can add collection removal actions to the action queue).
			final int oldSize = source.getActionQueue().numberOfCollectionRemovals();
			flushEverythingToExecutions(event);
			if ( flushIsReallyNeeded(event, source) ) {
				LOG.trace( "Need to execute flush" );
				// note: performExecutions() clears all collectionXxxxtion
				// collections (the collection actions) in the session
				performExecutions(source);
				postFlush(source);
				postPostFlush( source );
				if ( source.getFactory().getStatistics().isStatisticsEnabled() ) {
					source.getFactory().getStatistics().flush();
				}
			}
			else {
			........
			

可以看到有2个判断是否flush的方法:flushMightBeNeeded 和 flushIsReallyNeeded。

flushMightBeNeeded 只是简单的判断flushMode和当前session的是否有 persistence的entity,进而prepare flush所需的action,但是不会真正flush。

flushIsReallyNeeded:才是判断是否要真正flush 代码如下

1
2
3
4
private boolean flushIsReallyNeeded(AutoFlushEvent event, final EventSource source) {
	return source.getHibernateFlushMode() == FlushMode.ALWAYS
			|| source.getActionQueue().areTablesToBeUpdated( event.getQuerySpaces() );
}

而hibernate文档里说的HQL/JPQL与action queue是否有overlaps的判断 就在areTablesToBeUpdated方法里。

以上面测试1为例,attachmentRepository.findAllById在判断flushIsReallyNeeded时候,会获取当前action queue,在与当前执行的查询的表的 进行比较,如有包含 则认定有overlap。

attachmentRepository.findAllById 之前的 action queue如下:

1
2
3
// this circumstance source.getActionQueue() return:
// 1. updates that querySpaces is father (father.setName("flush test"))
// 2. collectionUpdates that querySpaces is attachment (father.getAttachmentList().clear())

所以,attachmentRepository.findAllById 要查的表是attachment 与 它之前的action queue有overlap,也就会trigger auto flush。

2、接着看 attachmentRepository.findById

它进入的地方是 SessionImpl.java里的find方法,看源码里面并没有invoke autoFlushIfRequired。

所以,官方文档没有说明的是:什么时候才会去判断 HQL/JPAL与action queue 是否有overlap。

看源码SessionImpl.java里调用autoFlushIfRequired方法的地方如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
List list(String query, QueryParameters queryParameters)

int executeUpdate(String query, QueryParameters queryParameters)

int executeNativeUpdate(
			NativeSQLQuerySpecification nativeQuerySpecification,
			QueryParameters queryParameters)

Iterator iterate(String query, QueryParameters queryParameters) 猜测针对CUD list的情况

List list(Criteria criteria)

List listCustomQuery(CustomQuery customQuery, QueryParameters queryParameters)

所以并没有find方法。

容易疑惑的点

1、如果我们自定义的repository custom method(如findByGroupId),最终进入的也是SessionImpl.java里的list方法。所以,也会进行autoFlushIfRequired判断。

2、如果我们entity的ID使用的UUID generator。在DefaultAutoFlushEventListener#onAutoFlush 中flushEverythingToExecutions时就会generate UUID identifier 并且会准备action queue,但不会执行action。

所以,这里就有个很神奇的地方了。hibernate在flushEverythingToExecutions会将当前session的修改 更新到action queue里,为了提高效率。因此,我们在flushEverythingToExecutions之前,给了hibernate一个不完整的entity,当flushEverythingToExecutions在准备action queue时候,就会更新一个insert action,此时这个insert action里field也是不完整的。所以,在执行SQL的时候,如果某个字段是NOT NULL的,就会产生SQL错。

对应的测试demo参见

3、超级大坑

一模一样的代码和上面的2,但是没有update 语句。找了很久很久╭(╯^╰)╮ 。最终,在entity的column上看到了@Column(updatable = false)。所以,不会有对应的update。

hibernate细节很多,任何小地方的配置,都会对最终行为产生影响。所以,在配置前要明确,该行为是否可行,并在以后的entity操作中 去明确这个问题。

最佳实践

先推荐一本书high-performance java persistence参考最后一段

由于我们使用的hibernate auto-flush,所以无法确定flush的时机,而且通过上面分析 hibernate list时也会隐式的执行flushEverythingToExecutions

所以最佳实践是:在执行一些DML时候,把要查询的语句 放在DML之前。即对entity的修改放到transaction方法的最后一步,尽量避免修改后的查询 引起的过早flush。