JPA FlushMode

一、问题描述

简化代码模型如下 

Device device = deviceRepo.findById(1).orElse(null);
Factory factory = new Factory();
device.setFactory(factory);
factoryRepo.findAll(PageRequest.of(0, 1));

异常信息如下

org.springframework.dao.InvalidDataAccessApiUsageException: org.hibernate.TransientPropertyValueException:
object references an unsaved transient instance - save the transient instance before flushing : fscut.entity.
device.Device.factory -> fscut.entity.factory.Factory; nested exception is java.lang.IllegalStateException: org.
hibernate.TransientPropertyValueException: object references an unsaved transient instance - save the transient
instance before flushing : fscut.entity.device.Device.factory -> fscut.entity.factory.Factory

这个异常还是比较常见的,通常在没有处理好实体之间的级联关系时执行保存会触发类似问题。

由异常信息推测是查询时触发flush,在flush时发生的异常。

Device和Factory的关系如下:

public class Device {
    private Integer factoryId;
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "factory_id", unique = true, insertable = false, updatable = false)
    @NotFound(action = NotFoundAction.IGNORE)
    private Factory factory;
}

二、断点调试异常链路

2.1SessionImpl.list

执行列表查询,查询时触发了autoFlush。

public List list(String query, QueryParameters queryParameters) throws HibernateException {
   ...
   autoFlushIfRequired( plan.getQuerySpaces() );
   ... 
}
2.2SessionImpl.autoFlushIfRequired

注册AutoFlush事件,到这里基本上可以断定是flush时刷新导致异常。

protected boolean autoFlushIfRequired(Set querySpaces) throws HibernateException {
   checkOpen();
   if ( !isTransactionInProgress() ) {
      // do not auto-flush while outside a transaction
      return false;
   }
   AutoFlushEvent event = new AutoFlushEvent( querySpaces, this );
   for ( AutoFlushEventListener listener : listeners( EventType.AUTO_FLUSH ) ) {
      listener.onAutoFlush( event );
   }
   return event.isFlushRequired();
}
2.3DefaultAutoFlushEventListener.onAutoFlush

先判断是否需要flush,然后执行flush。

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);
        }
        ... 
    }
    ... 
}
2.4DefaultAutoFlushEventListener.flushMightBeNeeded

source的flushMode与是否开启事务有关,在开启事务的情况下默认flushMode是AUTO,没开(断点显示)是MANUAL。

private boolean flushMightBeNeeded(final EventSource source) {
    return !source.getHibernateFlushMode().lessThan( FlushMode.AUTO )
            && source.getDontFlushFromFind() == 0
            && ( source.getPersistenceContext().getNumberOfManagedEntities() > 0 ||
                    source.getPersistenceContext().getCollectionEntries().size() > 0 );
}
2.5AbstractFlushingEventListener.flushEverythingToExecutions

这里的flushEverything是指session中被管理的所有实体的所有字段。

protected void flushEverythingToExecutions(FlushEvent event) throws HibernateException {
    ...
    prepareEntityFlushes( session, persistenceContext );
        ...
}
2.6AbstractFlushingEventListener.prepareEntityFlushes

在刷新前检查实体类中的级联实体,以便执行级联保存或更新。

private void prepareEntityFlushes(EventSource session, PersistenceContext persistenceContext) throws
HibernateException {
    LOG.debug( "Processing flush-time cascades" );
    final Object anything = getAnything();
    //safe from concurrent modification because of how concurrentEntries() is implemented on IdentityMap
    for ( Map.Entry<Object,EntityEntry> me : persistenceContext.reentrantSafeEntityEntries() ) {
//      for ( Map.Entry me : IdentityMap.concurrentEntries( persistenceContext.getEntityEntries() ) ) {
        EntityEntry entry = (EntityEntry) me.getValue();
        Status status = entry.getStatus();
        if ( status == Status.MANAGED || status == Status.SAVING || status == Status.READ_ONLY ) {
            cascadeOnFlush( session, entry.getPersister(), me.getKey(), anything );
        }    
    }
}
2.7AbstractFlushingEventListener.cascadeOnFlush » Cascade.cascade » CascadingActions.noCascade

