宇文老师今早说了一个问题:JPA 如何做部分字段的更新?

来源:1-2 为何我们需要每一个你

张勤一

2020-10-14

由于宇文老师最近在研究 1nm 的事,没有很多时间,这里我来提问吧。如题,JPA 中如何做部分字段的更新呢?

写回答

1回答

张勤一

提问者

2020-10-14

宇文老师你好(其实是老勤提问的):

    我们先来说宇文老师早上提到的一个注解:@DynamicUpdate,这个注解用在实体类上,那么,它是做什么用的呢?

    我们先来看看官网对它的说明:

    官网所说,它可以实现对实体对象的部分字段更新,也就是你自己 set 了哪些字段更新,就可以只对那些字段更新,而不是你调用 save 之后,全部都 set 一遍。如果进行频繁的更新操作,并且每次只更新少数字段,那么@DynamicUpdate 对性能的优化效果还是很好的。

    但是,你需要知道,只有当你的 JPA 背后的实现是 Hibernate 时,你才可以使用这个注解,而不能认为只要是 Spring 框架内允许的,都可以使用这个注解。这也就是我在课程中从没有说过这个注解的最主要原因,我们的规范里面也不允许使用。其实,就类似于日志框架一样,遵循 SLF4J,但是,不限于 Log4J 还是 logback!

 

    那么,我们通用的做法怎么去做部分字段的更新呢?我这里推荐两种实现方式:一种是使用自定义的 Query 更新实体对象;另一种是借助实体对象更新实体对象。下面,我来给出示例说明(早上宇文老师提出这个问题之后,我才意识到我从没有介绍过这个话题,实在是惭愧,于是我赶紧写出来):

    我们先来假设我有一张 AuthPermission 表,有若干个字段,我这里去部分更新其中的两个字段:permissionName 和 permissionDesc,主键是 int 类型。

    第一种方式:使用自定义的 Query 自己来实现 SQL 语句,我们需要在 Dao 接口中自己定义 update 方法,如下:

@Transactional
@Modifying
@Query("UPDATE AuthPermission ap SET ap.permissionName = :name, ap.permissionDesc = :desc WHERE ap.id = :id")
int updatePermissionNameAndPermissionDesc(
        @Param("name") String name, @Param("desc") String desc, @Param("id") Integer id
);

    你需要特别注意,这里有三个注解,一个都不能缺少。Query 注解我不多说了,毕竟就是你自己定义的 SQL 语句。如果缺少了 @Modifying,你会发现报错:

org.hibernate.hql.internal.QueryExecutionRequestException: Not supported for DML

    那如果缺少了 @Transactional,你会发现报错:

nested exception is javax.persistence.TransactionRequiredException: Executing an update/delete query

    我们可以写一个测试用例来验证下它是否好用:

@Autowired
private AuthPermissionDao permissionDao;

/**
 * <h2>使用自定义的 Query 更新实体对象</h2>
 * */
@Test
public void testUseQueryUpdateJpaEntity() {

    AuthPermission permission = permissionDao.findById(2).orElse(null);
    assert null != permission;

    log.info("update jpa entity: [{}]", permissionDao.updatePermissionNameAndPermissionDesc(
            "表哥", "老实人", permission.getId()
    ));
}

    执行测试用例,可以看到如下的日志打印输出:

Hibernate:
update
        auth_permission
set
permission_name=?,
permission_desc=?
where
id=?
10:54:26.229 [main] TRACE o.h.type.descriptor.sql.BasicBinder 65 - binding parameter [1] as [VARCHAR] - [表哥]
10:54:26.230 [main] TRACE o.h.type.descriptor.sql.BasicBinder 65 - binding parameter [2] as [VARCHAR] - [老实人]
10:54:26.231 [main] TRACE o.h.type.descriptor.sql.BasicBinder 65 - binding parameter [3] as [INTEGER] - [2]

    OK。这种方式是可行的。但是,这种方式有个明显的弊端,就是当你一次性更新的字段较多时,你的 SQL 语句会非常难写,而且可读性很差,怎么办?也就有了第二种方式。


    第二种方式: 借助实体对象更新实体对象。其实怎么说呢,这种方式与我们课程中讲解的内容几乎无异,只是说我们已经有了一个实体对象的部分属性,其他的属性没有,但是,直接更新的话,没有属性的字段就会变成 NULL 了。所以,这种方式需要先 SELECT 出来原来的表记录。这样的解释可能不好理解,我还是以代码的形式讲解吧:

