解决JPA save null的问题


目录
  • 解决JPA save null的问题
    • 利用hibernate本身的机制来重写部分逻辑

解决JPA save null的问题

如果调用jpa的save方法进行更新, 会将null字段也更新到数据库中, 这要求你在进行更新的时候, 必须先进行查询, 然后将要更新的字段数据set到实体类上, 最后save, 在事务中甚至不需要调用save即可更新到数据库.

例如:

public User update(Long id, User user) {
    var entity = userRepository.findById(id).orElseThrow();
    var optional = Optional.of(user);
    optional.map(User::getUsername).ifPresent(entity::setUsername);
    optional.map(User::getDepartment).ifPresent(entity::setDepartment);
    // ...set其它值
    userRepository.save(entity);
}

这样更新更加规范, 是一种更健壮的编码方式.

缺点也很明显: 每次更新都要先查询, 查询后还要手动调用实体类的setter来更新对应的数据. 很难为不同的类提取公共代码出来.

为了简化这样的过程, 部分框架可能利用反射. 例如利用hutoolBeanUtils忽略复制null字段.

public User update(Long id, User user) {
    var entity = userRepository.findById(id).orElseThrow();
    BeanUtil.copyProperties(user, entity, CopyOptions.create().setIgnoreNullValue(true));
    userRepository.save(entity);
}

反射的缺点也很明显, 不可避免的会影响到性能, 个人也不太喜欢在业务代码中使用反射.

利用hibernate本身的机制来重写部分逻辑

hibernate为定义orm提供了很多注解.

例如@Column可以配置与列的映射关系, 其中nullable可以配置该列是否为空.

然而即便配置了@Column(nullable = false), 该配置也不会在save时发挥任何作用, 该字段为null的数据仍然会被更新到数据库然后报错.

从语义上说, 当指定@Column(nullable = false)时, 那么字段值为空就不更新到数据库, 这是非常符合直觉的理解.

利用hibernate提供的一些配置, 可以自己重写部分处理该部分数据的逻辑来实现这样的逻辑.

创建一个CustomerDefaultMergeEventListener, 继承自DefaultMergeEventListener

public class CustomDefaultMergeEventListener extends DefaultMergeEventListener {
}

然后创建一个CustomIntegratorProvider继承自IntegratorProvider

public class CustomIntegratorProvider implements IntegratorProvider {

  @Override
  public List getIntegrators() {
    return List.of(
        new Integrator() {
          @Override
          public void integrate(
              Metadata metadata,
              SessionFactoryImplementor sessionFactory,
              SessionFactoryServiceRegistry serviceRegistry) {
            var serviceRegistryService = serviceRegistry.getService(EventListenerRegistry.class);
            // 直接设置EventType.MERGE的listener是自定义的CustomDefaultMergeEventListener
            serviceRegistryService.setListeners(
                EventType.MERGE, CustomDefaultMergeEventListener.class);
          }

          @Override
          public void disintegrate(
              SessionFactoryImplementor sessionFactory,
              SessionFactoryServiceRegistry serviceRegistry) {}
        });
  }
}

然后在在springboot配置文件中添加配置:

spring.jpa.properties.hibernate.integrator_provider=com.wymc.demo.jpa.CustomerDefaultMergeEventListener

此时配置已经可用, 但逻辑还没重写. 编辑CustomDefaultMergeEventListener添加以下代码:

public class CustomDefaultMergeEventListener extends DefaultMergeEventListener {
  @Override
  protected void copyValues(
      EntityPersister persister,
      Object entity,
      Object targetEntity,
      SessionImplementor session,
      Map copyCache) {
    var original = persister.getPropertyValues(entity);
    var target = persister.getPropertyValues(targetEntity);
    var types = persister.getPropertyTypes();
    // 获取列是否可以为空, 即@Column中的nullable配置
    var propertyNullability = persister.getPropertyNullability();

    Object[] copied = new Object[original.length];
    for (int i = 0; i < types.length; i++) {
      // 修改此处的判断条件, 你可以不验证@Column注解的nullable
      // 下面这种用法既可以给可以为null的字段设置null, 又不会更新不能为null的字段
      if (!propertyNullability[i] && original[i] == null) {
        // 如果nullable为false, 并且传入的值为null, 则使用原数据, 即不更新null
        copied[i] = target[i];
      } else 
      // 下面这部分是原有的代码, 不做改动    
      if (original[i] == LazyPropertyInitializer.UNFETCHED_PROPERTY
          || original[i] == PropertyAccessStrategyBackRefImpl.UNKNOWN) {
        copied[i] = target[i];
      } else if (target[i] == LazyPropertyInitializer.UNFETCHED_PROPERTY) {
        copied[i] = types[i].replace(original[i], null, session, targetEntity, copyCache);
      } else {
        copied[i] = types[i].replace(original[i], target[i], session, targetEntity, copyCache);
      }
    }
    persister.setPropertyValues(targetEntity, copied);
  }
}
jpa