How to implement partial data update in a Java REST service without hassle and dust: the first encouter
Disclaimer: This is the one story in a series of stories related to the topic of implementing partial data updates in Java REST services. Due to the widespread use of the Spring Boot framework in my work experience, this story will be dedicated to implementing partial data updates in a REST service using the Spring Boot framework. However, this experience can also be implemented in other frameworks or without them (using the mentioned libraries), but it may not be relevant.
In the world of web services, one of the most common tasks is managing objects using the standard CRUD operations (Create, Read, Update, and Delete), which correspond to HTTP methods POST, GET, PUT, and DELETE in the REST architecture. However, there are scenarios where a developer may need to partially modify an object, and for this task, the HTTP method PATCH exists. Its primary idea is to modify on the server only those fields of an object that are included in the request. This requirement is particularly useful when an object has a large number of fields, and not all of them need to be changed.
There are numerous ways to implement the PATCH method with partial data updates, some of which I have personally encountered, such as:
- Using JSON Patch and JSON Merge Patch.
- Using Map<String, Object> in the request body.
- Using JsonNode in the request body.
- Using DTOs and coding tricks.
- And others.
Using DTOs and coding tricks.
In this story, I will attempt to share my experience in organizing partial updates of objects using the DTO (Data Transfer Object) pattern, optional fields (Optional<>), and Jackson ObjectMapper. This approach can be beneficial for those who use the DTO pattern in their projects and also enables encapsulating the logic of partial entity updates while focusing on the business logic in the code.
The full example of implementing the PATCH method using this approach can be found here.
1 — First, you need to annotate the entity with @JsonProperty
so that the Jackson ObjectMapper can map fields between DTO and Entity classes.
@Entity(name = "Person")
@Table(name = "person")
public class Person implements Serializable {
@JsonProperty("uid")
@Id
@Column(
name = "uid",
insertable = false,
nullable = false
)
private Long uid;
@JsonProperty("first_name")
@Column(
name = "first_name",
nullable = false
)
private String firstName;
@JsonProperty("last_name")
@Column(
name = "last_name",
nullable = true
)
private String lastName;
@JsonProperty("age")
@Column(
name = "age",
nullable = false
)
private Integer age;
//getters and setters && constructors
}
2 — Next, let’s consider the DTO class for this entity:
- The class is annotated with
@JsonInclude(JsonInclude.Include.NON_NULL)
. - It is also annotated with
@JsonProperty
with names corresponding to the fields. - The fields are declared as
Optional<?>
fields.
In this case, the Jackson ObjectMapper will be able to understand which fields were explicitly sent from the frontend as null (meaning they need to be set to null) or were not sent from the frontend (no need to update these fields). The fields in the DTO will be set as Optional.empty()
or null
, respectively.
@JsonInclude(JsonInclude.Include.NON_NULL)
public class PersonDetail {
@JsonProperty("uid")
private Optional<Long> uid;
@JsonProperty("first_name")
private Optional<String> firstName;
@JsonProperty("last_name")
private Optional<String> lastName;
@JsonProperty("age")
private Optional<Integer> age;
//getters and setters && constructors
}
3 — That’s it; all the rest of the magic is left to the Jackson ObjectMapper. Let’s look at an example of implementing the PATCH method for the described entity. For this we have to use method updateValue(targetObject, fromObject) of the Jackson ObjectMapper. We will implement all the logic in the controller for the simplicity of the example, to avoid cluttering the code with many classes as in a real project. Where userRepository
is a regular JpaRepository, and objectMapper
is the standard ObjectMapper.
@PatchMapping(path = "/{uid}")
public PersonDetail update(
@ApiParam(
name="uid",
value = "The updatePersonRequest entity uid",
required = true
) @PathVariable Long uid,
@ApiParam(
name="updatePersonRequest",
value = "The updatePersonRequest entity model",
required = true
) @RequestBody PersonDetail updatePersonRequest) {
// (2)
Person people = this.peopleRepository.findById(uid)
.orElseThrow(() -> new RuntimeException("Person with uid" + uid "is not existed"));
// (3)
objectMapper.updateValue(people, updatePersonRequest);
// (4)
peopleRepository.save(people);
//other logics may-be and return result
}
In this example, you can see how:
- The method takes a DTO with data from the frontend (all fields can be sent, or just one of them, or a combination).
- The entity is looked up in the database by its identifier.
- DTO fields (1) are mapped to the fields of the found entity (2). Explicitly specified null fields are mapped to null, and those that were not sent are skipped and remain as they were (2).
- The entity is updated in the database.
Partial data update is a fundamental operation; while ORM can take care of it, sometimes it can be advantageous to have full control over it. As we’ve seen, we can implement this operation by encapsulating all the logic of partial updates (determining which fields need to be updated, etc.) and focusing on the business logic code.
I would like to remind you that the source code for this stiry is available on GitHub.