检查并处理需要级联的实体,这里已经看到了抛出异常的地方。

public void noCascade(
        EventSource session,
        Object parent,
        EntityPersister persister,
        Type propertyType,
        int propertyIndex) {
    if ( propertyType.isEntityType() ) {
        Object child = persister.getPropertyValue( parent, propertyIndex );
        String childEntityName = ((EntityType) propertyType).getAssociatedEntityName( session.getFactory() );
        if ( child != null
                && !isInManagedState( child, session )
                && !(child instanceof HibernateProxy) //a proxy cannot be transient and it breaks ForeignKeys.isTransient
                && ForeignKeys.isTransient( childEntityName, child, null, session ) ) {
            String parentEntiytName = persister.getEntityName();
            String propertyName = persister.getPropertyNames()[propertyIndex];
            throw new TransientPropertyValueException(
                "object references an unsaved transient instance - save the transient instance before flushing",
                childEntityName,
                parentEntiytName,
                propertyName
            );
        }
    }
}
2.8CascadingActions.isInManagedState

可以看到从已经持久化的上下文中找不到对应的关联实体,判定为异常。

private boolean isInManagedState(Object child, EventSource session) {
    EntityEntry entry = session.getPersistenceContext().getEntry( child );
    return entry != null &&
            (
                entry.getStatus() == Status.MANAGED ||
                    entry.getStatus() == Status.READ_ONLY ||
                    entry.getStatus() == Status.SAVING
            );
}

三、小结

在查询前会检查当前上下文中管理的所有实体和实体的所有字段,包括级联实体,此时如果发现未被持久化的实体,就抛出异常。

为避免误解,先将第一小节中的代码模型简化如下:

Blog blog = blogRepo.findById(1).orElse(null);
Location location = new Location();
blog.setLocation(location);
factoryRepo.findAll(PageRequest.of(0, 1));
3.1为什么在查询前要刷新

设计如此,为了保证查询的结果是最新的。

3.2为什么是立刻刷新

其实也不是立刻刷新,是根据FlushMode来的,JPA默认的FlushMode是AUTO。 

其实这也意味着JPA不推荐开发者自行flush,flush应当隐藏在JPA背后,由JPA决定。

将代码模型改为如下,设置FlushMode为COMMIT。 

可以看到在查询执行后并未立刻抛出异常,而是在整个函数执行完成后触发异常。

Blog blog = blogRepo.findById(1).orElse(null);
Location location = new Location();
blog.setLocation(location);
factoryRepo.getEntityManager().setFlushMode(FlushModeType.COMMIT);
factoryRepo.findAll(PageRequest.of(0, 1));

所以,可以直接将代码模型简化成这样。

Blog blog = blogRepo.findById(1).orElse(null);
Location location = new Location();
blog.setLocation(location);

另外,如果是在事务没有开启或直接设置FlushMode为MANUAL的情况下,不会触发异常。 

3.2为什么要刷新所有实体

在小节2.2中可以看到AuthFlushEvent事件的source是SessionImpl本身,所以就把Session中所有实例都检查了一遍。

3.3解决问题

在3.2中提到了,如果FlushMode为MANUAL就不会触发异常,那除了设置FlushMode为MANUAL,是否还有其他方案? 有,设置CascadeType为ALL。 

这么做的含义就是告诉JPA,这个关联实体完全由父级Blog做主,其结果就是在JPA提交事务后,临时对象Location被持久化到数据库了。 

这显然不是我们期望的结果。

所以,在使用JPA(或者类似的ORM)时,不要写这样的代码。

查询就是查询,变更就是变更,不应该变更到一半就不管了。

比如这个问题的原代码实现中,device中set的Factory其实是为了传参数。

传参数就传参数,传完就要处理掉,哪怕是这样处理。

Blog blog = blogRepo.findById(1).orElse(null);
Location location = new Location();
blog.setLocation(location);
blog.setLocation(null);

当然了,最好不要这样传参数,会给阅读代码的人造成不适。

JPA在数据库和应用层之间又做了一层,这一层的内容与数据库的数据是不完全对应的,什么时候提交到数据库、什么时候刷新都是由框架去做设计的,所以不能任意操作。

另,仅使用hibernate不会有这样的问题