The PUT
method described in the last lesson updates the whole record. There may be a scenario when only one or two fields needs to be updated. In that case, sending the whole record does not make sense. The HTTP PATCH
method is used for partial updates.
Sometimes we may need to update a single field. The player
entity we've been using so far only has a few fields and PUT
can be used for update. But if the entity is large and contains nested objects, it will have a performance impact to send the whole entity only to update a single field.
In our example, partial request means that we only send the titles
in the request body instead of the whole Player
object. If we use PUT
to send a partial request, the titles
will be modified but all other fields are set to null. PATCH
comes in handy in such situations.
In the PlayerService
class, we implement a method to handle partial updates to the Player
object. The method patch
has two arguments: id
of the player on which the patch is to be applied, and the Map
containing the key-value pairs of the fields that will be updated. The key (field name) is a String
while the value is an Object
as it can have different data types.
We use the id
to fetch the existing Player
object from the database using the findById
method of the JpaRepository
, which loads the entity from the database. The findById
method returns an Optional
and we need to check if a Player
object is returned using the isPresent()
method.
public Player patch(int id, Map<String, Object> partialPlayer) {
Optional<Player> player = repo.findById(id);
if (player.isPresent()){
//update fields using Map
}
return repo.save(player);
}
Next, we loop through the Map
, find the field that will be updated, and then change the value of that field in the existing Player
object that we have retrieved from the database.
The Reflection API is used to examine and modify fields, methods, and classes at runtime. It allows access to the private fields of a class and can be used to access the fields irrespective of their access modifiers. Spring provides the ReflectionUtils
class for handling reflection and working with the Reflection API.
-
The
ReflectionUtils
class has afindField
method to identify the field of an object using aString
name.The
findField
method takes two arguments, the class having the field and the name of the field which in our case is contained in the variablekey
.This method returns a
Field
object.Field field = ReflectionUtils.findField(Player.class, key);
-
To set a value for this
field
, we need to set the field’s accessible flag totrue
.ReflectionUtils
setAccessible
method, when called on a field, toggles its accessible flag.We can also use another method called
makeAccessible
. This method makes the givenfield
accessible by calling thesetAccessible(true)
method if necessary.ReflectionUtils.makeAccessible(field);
-
Lastly, we call the
setField
method and use the value from theMap
to set the field in theplayer
object.The
setField
method takes three arguments, the reference of the field, the object in which the field is to be set, and the value to set. This method requires that the given field is accessible.ReflectionUtils.setField(field, player.get(), value);
Here, we have used the
get
method on theOptional
player object to retrieve it.
In this way using reflection, a field can be updated in an object. Since we are passing the fields to be updated as a Map
, we use the above steps while iterating through the map of key-value pairs as follows:
partialPlayer.forEach( (key, value) -> {
Field field = ReflectionUtils.findField(Player.class, key);
ReflectionUtils.makeAccessible(field);
ReflectionUtils.setField(field, player.get(), value);
});
At the end we call the save
method to update the player
record. The complete code of the method is shown below:
public Player patch( int id, Map<String, Object> partialPlayer) {
Optional<Player> player = repo.findById(id);
if(player.isPresent()) {
partialPlayer.forEach( (key, value) -> {
Field field = ReflectionUtils.findField(Player.class, key);
ReflectionUtils.makeAccessible(field);
ReflectionUtils.setField(field, player.get(), value);
});
}
return repo.save(player.get());
}
Now, we write a controller method called partialUpdate
to handle PATCH
mapping. It has an @PatchMapping
annotation with endpoint /players/{id}
, where id
is a path variable. The method takes a Map
argument containing the key-value pair of the fields we want to update.
Since the field names are String
and the values can be any datatype, we will use Map<String, Object>
. The list of fields and their values will come in the request body and the @RequestBody
annotation binds the JSON to the Map
variable.
@PatchMapping("/players/{id}")
public Player partialUpdate( @PathVariable int id,
@RequestBody Map<String, Object> playerPatch) {
//call service layer method for patch
}
The @PatchMapping
is a shortcut annotation. It is the same as:
@RequestMapping(method = RequestMethod.PATCH);
Inside the method, we simply delegate the call to the service method and pass the player id
and the Map
with the fields to be updated, along with their values.
@PatchMapping("/players/{id}")
public Player partialUpdate(@PathVariable int id, @RequestBody Map<String, Object> playerPatch) {
return service.patch(id, playerPatch);
}
To test, use Postman to send a PATCH
request to localhost:8080/players/1
. The request body in JSON format will look like this:
{
"titles": 161
}
After the request has been sent, the player with id
1 now has titles 161. This can be verified by creating a GET
request to localhost:8080/players/1
.
We can also verify by making a GET
request to localhost:8080/players
. Notice that player with id
1 has been partially updated in the table.
A point to note here is that we have used the save
method to applying the patch. This method updates all the columns in the table. For large objects with a lot of fields, this can have a performance impact. To avoid this, we can implement queries for partial updates. These queries can target frequently updated columns.
If we want to update the titles
column of the player
table, we can create an updateTitles
method implementing a query in the PlayerRepository
interface. This method takes two arguments, the id
of the player and the titles
.
The @Query
annotation is used to implement an update query as follows:
@Modifying
@Query("update Player p set p.titles = :titles where p.id = :id")
void updateTitles(@Param("id") int id, @Param("titles") int titles);
The query must be used with the @Modifying
annotation to execute the UPDATE
query.
The @Param
annotation binds the method parameters to the query. This method will only change a single column of the table unlike the save
method which updates all the columns of the table.
After writing the repository method, we move to the service layer. The service layer will implement the updateTitles
method as follows:
@Transactional
public void updateTitles(int id, int titles) {
repo.updateTitles(id, titles);
}
The @Transactional
annotation ensures that the database is left in a consistent state. The transaction is either committed or rolled back in case of failure.
Now, in the PlayerController
class, we can define a new PATCH
mapping for /players/{id}/titles
as follows:
@PatchMapping("/players/{id}/titles")
public void updateTitles(@PathVariable int id, @RequestBody int titles) {
service.updateTitles(id, titles);
}
id
is the path variable. This method accepts an integer from the request body.
To test the new endpoint, we can send a PATCH
request to localhost:8080/players/1/titles
. The request body in JSON format contains an integer value of the title count:
157
The REST API responds with 200
status code which indicates that the request was successful.
After the PATCH
request has been sent, the player with id
1 now has titles 157. This can be verified by creating a GET
request to localhost:8080/players/1
.
We can also verify by making a GET
request to localhost:8080/players
. Notice that player with id
1 has been partially updated in the table with title count of 157.