一招解决JPA(Hibernate)不能Update Selective的问题

1,849 阅读5分钟

一. 背景

众所周知, Hibernate不能在Update的时候将某几个字段设置为null.

就算设置了@DynamicUpdate注解, 也只能在数据库字段和内存字段相等的情况下才会不触发对应字段的更新.

但实际场景下, 前端按需传入需要更新的字段, 把不需要进行更新的字段设置为NULL是非常常见的情况, 现状传入NULL的时候, 会导致数据库层面NULL约束报错, 不加约束的情况下, 甚至会将数据写成NULL.

二. 思路

第一个思路就是去看Hibernate的merge方法的大致链路.

在调用SessionImpl.merge()进行持久化后, 会触发fireMerge(EventType.MERGE). 随后会执行DefaultMergeEventListener.onMerge()方法, 当中会使用子类的JpaMergeEventListener.copyValues()方法, 来进行数据库到内存持久化对象之间的值复制.

JpaMergeEventListener, 用于在Merge行为发生时进行一些额外的处理操作. 因此想到是不是可以从此处入手去修改值复制的逻辑, 将Null的字段取数据库当中的值来替换内存值, 在后续流程中结合@DynamicUpdate注解, 达到不触发这些字段的更新的目的.

随后就想到去观察JpaMergeEventListener的注入流程, 找到注册入口, 问题就解决了一大半.

三. 解法

第一步: 寻找注册点 - JPA整合器JpaIntegrator

// JpaMergeEventListener的注册流程: JpaIntegrator
public class JpaIntegrator implements Integrator {
   private ListenerFactory jpaListenerFactory;
   private CallbackBuilder callbackBuilder;
   private CallbackRegistryImpl callbackRegistry;

   private static final DuplicationStrategy JPA_DUPLICATION_STRATEGY = new JPADuplicationStrategy();

