Partial Data Update With Spring Data – 用Spring数据进行部分数据更新

最后修改: 2020年 6月 2日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

Spring Data’s CrudRespository#save is undoubtedly simple, but one feature could be a drawback: It updates every column in the table. Such are the semantics of the U in CRUD, but what if we want to do a PATCH instead?

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

In this tutorial, we’re going to cover techniques and approaches to performing a partial instead of a full update.

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

2. Problem

2 问题

As stated before, save() will overwrite any matched entity with the data provided, meaning that we cannot supply partial data. That can become inconvenient, especially for larger objects with a lot of fields.

如前所述,save()将用提供的数据覆盖任何匹配的实体,这意味着我们不能提供部分数据。这可能会变得很不方便,尤其是对于有很多字段的大型对象。

If we look at an ORM, some patches exist:

如果我们看一下ORM,存在一些补丁。

  • Hibernate’s @DynamicUpdate annotation, which dynamically rewrites the update query
  • JPA’s @Column annotation, as we can disallow updates on specific columns using the updatable parameter

But we’re going to approach this problem with specific intent: Our purpose is to prepare our entities for the save method without relying on an ORM.

但是我们将带着特定的意图来处理这个问题。我们的目的是为save方法准备我们的实体,而不依赖ORM。

3. Our Case

3.我们的案例

First, let’s build a Customer entity:

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

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

Then we define a simple CRUD repository:

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

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

Finally, we prepare a CustomerService:

最后,我们准备一个CustomerService

@Service 
public class CustomerService {
    @Autowired 
    CustomerRepository repo;

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

4. Load and Save Approach

4.加载和保存的方法

Let’s first look at an approach that is probably familiar: loading our entities from the database and then updating only the fields we need. It’s of the simplest approaches we can use.

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

Let’s add a method in our service to update the contact data of our customers.

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

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

We’ll call the findById method and retrieve the matching entity. Then we proceed and update the fields required and persist the data.

我们将调用findById方法,检索出匹配的实体。然后我们继续进行,更新所需的字段并持久化数据。

This basic technique is efficient when the number of fields to update is relatively small and our entities are rather simple.

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

What would happen with dozens of fields to update?

如果有几十个字段需要更新,会发生什么?

4.1. Mapping Strategy

4.1.制图策略

When our objects have a large number of fields with different access levels, it’s quite common to implement the DTO pattern.

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

Now suppose we have more than a hundred phone fields in our object. Writing a method that pours the data from DTO to our entity, as we did before, could be annoying and pretty unmaintainable.

现在,假设我们的对象中有超过100个phone字段。编写一个方法将数据从DTO倒入我们的实体,就像我们之前做的那样,可能会很烦人,而且相当不容易维护。

Nevertheless, we can get over this issue using a mapping strategy, and specifically with the MapStruct implementation.

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

Let’s create a CustomerDto:

让我们创建一个CustomerDto

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

And we’ll also create a CustomerMapper:

我们还将创建一个CustomerMapper

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

The @MappingTarget annotation lets us update an existing object, saving us the pain of writing a lot of code.

@MappingTarget 注解让我们更新一个现有的对象,省去了我们写大量代码的痛苦。

MapStruct has a @BeanMapping method decorator that lets us define a rule to skip null values during the mapping process.

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

Let’s add it to our updateCustomerFromDto method interface:

让我们把它添加到我们的updateCustomerFromDto方法接口。

@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)

With this, we can load stored entities and merge them with a DTO before calling JPA save method — in fact, we’ll update only the modified values.

利用这一点,我们可以在调用JPA save方法之前加载存储的实体并将其与DTO合并 – 事实上,我们将只更新修改后的值。

So, let’s add a method to our service, which will call our mapper:

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

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

The drawback of this approach is that we can’t pass null values to the database during an update.

这种方法的缺点是,我们不能在更新期间向数据库传递null值。

4.2. Simpler Entities

4.2.简单的实体

Finally, keep in mind that we can approach this problem from the design phase of an application.

最后,请记住,我们可以从应用程序的设计阶段来处理这个问题。

It’s essential to define our entities to be as small as possible.

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

Let’s take a look at our Customer entity.

让我们看一下我们的Customer实体。

We’ll structure it a little bit and extract all the phone fields to ContactPhone entities and be under a one-to-many relationship:

我们将结构化一点,将所有的电话字段提取到ContactPhone实体中,并处于one-to-many关系之下。

@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;
}

The code is clean and, more importantly, we achieved something. Now we can update our entities without having to retrieve and fill all the phone data.

代码很干净,更重要的是,我们取得了一些成果。现在我们可以更新我们的实体,而不必检索和填充所有的phone数据。

Handling small and bounded entities allows us to update only the necessary fields.

处理小的和有界限的实体使我们能够只更新必要的字段。

The only inconvenience of this approach is that we should design our entities with awareness, without falling into the trap of overengineering.

这种方法的唯一不便之处在于,我们应该有意识地设计我们的实体,不要落入过度工程的陷阱。

5. Custom Query

5.自定义查询

Another approach we can implement is to define a custom query for partial updates.

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

In fact, JPA defines two annotations, @Modifying and @Query, that allow us to write our update statement explicitly.

事实上,JPA定义了两个注解,@Modifying@Query,它们允许我们明确地编写更新语句。

We can now tell our application how to behave during an update, without leaving the burden on the ORM.

我们现在可以告诉我们的应用程序在更新过程中的行为,而无需将负担留给ORM。

Let’s add our custom update method in the repository:

让我们在版本库中添加我们的自定义更新方法。

@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);

Now we can rewrite our update method:

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

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

We are now able to perform a partial update. With just a few lines of code and without altering our entities, we’ve achieved our goal.

我们现在能够执行部分更新。仅仅用了几行代码,而且没有改变我们的实体,我们就实现了我们的目标。

The disadvantage of this technique is that we’ll have to define a method for each possible partial update of our object.

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

6. Conclusion

6.结语

The partial data update is quite a fundamental operation; while we can have our ORM to handle it, it can sometimes be profitable to get full control over it.

部分数据更新是一个相当基本的操作;虽然我们可以让我们的ORM来处理它,但有时获得对它的完全控制是有利的。

As we’ve seen, we can preload our data and then update it or define our custom statements, but remember to be aware of the drawbacks that these approaches imply and how to overcome them.

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

As usual, the source code for this article is available over on GitHub.

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