Spring Data进行部分数据更新

1. 概述

Spring Data的CrudRespository#save无疑很简单,但有一个功能可能是一个缺点:它更新表中的每一列。这就是 CRUD 中 U 的语义,但如果我们想做一个 PATCH 怎么办?

在本教程中,我们将介绍执行部分更新而不是完整更新的技术和方法。

2. 问题

如前所述,save() 将使用提供的数据覆盖任何匹配的实体,这意味着我们无法提供部分数据。这可能会变得不方便,特别是对于具有大量场的大型对象。

如果我们看一下ORM,就会发现存在一些补丁:

  • Hibernate的@DynamicUpdate注释,动态重写更新查询
  • JPA 的@Column注释,因为我们可以使用可更新参数禁止对特定列进行更新

但是我们将以特定的意图来解决这个问题:我们的目的是为我们的实体准备保存方法,而不依赖于ORM。

3. 我们的案例

首先,让我们构建一个客户实体:

@Entity 
public class Customer {
    @Id 
    @GeneratedValue(strategy = GenerationType.AUTO)
    public long id;
    public String name;
    public String phone;
}

然后我们定义一个简单的 CRUD 存储库:

@Repository 
public interface CustomerRepository extends CrudRepository<Customer, Long> {
    Customer findById(long id);
}

最后,我们准备一个客户服务

@Service 
public class CustomerService {
    @Autowired 
    CustomerRepository repo;

    public void addCustomer(String name) {
        Customer c = new Customer();
        c.name = name;
        repo.save(c);
    }	
}

4. 加载和保存方法

让我们首先看一个可能熟悉的方法:从数据库加载我们的实体,然后只更新我们需要的字段。这是我们可以使用的最简单的方法。

让我们在服务中添加一种方法来更新客户的联系人数据。

public void updateCustomerContacts(long id, String phone) {
    Customer myCustomer = repo.findById(id);
    myCustomer.phone = phone;
    repo.save(myCustomer);
}

我们将调用findById方法并检索匹配的实体。然后,我们继续更新所需的字段并保留数据。

当要更新的字段数量相对较少且我们的实体相当简单时,此基本技术非常有效。

要更新数十个字段会发生什么情况?

4.1. 映射策略

当我们的对象具有大量具有不同访问级别的字段时,实现DTO 模式是很常见的。

现在假设我们的对象中有一百多个电话字段。像以前一样编写将数据从 DTO 倾倒到我们的实体的方法可能会很烦人且非常难以维护。

尽管如此,我们可以使用映射策略来解决这个问题,特别是使用 MapStruct实现。

让我们创建一个CustomerDto

public class CustomerDto {
    private long id;
    public String name;
    public String phone;
    //...
    private String phone99;
}

我们还将创建一个客户映射器

@Mapper(componentModel = "spring")
public interface CustomerMapper {
    void updateCustomerFromDto(CustomerDto dto, @MappingTarget Customer entity);
}

@MappingTarget注释允许我们更新现有对象,从而节省了编写大量代码的痛苦。

MapStruct有一个@BeanMapping方法装饰器,它允许我们定义一个规则来在映射过程中跳过值。

让我们将其添加到我们的updateCustomerFromDto方法接口中:

@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)

有了这个,我们可以加载存储的实体并在调用 JPAsave方法之前将它们与 DTO 合并——事实上,我们只会更新修改后的值。

因此,让我们向服务添加一个方法,它将调用我们的映射器:

public void updateCustomer(CustomerDto dto) {
    Customer myCustomer = repo.findById(dto.id);
    mapper.updateCustomerFromDto(dto, myCustomer);
    repo.save(myCustomer);
}

这种方法的缺点是我们无法在更新期间将null值传递给数据库。

4.2. 更简单的实体

最后,请记住,我们可以从应用程序的设计阶段开始解决这个问题。

必须将我们的实体定义为尽可能小。

让我们看一下我们的客户实体

我们将稍微构建一下它,并将所有电话字段提取到ContactPhone实体,并处于一对多关系下:

@Entity public class CustomerStructured {
    @Id 
    @GeneratedValue(strategy = GenerationType.AUTO)
    public Long id;
    public String name;
    @OneToMany(fetch = FetchType.EAGER, targetEntity=ContactPhone.class, mappedBy="customerId")    
    private List<ContactPhone> contactPhones;
}

代码很干净,更重要的是,我们取得了一些成就。现在我们可以更新我们的实体,而无需检索和填写所有电话数据。

处理小型实体和有界实体允许我们仅更新必要的字段。

这种方法的唯一不便是,我们应该有意识地设计我们的实体,而不是陷入过度设计的陷阱。

5. 自定义查询

我们可以实现的另一种方法是为部分更新定义自定义查询。

事实上,JPA 定义了两个注释,@Modifying和 @Query,允许我们显式地编写更新语句。

现在,我们可以告诉我们的应用程序在更新期间的行为方式,而不会给ORM留下负担。

让我们在存储库中添加自定义更新方法:

@Modifying
@Query("update Customer u set u.phone = :phone where u.id = :id")
void updatePhone(@Param(value = "id") long id, @Param(value = "phone") String phone);

现在我们可以重写我们的更新方法:

public void updateCustomerContacts(long id, String phone) {
    repo.updatePhone(id, phone);
}

我们现在能够执行部分更新。只需几行代码,无需更改实体,我们就实现了目标。

这种技术的缺点是,我们必须为对象的每个可能的部分更新定义一种方法。

6. 结论

部分数据更新是一项非常基本的操作;虽然我们可以让我们的 ORM 来处理它,但有时完全控制它是有利可图的。

正如我们所看到的,我们可以预加载数据,然后更新它或定义我们的自定义语句,但请记住要意识到这些方法所暗示的缺点以及如何克服它们。

像往常一样,本文的源代码可在GitHub 上找到。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值