   /**
    * Perform integration.
    *
    * @param metadata The "compiled" representation of the mapping information
    * @param sessionFactory The session factory being created
    * @param serviceRegistry The session factory's service registry
    */
   public void integrate(
         Metadata metadata,
         SessionFactoryImplementor sessionFactory,
         SessionFactoryServiceRegistry serviceRegistry) {

      // then prepare listeners
      final EventListenerRegistry eventListenerRegistry = serviceRegistry.getService( EventListenerRegistry.class );

      eventListenerRegistry.addDuplicationStrategy( JPA_DUPLICATION_STRATEGY );

      // op listeners
      eventListenerRegistry.setListeners( EventType.AUTO_FLUSH, JpaAutoFlushEventListener.INSTANCE );
      eventListenerRegistry.setListeners( EventType.DELETE, new JpaDeleteEventListener() );
      eventListenerRegistry.setListeners( EventType.FLUSH_ENTITY, new JpaFlushEntityEventListener() );
      eventListenerRegistry.setListeners( EventType.FLUSH, JpaFlushEventListener.INSTANCE );
      
      // **在这里, JpaMergeEventListener得到注入, 因此要替换它.**
      eventListenerRegistry.setListeners( EventType.MERGE, new JpaMergeEventListener() );
      
      eventListenerRegistry.setListeners( EventType.PERSIST, new JpaPersistEventListener() );
      eventListenerRegistry.setListeners( EventType.PERSIST_ONFLUSH, new JpaPersistOnFlushEventListener() );
      eventListenerRegistry.setListeners( EventType.SAVE, new JpaSaveEventListener() );
      eventListenerRegistry.setListeners( EventType.SAVE_UPDATE, new JpaSaveOrUpdateEventListener() );

      // post op listeners
      eventListenerRegistry.prependListeners( EventType.POST_DELETE, new JpaPostDeleteEventListener() );
      eventListenerRegistry.prependListeners( EventType.POST_INSERT, new JpaPostInsertEventListener() );
      eventListenerRegistry.prependListeners( EventType.POST_LOAD, new JpaPostLoadEventListener() );
      eventListenerRegistry.prependListeners( EventType.POST_UPDATE, new JpaPostUpdateEventListener() );
      
      // 省略其他流程 ....

找到了注册入口, 随后就可以进行替换类的编写.

第二步: 编写替换类 IgnoreNullMergeEventListener

/**
 * 修改Merge行为, null的时候从数据库持久化对象复制过来. 参考JpaMergeEventListener对象
 *
 * @see org.hibernate.jpa.event.internal.core.JpaMergeEventListener
 * @see org.hibernate.jpa.event.spi.JpaIntegrator#integrate(Metadata, SessionFactoryImplementor, SessionFactoryServiceRegistry)
 *
 * @author Rongzhen.Yan
 */
public class IgnoreNullMergeEventListener extends DefaultMergeEventListener {

    private static final long serialVersionUID = 1039333630147106744L;

    private CallbackRegistry callbackRegistry;

    public void injectCallbackRegistry(CallbackRegistry callbackRegistry) {
        this.callbackRegistry = callbackRegistry;
    }

    public IgnoreNullMergeEventListener() {
        super();
    }

    public IgnoreNullMergeEventListener(CallbackRegistry callbackRegistry) {
        super();
        this.callbackRegistry = callbackRegistry;
    }

    @Override
    protected Serializable saveWithRequestedId(
            Object entity,
            Serializable requestedId,
            String entityName,
            Object anything,
            EventSource source) {
        callbackRegistry.preCreate( entity );
        return super.saveWithRequestedId( entity, requestedId, entityName, anything, source );
    }

    @Override
    protected Serializable saveWithGeneratedId(
            Object entity,
            String entityName,
            Object anything,
            EventSource source,
            boolean requiresImmediateIdAccess) {
        callbackRegistry.preCreate( entity );
        return super.saveWithGeneratedId( entity, entityName, anything, source, requiresImmediateIdAccess );
    }

    @Override
    protected void copyValues(
            EntityPersister persister,
            Object entity,
            Object target,
            SessionImplementor source,
            Map copyCache) {

        Object[] original = persister.getPropertyValues(entity);
        Object[] targets = persister.getPropertyValues(target);
        Type[] types = persister.getPropertyTypes();
        Object[] copied = new Object[original.length];
        for ( int i = 0; i < types.length; i++ ) {
            if ( original[i] == null    // Copy when null, 其他均来源于源码
                    || original[i] == LazyPropertyInitializer.UNFETCHED_PROPERTY
                    || original[i] == PropertyAccessStrategyBackRefImpl.UNKNOWN ) {
                copied[i] = targets[i];
            }
            else if ( targets[i] == LazyPropertyInitializer.UNFETCHED_PROPERTY ) {
                // Should be no need to check for target[i] == PropertyAccessStrategyBackRefImpl.UNKNOWN
                // because PropertyAccessStrategyBackRefImpl.get( object ) returns
                // PropertyAccessStrategyBackRefImpl.UNKNOWN, so target[i] == original[i].
                //
                // We know from above that original[i] != LazyPropertyInitializer.UNFETCHED_PROPERTY &&
                // original[i] != PropertyAccessStrategyBackRefImpl.UNKNOWN;
                // This is a case where the entity being merged has a lazy property
                // that has been initialized. Copy the initialized value from original.
                if ( types[i].isMutable() ) {
                    copied[i] = types[i].deepCopy( original[i], source.getFactory() );
                }
                else {
                    copied[i] = original[i];
                }
            }
            else {
                copied[i] = types[i].replace( original[i], targets[i], source, target, copyCache );
            }
        }
        persister.setPropertyValues(target, copied);
    }
}

这里的核心变更就是在于original[i] == null时也进行值的替换.

第三步: 注册

/**
 * @author Rongzhen.Yan
 */
@Configuration
public class HibernateListenerConfigurer {

    @PersistenceUnit
    private EntityManagerFactory entityManagerFactory;

    @PostConstruct
    protected void init() {
        // 获取Session工厂
        SessionFactoryImpl sessionFactory = entityManagerFactory.unwrap(SessionFactoryImpl.class);
        // 获取事件监听注册器
        EventListenerRegistry registry = sessionFactory.getServiceRegistry().getService(EventListenerRegistry.class);
        // 实例化替换类
        IgnoreNullMergeEventListener newListener = new IgnoreNullMergeEventListener();
        // 清除老的监听器, 避免重复监听
        registry.getEventListenerGroup(EventType.MERGE).clear();
        // 加入新的监听器
        registry.getEventListenerGroup(EventType.MERGE).prependListener(newListener);
    }
}

第四步: 优化

此时发现null值并没有被替换, 但发现了另外一个比较大的问题, 由于项目当中有使用到AuditingEntityListener来进行审计功能, 而

AuditingEntityListener类源码如下:

@Configurable
public class AuditingEntityListener {

   private ObjectFactory<AuditingHandler> handler;

   /**
    * Configures the {@link AuditingHandler} to be used to set the current auditor on the domain types touched.
    * 
    * @param auditingHandler must not be {@literal null}.
    */
   public void setAuditingHandler(ObjectFactory<AuditingHandler> auditingHandler) {

      Assert.notNull(auditingHandler, "AuditingHandler must not be null!");
      this.handler = auditingHandler;
   }

   /**
    * Sets modification and creation date and auditor on the target object in case it implements {@link Auditable} on
    * persist events.
    * 
    * @param target
    */
   @PrePersist
   public void touchForCreate(Object target) {
      if (handler != null) {
         handler.getObject().markCreated(target);
      }
   }

   /**
    * Sets modification and creation date and auditor on the target object in case it implements {@link Auditable} on
    * update events.
    * 
    * @param target
    */
   @PreUpdate
   public void touchForUpdate(Object target) {
      if (handler != null) {
         handler.getObject().markModified(target);
      }
   }
}

这里的核心就是通过@PreUpdate, @PrePersist等注解来实现生命周期的监听. 那么是不是代表会影响所有自定义的生命周期的监听器呢? 答案是肯定的, 直接粗暴替换会影响所有通过持久化对象当中 @EntityListeners 来指定的自定义监听器.

盲猜JpaMergeEventListener和生命周期监听函数有关系, 随后的思路就是去看@PreUpdate, @PrePersist, @PostUpdate, @PostPersist等等生命周期注解函数的执行时机是什么, 看看能不能把这些执行时机原封不动的迁移到新的替换类当中.

此时重新回看一下上文提到的JpaIntegrator

// JpaMergeEventListener的注册流程: JpaIntegrator
public class JpaIntegrator implements Integrator {
   private ListenerFactory jpaListenerFactory;
   private CallbackBuilder callbackBuilder;
   private CallbackRegistryImpl callbackRegistry;

   private static final DuplicationStrategy JPA_DUPLICATION_STRATEGY = new JPADuplicationStrategy();

   /**
    * Perform integration.
    *
    * @param metadata The "compiled" representation of the mapping information
    * @param sessionFactory The session factory being created
    * @param serviceRegistry The session factory's service registry
    */
   public void integrate(
         Metadata metadata,
         SessionFactoryImplementor sessionFactory,
         SessionFactoryServiceRegistry serviceRegistry) {

      // first, register the JPA-specific persist cascade style
      CascadeStyles.registerCascadeStyle(
            "persist",
            new PersistCascadeStyle()
      );


      // then prepare listeners
      final EventListenerRegistry eventListenerRegistry = serviceRegistry.getService( EventListenerRegistry.class );

      eventListenerRegistry.addDuplicationStrategy( JPA_DUPLICATION_STRATEGY );

      // op listeners
      eventListenerRegistry.setListeners( EventType.AUTO_FLUSH, JpaAutoFlushEventListener.INSTANCE );
      eventListenerRegistry.setListeners( EventType.DELETE, new JpaDeleteEventListener() );
      eventListenerRegistry.setListeners( EventType.FLUSH_ENTITY, new JpaFlushEntityEventListener() );
      eventListenerRegistry.setListeners( EventType.FLUSH, JpaFlushEventListener.INSTANCE );
      
      // 替换这个JpaMergeEventListener
      eventListenerRegistry.setListeners( EventType.MERGE, new JpaMergeEventListener() );
      
      eventListenerRegistry.setListeners( EventType.PERSIST, new JpaPersistEventListener() );
      eventListenerRegistry.setListeners( EventType.PERSIST_ONFLUSH, new JpaPersistOnFlushEventListener() );
      eventListenerRegistry.setListeners( EventType.SAVE, new JpaSaveEventListener() );
      eventListenerRegistry.setListeners( EventType.SAVE_UPDATE, new JpaSaveOrUpdateEventListener() );

      // post op listeners
      eventListenerRegistry.prependListeners( EventType.POST_DELETE, new JpaPostDeleteEventListener() );
      eventListenerRegistry.prependListeners( EventType.POST_INSERT, new JpaPostInsertEventListener() );
      eventListenerRegistry.prependListeners( EventType.POST_LOAD, new JpaPostLoadEventListener() );
      eventListenerRegistry.prependListeners( EventType.POST_UPDATE, new JpaPostUpdateEventListener() );

      final ConfigurationService cfgService = serviceRegistry.getService( ConfigurationService.class );

      for ( Map.Entry entry : ( (Map<?, ?>) cfgService.getSettings() ).entrySet() ) {
         if ( !String.class.isInstance( entry.getKey() ) ) {
            continue;
         }
         final String propertyName = (String) entry.getKey();
         if ( !propertyName.startsWith( AvailableSettings.EVENT_LISTENER_PREFIX ) ) {
            continue;
         }
         final String eventTypeName = propertyName.substring( AvailableSettings.EVENT_LISTENER_PREFIX.length() + 1 );
         final EventType eventType = EventType.resolveEventTypeByName( eventTypeName );
         final EventListenerGroup eventListenerGroup = eventListenerRegistry.getEventListenerGroup( eventType );
         for ( String listenerImpl : ( (String) entry.getValue() ).split( " ," ) ) {
            eventListenerGroup.appendListener( instantiate( listenerImpl, serviceRegistry ) );
         }
      }

      // handle JPA "entity listener classes"...
      final ReflectionManager reflectionManager = ( (MetadataImpl) metadata ).getMetadataBuildingOptions()
            .getReflectionManager();

      this.callbackRegistry = new CallbackRegistryImpl();
      this.jpaListenerFactory = ListenerFactoryBuilder.buildListenerFactory( sessionFactory.getSessionFactoryOptions() );
      this.callbackBuilder = new CallbackBuilderLegacyImpl( jpaListenerFactory, reflectionManager );
      for ( PersistentClass persistentClass : metadata.getEntityBindings() ) {
         if ( persistentClass.getClassName() == null ) {
            // we can have non java class persisted by hibernate
            continue;
         }
         callbackBuilder.buildCallbacksForEntity( persistentClass.getClassName(), callbackRegistry );
      }

      for ( EventType eventType : EventType.values() ) {
         final EventListenerGroup eventListenerGroup = eventListenerRegistry.getEventListenerGroup( eventType );
         for ( Object listener : eventListenerGroup.listeners() ) {
            if ( CallbackRegistryConsumer.class.isInstance( listener ) ) {
               ( (CallbackRegistryConsumer) listener ).injectCallbackRegistry( callbackRegistry );    // 注入CallbackRegistry
            }
         }
      }
   }

通过断点发现, CallbackRegistry当中存的就是我们要寻找的生命周期的回调函数.

image.png

那么就可以通过反射来进行引用获取, 直接赋到新的替换类: IgnoreNullMergeEventListener当中. 最终代码如下:

第五步: 最终方案: 还原CallbackRegistry

@Configuration
public class HibernateListenerConfigurer {

    @PersistenceUnit
    private EntityManagerFactory entityManagerFactory;

    @PostConstruct
    protected void init() {
        SessionFactoryImpl sessionFactory = entityManagerFactory.unwrap(SessionFactoryImpl.class);
        EventListenerRegistry registry = sessionFactory.getServiceRegistry().getService(EventListenerRegistry.class);
        EventListenerGroup<MergeEventListener> eventListenerGroup = registry.getEventListenerGroup(EventType.MERGE);
        // 获取所有Merge事件相关的监听器, 其中就有JpaMergeEventListener
        Iterable<MergeEventListener> listeners = eventListenerGroup.listeners();

        for (MergeEventListener listener : listeners) {
            if (listener instanceof JpaMergeEventListener) {  // 防止转换异常
                // 获取老的JpaMergeEventListener
                JpaMergeEventListener oldListener = (JpaMergeEventListener) listener;
                // 反射获取老监听器当中的CallbackRegistry, 防止AuditEventListener等叠加不进去.
                CallbackRegistry callbackRegistry = (CallbackRegistry) ReflectUtil
                        .getFieldValue(oldListener, "callbackRegistry");

                // 加入CallbackRegistry, 在执行MERGE之前执行对应的Callback行为(如: AuditEventListener等--@PreCreate, @PreUpdate注释的监听函数)
                IgnoreNullMergeEventListener newListener = new IgnoreNullMergeEventListener(callbackRegistry);
                // 清除老的监听器, 避免重复监听
                registry.getEventListenerGroup(EventType.MERGE).clear();
                // 加入新的监听器
                registry.getEventListenerGroup(EventType.MERGE).prependListener(newListener);
                oldListener = null;     // for GC.
            }
        }
    }
}

总结

对于JPA(Hibernate) Update Selective问题, 网上找了很多办法, 但是并没能得到很完美的解决, 通过源码分析, 得出了最终方案, 希望对各位有所帮助.