问题背景

最近在项目中使用 jpa 操作数据库的时候,遇到了一个问题。

我所使用的数据库是 postgreSQL,通过定义实体类添加@Table注解指定表,定义接口继承CrudRepository类,将实体类作为参数传入,即可调用默认的 save 方法,jpa 会根据主键将实体进行插入或更新操作。

但是,如果用于更新的实体中含有未赋值的属性,即值为 null 的情况时,数据库中对应字段的值会被 null 覆盖。

我先是在 Google 查询了解决方案,有网友提出可以在实体类加上@DynamicUpdate@DynamicInsert两个注解,序列化时即可忽略为 null 的值。我尝试了这种方法,启动单元测试进行了一次更新操作,却发现数据库的数据依然被 null 覆盖。

为了寻找原因,我查看了这两个注解的源码,它们是在 hebernate-core 里定义的。使用 idea 的全局搜索找到了注解的实现代码,还没来得及看具体实现,却发现它被加上了@Deprecated注解。虽然理论上对使用是无影响的,但既然作者不推荐了,还是放弃使用吧。

自己动手

实在不想在苦苦寻找方法了,决定自己造个轮子,实现 update 的功能。逻辑非常简单,每次 save 之前先从数据库查找出对应数据存入实体A,将含有更新数据的实体B中的非空属性赋值给实体A,最后用实体 A 存回数据库。具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* 防止 jpa 将为 null 的属性更新(因为注解DynamicUpdate无效)
*
* @param entity 包含需要更新字段的实体
* @param oldEntity 从数据库获取的旧实体
* @return 最终往数据库插入的实体
*/

public static <T> T updateEntity(T entity,T oldEntity){
Class clazz = entity.getClass();
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields){
try {
PropertyDescriptor pd = new PropertyDescriptor(field.getName(), clazz);
Method readMethod = pd.getReadMethod();
Method writeMethod= pd.getWriteMethod();
if (readMethod.invoke(entity)!=null){ //不为null 的覆盖
writeMethod.invoke(oldEntity,readMethod.invoke(entity));
}
}catch (Exception e){
log.error(e.getMessage());
}
}
return oldEntity;
}

利用反射+泛型写了个通用方法,将新旧实体作为参数传入,反射取出新实体中非 null 的值赋给旧实体,最后将旧实体返回。将方法封装成工具类方便调用。

在代码中,每次更新数据库前调用一下工具,用返回的实体作为 save 对象,经测试,成功解决 null 覆盖问题。

现成工具类

实际上,通过寻找,我确实发现了现成的工具。

大家都知道 BeanUtil 类的 copyProperties 方法可以拷贝两个实体的同名变量,而通过控制传入参数,就可以实现只拷贝非空属性。

BeanUtil 中有个 getNullPropertyNames 方法:

1
2
3
4
5
6
7
8
9
10
11
12
public static String[] getNullPropertyNames (Object source) {
final BeanWrapper src = new BeanWrapperImpl(source);
java.beans.PropertyDescriptor[] pds = src.getPropertyDescriptors();

Set<String> emptyNames = new HashSet<String>();
for(java.beans.PropertyDescriptor pd : pds) {
Object srcValue = src.getPropertyValue(pd.getName());
if (srcValue == null) emptyNames.add(pd.getName());
}
String[] result = new String[emptyNames.size()];
return emptyNames.toArray(result);
}

此方法返回实体的所有非 null 字段,将其作为参数传入 copyProperties ,即可实现指定复制。代码如下:

1
2
3
4
5
	public void update(ClassA source, Integer id ){
ClassA target = classDao.findById(id);
BeanUtils.copyProperties(source,target,getNullPropertyNames(source));
classDao.update(target );
}