/**
 * <h2>借助实体对象更新实体对象</h2>
 * */
@Test
public void testUseEntityUpdateJpaEntity() {

    AuthPermission permission = permissionDao.findById(2).orElse(null);
    assert null != permission;

    // 复制想要更改的字段值
    AuthPermission newPermission = new AuthPermission();
    newPermission.setPermissionName("宇文老师");
    newPermission.setPermissionDesc("感谢您为 1nm 事业所做的贡献");

    BeanUtils.copyProperties(newPermission, permission, JpaEntityUpdateUtil.getNullPropertyNames(newPermission));

    log.info("update jpa entity: [{}]", permissionDao.save(permission).getId());
}

        其中,定义了一个工具类,用于拷贝属性,我也贴一下:

import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeanWrapperImpl;

import java.util.HashSet;
import java.util.Set;

/**
 * <h1>Jpa 实体类更新工具类</h1>
 * */
public class JpaEntityUpdateUtil {

    public static String[] getNullPropertyNames(Object source) {

        BeanWrapper src = new BeanWrapperImpl(source);
        java.beans.PropertyDescriptor[] pds = src.getPropertyDescriptors();
        Set<String> emptyNames = new HashSet<>();

        for (java.beans.PropertyDescriptor pd : pds) {
            Object srcValue = src.getPropertyValue(pd.getName());
            if (srcValue == null) {
                emptyNames.add(pd.getName());
            }
        }

        return emptyNames.toArray(new String[0]);
    }
}

    执行下吧,你会看到如下的日志:

Hibernate:
update
        auth_permission
set
create_time=?,
permission_desc=?,
permission_name=?,
permission_type_id=?,
update_time=?
where
id=?
11:01:40.978 [main] TRACE o.h.type.descriptor.sql.BasicBinder 65 - binding parameter [1] as [TIMESTAMP] - [2020-08-03 14:41:36.0]
11:01:40.979 [main] TRACE o.h.type.descriptor.sql.BasicBinder 65 - binding parameter [2] as [VARCHAR] - [感谢您为 1nm 事业所做的贡献]
11:01:40.980 [main] TRACE o.h.type.descriptor.sql.BasicBinder 65 - binding parameter [3] as [VARCHAR] - [宇文老师]
11:01:40.981 [main] TRACE o.h.type.descriptor.sql.BasicBinder 65 - binding parameter [5] as [INTEGER] - [1]
11:01:40.982 [main] TRACE o.h.type.descriptor.sql.BasicBinder 65 - binding parameter [6] as [TIMESTAMP] - [Wed Oct 14 11:01:40 CST 2020]
11:01:40.982 [main] TRACE o.h.type.descriptor.sql.BasicBinder 65 - binding parameter [7] as [INTEGER] - [2]

    OK,也做到了部分字段的更新。而且,我觉得你应该能想的到这种方式的适用场景,对,就是前端直接传递过来一个对象,包含有部分需要更新的字段,你去做更新的时候就可以使用这个模板了。


    我是勤一,致力于将这门课程的问答区打造为 Java 知识体系知识库,Java 知识体系 BBS!共同建造、维护这门课程,我需要每一个你!

2
2
笑看从前小菜哥
感谢宇文老师,你为 1nm 做出的贡献,也感谢老勤为宇文老师指点迷津
2021-05-24
共2条回复

Java实操避坑指南 SpringBoot/MySQL/Redis错误详解

掌握业务开发中各种类型的坑,,Java web开发领域通用

452 学习 · 204 问题

查看课程