详解Spring Data Jpa当属性为Null也更新的完美解决方案

开场白

我本来是一名android开发者,突然就对java后端产生了浓烈的兴趣。所以,立马就转到了后端。第一个项目使用的使用Spring Data Jpa来操作数据库的,可是在更新数据的时候发现一个问题,属性值为Null竟然也更新,这就会导致本来没有更新的属性值,全部就成了Null。

原因

经过一番度娘操作,原来Jpa,不知道你是想把属性设置为Null,还是不想。

解决方法

找到一个方法,就是在数据模型上加上注解@DynamicUpdate,可是发现并不好使。而后经过整理,找到以下解决方案

我们有如下实体

@Entity
public class User{

 public User(){

 }

 @Id
 @GeneratedValue
 public Long id;

 private String name;
 private String mobileNo;
 private String email;
 private String password;
 private Integer type;
 private Date registerTime;
 private String region;
 private Integer validity;

 setter...
 getter...
}

需求:我们只更新用户的名字,其他属性值不变。

controller代码如下

@RestController
public class UserController {
 @Autowired
 private UserDao userDao;

 @PostMapping(value = "/save")
 public String save(@RequestBody User u) {
  userDao.save(u)
  return "更新成功";
 }
}

注意:如果我们只是更新用户的名字,我们会这样操作,如下只是提交需要更新的用户的id和需要更新属性的值,但是这样的结果就是,其他没有提交更改的属性值,会被当成Null,将数据库中对应值全部设为Null,为了解决这个问题提出以下方案。

 {
  "id" : "1",
  "name" : "张三"
 }

方案如下:

说明:

  1. 目标源:请求更新的实体数据。
  2. 数据源:通过目标源传上来的id,去数据库中查出的实体数据

我们可以将目标源中需要改变的属性值过滤掉以后,将数据源中的数据复制到目标源中,这样就达到了,只是更新需要改变的属性值,不需要更新的保持不变。

工具类如下

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

import java.beans.PropertyDescriptor;
import java.util.HashSet;
import java.util.Set;

/**
 * There is no royal road to learning.
 * Description:提交实体对象中的null赋值
 * Created by 贤领·周 on 2018年04月10日 15:26
 */
public class UpdateTool {
 /**
  * 将目标源中不为空的字段过滤,将数据库中查出的数据源复制到提交的目标源中
  *
  * @param source 用id从数据库中查出来的数据源
  * @param target 提交的实体,目标源
  */
 public static void copyNullProperties(Object source, Object target) {
  BeanUtils.copyProperties(source, target, getNoNullProperties(target));
 }

 /**
  * @param target 目标源数据
  * @return 将目标源中不为空的字段取出
  */
 private static String[] getNoNullProperties(Object target) {
  BeanWrapper srcBean = new BeanWrapperImpl(target);
  PropertyDescriptor[] pds = srcBean.getPropertyDescriptors();
  Set<String> noEmptyName = new HashSet<>();
  for (PropertyDescriptor p : pds) {
   Object value = srcBean.getPropertyValue(p.getName());
   if (value != null) noEmptyName.add(p.getName());
  }
  String[] result = new String[noEmptyName.size()];
  return noEmptyName.toArray(result);
 }
}

