如何在JPA选择性的UPDATE一个表项?

众所周知,JPA是Sun公司搞的一套接口,这套接口好是好用,就是默认没有提供选择性更新的方法,它自带的save(T t)会保存t这个POJO里的所有属性到数据库里,这就很恼火了。今天笔者也遇到了这个问题,因为大多数时候,进行PUT动作需要更新数据库里的字段信息,前端传过来的JSON不会包含原POJO里的所有数据,比如这样:

PATCH {ServerRoot}/XXX/{id = 3}
param = {"name":"武装120重型坦克"}

传到后端进行JSON转对象以后,是这样的:

Want(id=3, name=武装120重型坦克, amount=null, price=null, remark=null, date=null, status=null)

可以看到除了传到后端的对象只有在前端指定的数据,直接执行

wantService.save(want);

肯定会报错,那该怎么办?
思路1:
  看看JPA的库有没有提供这个方法?
答:
  没有。
思路2:
  看看网上有没有现成的解决方案?
答:
  基本都是三言两语,没有完整的解决方案。
笔者的解决方案:
  使用对象克隆,源是XXX,目标是使用id查询到的oldXXX,当遇到空值则不拷贝,最后将oldXXX存入数据库,以此模拟更新行为。

行动

既然我有了解决方案,那就立即着手开始解决问题把:

Spring框架提供的BeanUtils

笔者最开始准备使用org.springframework.beans.BeanUtils的public static void copyProperties(Object source, Object target, String... ignoreProperties)方法,其中String... ignoreProperties是过滤属性参数,也就是不参与复制的参数名,但是这个问题来了,该怎么获取属性为null的属性名,再转为一个个String作为这个方法的参数,这个参数也有毛病,为什么不用Collection、List这种集合类,而是搞可变参。感觉到困难重重,遂放弃。

Hutool工具包的BeanUtil

使用Hutool工具包中的BeanUtil中的这个方法(参考文档Hutool工具包文档)中:

public static <T> T fillBean(T bean, ValueProvider<String> valueProvider, CopyOptions copyOptions);

可以看到它需求参数:类型为T的bean、ValueProvider<String>的值提供者、CopyOptions选项。
下边将一个一个讲怎么做。

bean的实现

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import java.io.Serializable;
import java.util.Date;

@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Want {

    public Want(int id) {
        this.id = id;
    }

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;

    private String name;

    private Integer amount;

    private Double price;

    private String remark;

    private Date date;

    private Integer status;

}

再向它对应的服务接口添加方法:

    /**
     * @param id 前端传入的path变量id
     * @param want 待更新的数据
     * @return 更新后的POJO
     */
    Want update(int id, Want want);

向接口实现类添加实现:

    /**
     * @param id 前端传入的path变量id
     * @param want 待更新的数据
     * @return 更新后的POJO
     */
    @Override
    public Want update(int id, Want want) {
        Want oldWant = this.findById(id); // 通过id获取表项对应的POJO
        System.out.println("待合并的POJO: " + want.toString()); // 这个sout和下边那个记得删掉
        WantValueProvider wantValueProvider = new WantValueProvider(); // 实例化一个ValueProvider
        wantValueProvider.setValueProvider(want); // 设置值提供者为want
        BeanUtil.fillBean(oldWant, wantValueProvider, CopyOptions.create().ignoreNullValue()); // 复制目标为oldWant,选项选择无视空值(即不对want的空值操作)
        oldWant.setId(id); // 设置id
        System.out.println("合并后的POJO: " + oldWant.toString());
        return wantRepository.save(oldWant);
    }

ValueProvider的实现

首先它要继承ValueProvider<T>这个接口,通过反编译可以看到这个接口长这样:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package cn.hutool.core.bean.copier;

import java.lang.reflect.Type;

public interface ValueProvider<T> {
    Object value(T var1, Type var2);

    boolean containsKey(T var1);
}

fillBean方法已经指定这个泛型T是String了,所以这里继承 ValueProvider<String>:

import cn.hutool.core.bean.copier.ValueProvider;
import domain.Want;
import java.lang.reflect.Type;

public class WantValueProvider implements ValueProvider<String> {

    private Want valueProvider = null; // 指定值提供者为Wantl类型

    /**
     * 需要在service里设置好
     * @param valueProvider 值提供者,系前端传来的JSON转的bean
     */
    public void setValueProvider(Want valueProvider) {
        this.valueProvider = valueProvider;
    }

    /**
     * Key-Value
     * @param s Key,属性名
     * @param type 数据类型
     * @return Value,属性值
     */
    @Override
    public Object value(String s, Type type) {
        switch (s) {
            case "id":
                return this.valueProvider.getId();
            case "name":
                return this.valueProvider.getName();
            case "amount":
                return this.valueProvider.getAmount();
            case "price":
                return this.valueProvider.getPrice();
            case "remark":
                return this.valueProvider.getRemark();
            case "date":
                return this.valueProvider.getDate();
            case "status":
                return this.valueProvider.getStatus();
        }
        return null;
    }

    /**
     *
     * @param s 待确认是否包含在Bean里的属性名
     * @return  是否包含有名为s的属性 ? true : false
     */
    @Override
    public boolean containsKey(String s) {
        switch (s) {
            case "id":
            case "name":
            case "amount":
            case "price":
            case "remark":
            case "date":
            case "status":
                return true;
        }
        return false;
    }
}

笔者这里为了保证复用性,确实搞得有些麻烦了,这里其实也可以用匿名的内部类,效果一样的,像这样:

        BeanUtil.fillBean(oldWant, new ValueProvider<String>() {

            @Override
            public Object value(String s, Type type) {
                // 方法实现
                return null;
            }

            @Override
            public boolean containsKey(String s) {
                // 方法实现
                return false;
            }

        } ,CopyOptions.create().ignoreNullValue());

方法实现参考上边。

CopyOptions选项

这里因为是需要把从前端获取的JSON转对象选择性的保存入表中,所以需要设置为空值不保存,也就是传入:

CopyOptions.create().ignoreNullValue()

测试

运行,点击链接http://localhost:8080/swagger-ui.html打开Swagger UI测试界面 ,展开需要测试的控制器:


点击'Try it out'按钮:

输入数据,点击Execute,得到返回值:


查看控制台:

待合并的POJO: Want(id=0, name=辣条, amount=5, price=0.5, remark=null, date=null, status=null)
合并后的POJO: Want(id=25, name=辣条, amount=5, price=0.5, remark=加满油, date=2019-10-13 01:23:26.0, status=1)

数据库中的原始数据:


刷新后:

说明合并成功了。
也说明选择性更新成功。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容