宇文老师你好(其实是老勤提问的):
我们先来说宇文老师早上提到的一个注解:@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!共同建造、维护这门课程,我需要每一个你!