这里重点说明一下, BeanUtils.copyProperties这个方法,网上很多教程都是存在误区的,源码如下:

 /**
  * 通过源码不难看出,该方法就是将source的属性值复制到target中
  *
  * @param source 数据源(也就是我们通过id去数据库查询出来的数据)
  * @param target 目标源(也就是我们请求更新的数据)
  * @param ignoreProperties (需要过滤的字段)
  */
 public static void copyProperties(Object source, Object target, String... ignoreProperties) throws BeansException {
  copyProperties(source, target, (Class)null, ignoreProperties);
 }

 private static void copyProperties(Object source, Object target, Class<?> editable, String... ignoreProperties) throws BeansException {
  Assert.notNull(source, "Source must not be null");
  Assert.notNull(target, "Target must not be null");
  Class<?> actualEditable = target.getClass();
  if (editable != null) {
   if (!editable.isInstance(target)) {
    throw new IllegalArgumentException("Target class [" + target.getClass().getName() + "] not assignable to Editable class [" + editable.getName() + "]");
   }

   actualEditable = editable;
  }

  PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable);
  List<String> ignoreList = ignoreProperties != null ? Arrays.asList(ignoreProperties) : null;
  PropertyDescriptor[] var7 = targetPds;
  int var8 = targetPds.length;

  for(int var9 = 0; var9 < var8; ++var9) {
   PropertyDescriptor targetPd = var7[var9];
   Method writeMethod = targetPd.getWriteMethod();
   if (writeMethod != null && (ignoreList == null || !ignoreList.contains(targetPd.getName()))) {
    PropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), targetPd.getName());
    if (sourcePd != null) {
     Method readMethod = sourcePd.getReadMethod();
     if (readMethod != null && ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType())) {
      try {
       if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) {
        readMethod.setAccessible(true);
       }

       Object value = readMethod.invoke(source);
       if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) {
        writeMethod.setAccessible(true);
       }

       writeMethod.invoke(target, value);
      } catch (Throwable var15) {
       throw new FatalBeanException("Could not copy property '" + targetPd.getName() + "' from source to target", var15);
      }
     }
    }
   }
  }

 }

有了上面的工具类以后,我们的controller如下写:

@RestController
public class UserController {
 @Autowired
 private UserDao userDao;

 @PostMapping(value = "/save")
 public String save(@RequestBody User u) {
  if(u.getId != 0){
   User source= userDao.findOne(u.getId);
   UpdateTool.copyNullProperties(source, u);
  }
  userDao.save(u)
  return "更新成功";
 }
}

结果

这样我们更新部分属性值得时候,其他不更新的属性值就不会设置为Null

 {
  "id" : "1",
  "name" : "张三"
 }

性能上肯定是有影响,但是目前整理可行的方案,如果有更好的解决方案欢迎留言。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

时间: 2019-01-31

Spring Data JPA 建立表的联合主键

最近遇到了一个小的问题,就是怎么使用 Spring Data JPA 建立表的联合主键?然后探索出了下面的两种方式. 第一种方式: 第一种方式是直接在类属性上面的两个字段都加上 @Id 注解,就像下面这样,给 stuNo 和 stuName 这两个字段加上联合主键: @Entity @Table(name = "student") public class Student { @Id @Column(name = "stu_no", nullable = false

Spring Data Jpa 自动生成表结构的方法示例

想在部署的时候随应用的启动而初始化数据脚本,这不就是Spring Data Jpa中的自动生成表结构,听起来特别简单,不就是配置Hibernate的ddl-auto嘛,有什么好说的,是个人都知道.当初我也是这样认为,实际操作了一把,虽然表是创建成功了,但是字段注释,字符集以及数据库引擎都不对,没想到在这些细节上翻车了. 毕竟开翻的车还要自己扶起来,于是在这记录一下. 注:本文中使用的Spring Data Jpa版本为2.1.4.RELEASE 以MySQL为例,我这边举个例子: import

Spring Boot中使用Spring-data-jpa的配置方法详解

为了解决这些大量枯燥的数据操作语句,我们第一个想到的是使用ORM框架,比如:hibernate.通过整合Hibernate之后,我们以操作Java实体的方式最终将数据改变映射到数据库表中. 为了解决抽象各个Java实体基本的"增删改查"操作,我们通常会以泛型的方式封装一个模板Dao来进行抽象简化,但是这样依然不是很方便,我们需要针对每个实体编写一个继承自泛型模板Dao的接口,再编写该接口的实现.虽然一些基础的数据访问已经可以得到很好的复用,但是在代码结构上针对每个实体都会有一堆Dao的

实例讲解使用Spring通过JPA连接到Db2

前提条件 Maven 一个拥有访问凭证的 Db2 实例: IBM Cloud 本地 Java JDK IBM Cloud 开发者工具(可选) 创建项目 首先,确定您计划使用 IBM Cloud 开发者工具还是 Spring Initializr 创建项目,然后按照各自的说明进行操作. IBM Cloud 开发者工具 如果您使用 IBM Cloud 开发者工具创建项目,那么使用 dev 插件创建新的 Spring 微服务. ibmcloud dev create 1.选择 Backend Serv

Spring Data Jpa 复合主键的实现

前言 这次大创有个需求,在数据库建表时发现,user表与project表的关系表 user_project的主键为复合主键: CREATE TABLE user_project( user_id INT(20), project_id INT(20), timestamp VARCHAR (50), donate_money DOUBLE(10,2), PRIMARY KEY (user_id,project_id) ); 在网上看了几篇博客,以及在spring boot干货群咨询(感谢夜升额耐

Db2数据库中常见的堵塞问题分析与处理方法

Db2 数据库堵塞怎么办 作为一个数据库管理员,工作中经常会遇到的一个问题:当数据库出现故障的情况下,如何快速定位问题和找到解决方案.尤其是在运维非常重要系统的时候,解决问题恢复服务是分秒必争.Db2 作为广泛使用的商业数据库,内部提供了众多方法论和诊断工具等来协助分析问题.然而当问题真正发生的时候,数据库管理员还是会手忙脚乱,不知道从何处下手.如果着手分析的方向发生了错误,时间更是浪费严重,问题得不到及时解决,甚至有可能采取了错误的措施,导致更严重的后果. 导致数据库堵塞原因有很多,即便是现在

详解易语言链接DB2 OLEDB实例方法

很多朋友在写易语言软件的时候需要用到链接DB2等内容,大家参阅下. 'database=rbsj:这是连数据库名,Hostname=PC-200806261910;"这是自己的计算机的名字, 如果真 (数据库连接.连接 ("Provider=IBMDADB2;database=rbsj;Hostname=PC-200806261910;")) sql = "SELECT * FROM ADMIN.RBSJ " .版本 2 .支持库 eDB .支持库 eGr

详解易语言的程序的输入方法概念

为了便于输入程序,易语言内置四种名称输入法:首拼.全拼.双拼.英文.三种拼音输入法均支持南方音及多音字.首拼输入法及全拼输入法在系统中被合并为"首拼及全拼输入法",系统自动判别所输入的拼音是首拼方式还是全拼方式.双拼输入法的编码规则与 Windows 系统所提供的双拼输入法一致.例如:欲输入"取整 (1.23)"语句,各种输入法的输入文本为: ・ 首拼及全拼输入法: qz(1.23) 或者 quzheng(1.23) ・ 双拼输入法: quvg(1.23) ・ 英文

详解易语言字符命令

易语言字符命令,这个命令比较少见,但是有用,我教大家操作. 1.易语言新建一个windows窗口 点击进入代码编辑区 具体看如何用易语言编写自己第一个程序? 2.我们输入 这个命令 字符() 3.展开这个字符命令 我们发现只有一个参数 4.这个参数比较少见,字节型它的取值范围为0~255 我们输入100看看 5.运用调试输出这个函数,具体看易语言调试输出函数实例详解 我们输入调试输出 (字符 (100)) 6.结果为 d 这个需要对照 ASCII表看看

详解易语言中的数据类型

各种数据存放在磁盘或内存中都有其不同的存放格式,因此就存在不同的数据类型.了解各种数据的特性,对编程开发来说是十分重要. 程序中经常会进行一些运算,易语言中的运算都要使用运算符进行识别处理,并通过运算表达式来完成运算操作.程序中对各数据之间的关系的描述也要通过运算符. 1.易语言的数据类型 一个程序内部应包括两个方面的内容:1.数据的描述.2.操作步骤,即对程序动作的描述. 数据是程序操作的对象,操作的结果会改变数据的内容.打个比方:要做一道菜,做菜前先选择烹饪的原材料(即对数据进行描述),然后

详解易语言变量用法和原理

易语言是一款可视全中文的编程语言,由于他的简单易用,深受国人喜欢,下面我来为大家介绍易语言变量的用法. 易语言变量,就像是数学中设x一样,顾名思义它是不定值的,它分为局部变量,全局变量,和程序集变量,下面我为用一个程序来向大家介绍这3种变量的区别. 打开易语言,新建一个windows窗口程序,将按钮按下图所示排列. 点击插入,选择窗口,插入一个新的窗口,点击按钮1,写入代码入下图所示, 点击窗口程序集,回车,添加一个程序集变量,命名为c,类型为文本型. 点击插入,分别添加全局变量,命名为q,类型

详解易语言写内存整数型

本篇文章主要介绍如何使用编程软件"易语言"做到修改指定进程的指定内存地址中的整数型数据. 1.启动"易语言". 2.选择"菜单栏"中的"f.程序",再在弹出的列表中选择"N.新建". 3.在弹出的标题为"新建:"的窗口中选择"Windows窗口程序",再点击标题为"确定(o)"的按钮. 4.在背景为灰色的,且标题为""(空的文本

详解易语言线程同步

在易语言官方多线程支持库中提供线程同步的方法是用许可区. 加入许可区之后可以防止多个线程同时访问公用变量是发生冲突.加入许可区的代码同时只能有一个线程访问,避免冲突. 创建许可区: 创建并返回一个进入许可证数值,此许可证值用作进入程序中的指定许可代码区,以避免多线程冲突.成功返回非零整数值,失败返回0.所创建的许可证在不再使用后,必须使用"删除进入许可证"命令将其删除.本命令为初级命令. 删除进入许可证: 删除由"创建进入许可证"命令所创建返回的进入许可证.成功返回

详解易语言常量用法

易语言常量用法,我用实例讲解,下面一步步操作,大家学懂了,给我投一票,谢谢! 1.易语言新建一个windows窗口点击常量表进入 2.ctrl+N 就会增加一列 如图 3.我们设置常量 分别在相应位置输入数据  如图 4.我们双击 新建的窗口 进入代码编辑区 输入_启动窗口.标题=#常量1 5.这里介绍下 常量的用法 就是常量名前面加上# 看图 这样就把它的值使用了 6.我们运行看看结果 看图 标题改变成了我们设置的常量

详解易语言时钟的用法

易语言时钟是易语言的一个基本组件,有时程序很常用,也非常重要. 1.打开易语言 2.新建一个windows窗口程序,把时钟,标签如下图所示放置 3.在2个时钟的属性事件中都选择周期事件 4.添加一个常量,命名为时间,数值为30 5.双击启动窗口,开始编写,写入代码如下图所示 6.点击运行,时钟的功能就展现出来了,时钟的功能主要是倒计时,计时等功能,功能小,功能效果如下面的动态图所示 以上六个步骤就是关于易语言时钟用法的教程,如果还有任何问题大家可以给小编留言,感谢大家对我们的支持.

详解易语言的如果命令

易语言如果是流程控制的命令,怎么使用呢?我用实例教大家,下面一步步演示给大家看. 1.易语言新建一个windows程序 点击进入代码编辑区 2.我们输入如果出现了如图所示 有2各箭头 3.我们查找帮助了解下,知道 括号里的条件为真是就顺序向下执行,否则跳转到左侧箭头指向 4.我们输入 如果(1>3) 5.在相应位置输入执行代码 调试输出("真") 调试输出("假") 6.执行看看效果 显示为假 是正确的 7.我们修改条件为 3>1 8.运行后结果为真了,