Eric Rybarczyk

Eric Rybarczyk

Enthusiastic and motivated software engineer with diverse experience

AWS & Java Certified Developer

To Put or to Patch

Updating Data in a Spring Boot REST API

Eric Rybarczyk

Patch cables with audio equipment
Feature photo by Steve Harvey on Unsplash

When the time comes to handle data updates via a REST API, we may have a choice to make. The list of HTTP verbs, which are the basis for REST operations, includes both PUT and PATCH. Each of these provide a way to update existing resources, and we will cover both in this article.

Guide to the Series: REST Fundamentals with Spring Boot

 

You might be wondering why there are two different ways to update a resource. Which should be used, and in what circumstances?

The difference between these two methods is related to the scope of the data being passed in to the update process. PUT replaces the entire resource, while PATCH updates only the resource fields included in the request. I tend to think of PUT in terms of placing a physical object in a certain location, such as placing a book on a bookshelf. On the other hand, I think of PATCH in terms of installing a software patch to change one aspect of a larger, preexisting software program.

Which should you choose? The details of your data and use cases will influence you choices here. The only universal answer is "It depends." You may also have use cases for each of these in your service.

Be aware that PUT is idempotent, while PATCH is not (though it can be, and often is in practice). You can find more information in two excellent answers to this question on Stack Overflow.

The PUT Method

You’ll notice we are using a new type, WorkOrderDto, rather than the WorkOrderRequest which was used in the POST method. This illustrates a realistic scenario where subtle differences exist between the requirements for creating new resources and updating existing ones. Here, the existing resource includes the id and createdDateTime fields. You can imagine that these fields could be meaningful to a client application working with existing data, while they are not appropriate to include from the client when creating a new resource.

DTO Object
package com.springbikeclinic.api.web.model;

import lombok.Builder;
import lombok.Data;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.time.OffsetDateTime;

@Data
@Builder
public class WorkOrderDto {

  @NotNull
  private Long id;

  @NotNull
  private OffsetDateTime createdDateTime;

  @NotEmpty
  private String bikeDescription;

  @NotEmpty
  private String workDescription;

  @NotEmpty
  private String status;

  private String mechanicNotes;
}

We provide the id field via @PathVariable on the method parameter, mainly because defining the target resource by location is consistent with REST principles - it is consistent with how the resource is located in a GET request.

@PutMapping("/{id}")
public ResponseEntity<WorkOrderDto> putWorkOrder(@PathVariable("id") Long id, 
                                                 @Valid @RequestBody WorkOrderDto workOrderDto) {

  return ResponseEntity.ok(workOrderService.updateWorkOrder(id, workOrderDto));
}

The implementation of this controller method is quite simple, as you see above. The method returns a 200 response code, which is consistent with the choice to return the updated resource in the response body. A 204 No Content response code should be used if you return an empty response body.

The logic to update the resource is delegated to the WorkOrderService which is shown below. To be consistent with the definition of PUT, all fields are updated in order to effectively replace the entire resource. However, we are not literal about "all fields" since we do not update the id or createdDateTime fields. This is simply a matter of ensuring data integrity in terms of our business rules.

Service Implementation
@Service
@RequiredArgsConstructor
public class WorkOrderServiceImpl implements WorkOrderService {

  private final WorkOrderRepository workOrderRepository;
  private final WorkOrderMapper workOrderMapper;

  // other code omitted

  @Override
  @Transactional
  public WorkOrderDto updateWorkOrder(Long id, WorkOrderDto workOrderDto) {
    WorkOrder existingWorkOrder = workOrderRepository
              .findById(id)
              .orElseThrow(() -> 
                  new NotFoundException(String.format("Work Order not found for Id %s", id)));

    existingWorkOrder.setBikeDescription(workOrderDto.getBikeDescription());
    existingWorkOrder.setWorkDescription(workOrderDto.getWorkDescription());
    existingWorkOrder.setMechanicNotes(workOrderDto.getMechanicNotes());
    existingWorkOrder.setStatus(workOrderDto.getStatus());

    WorkOrder result = workOrderRepository.save(existingWorkOrder);

    return workOrderMapper.workOrderToWorkOrderDto(result);
  }
}

Unit Tests

Unit testing the controller method is straightforward, and quite similar to the test for the POST method. We generate a JSON representation of the WorkOrderDto input object. We prepare the mock WorkOrderService using Mockito.when(). As usual, Spring’s MockMvc class allows us to perform a mock PUT request, and we assert that a 200 response is returned. Finally, Mockito verifies the service mock was called as expected.

@Test
void putWorkOrder_validInput_isUpdatedAndOk() throws Exception {
  String jsonExistingWorkOrderDto = objectMapper.writeValueAsString(existingWorkOrderDto);
  when(workOrderService.updateWorkOrder(anyLong(), any(WorkOrderDto.class)))
                                       .thenReturn(existingWorkOrderDto);

  mockMvc.perform(put(WORK_ORDERS_BASE_PATH + WORK_ORDER_EXISTING_SUB_PATH)
          .contentType(MediaType.APPLICATION_JSON)
          .content(jsonExistingWorkOrderDto))
          .andExpect(status().isOk());

  verify(workOrderService, times(1)).updateWorkOrder(anyLong(), any(WorkOrderDto.class));
}

 

Proper testing of the service method is a little more interesting. Much like the controller delegates to a service, a service often delegates to a repository, at least for persistence operations. If we followed the same approach as we established with the controller test above, such as mocking the WorkOrderRepository and asserting on the result, we could end up simply verifying the field values on our mock return object. We’d really be testing our use of Mockito rather than our service implementation.

Instead, we want to test that the values passed to the repository as a WorkOrder object match the values provided to the method in the WorkOrderDto parameter. Mockito provides the ArgumentCaptor<T> so we can assert on the actual values passed to a mocked method.

An ArgumentCaptor<T> is passed as a parameter to Mockito verification methods, and this captures the value passed to those methods to allow assertions to confirm the values used when those calls were made. Another option would be an integration test where we actually exercise these components instead of mocking them out, and a real project would almost certainly use both approaches.

@Test
void updateWorkOrder_validInput_savesAllValues() throws Exception {
  when(workOrderRepository.findById(anyLong())).thenReturn(Optional.of(existingWorkOrder));

  ArgumentCaptor<WorkOrder> workOrderDtoArgumentCaptor = ArgumentCaptor.forClass(WorkOrder.class);
  ArgumentCaptor<Long> idArgumentCaptor = ArgumentCaptor.forClass(Long.class);

  // set up the WorkOrderDto with alternate values
  Long existingId = existingWorkOrderDto.getId();
  String diffBikeDescription = "Altered Bike";
  String diffWorkDescription = "Altered Work";
  String diffMechanicNotes = "Altered Notes";
  String diffStatus = "closed";

  existingWorkOrderDto.setBikeDescription(diffBikeDescription);
  existingWorkOrderDto.setWorkDescription(diffWorkDescription);
  existingWorkOrderDto.setMechanicNotes(diffMechanicNotes);
  existingWorkOrderDto.setStatus(diffStatus);

  // Call the SUT method
  workOrderService.updateWorkOrder(existingId, existingWorkOrderDto);

  // mockito verify and capture arguments
  verify(workOrderRepository, times(1)).findById(idArgumentCaptor.capture());
  verify(workOrderRepository, times(1)).save(workOrderDtoArgumentCaptor.capture());

  // assert that the values on the captured arguments are the expected values
  assertThat(idArgumentCaptor.getValue()).isEqualTo(existingId);
  WorkOrder capturedWorkOrder = workOrderDtoArgumentCaptor.getValue();
  assertThat(capturedWorkOrder.getBikeDescription()).isEqualTo(diffBikeDescription);
  assertThat(capturedWorkOrder.getWorkDescription()).isEqualTo(diffWorkDescription);
  assertThat(capturedWorkOrder.getMechanicNotes()).isEqualTo(diffMechanicNotes);
  assertThat(capturedWorkOrder.getStatus()).isEqualTo(diffStatus);
}

 

The PATCH Method

Finally, back to the title of this post: what about the PATCH method?

As noted earlier, PATCH updates only the resource fields included in the request, rather than the complete resource. One approach to support this variability in parameter values is to accept a Map<K, V> with the Map keys being the field names to be updated on the WorkOrder.

@PatchMapping("/{id}") 
public ResponseEntity<WorkOrderDto> patchWorkOrder(@PathVariable("id") Long id, 
                                                   @RequestBody Map<String, Object> patch) {
  return ResponseEntity.ok(workOrderService.patchWorkOrder(id, patch));
}

We then use a bit of reflection in the WorkOrderService implementation to apply the Map values to the corresponding fields in the WorkOrder object.

@Override
@Transactional
public WorkOrderDto patchWorkOrder(Long id, Map<String, Object> patch) {
  WorkOrder existingWorkOrder = workOrderRepository.findById(id)
          .orElseThrow(() -> 
              new NotFoundException(String.format("Work Order not found for Id %s", id)));

  patch.forEach( (key, value) -> {
    Field field = ReflectionUtils.findField(WorkOrder.class, key);
    if(field != null) {
        field.setAccessible(true);
        ReflectionUtils.setField(field, existingWorkOrder, value);
    }
  });

  WorkOrder result = workOrderRepository.save(existingWorkOrder);

  return workOrderMapper.workOrderToWorkOrderDto(result);
}

Unit Tests

When it comes to unit testing this PATCH support we test both the controller and the service. For the controller we simply need to make a MockMvc call to perform a PATCH call with an appropriate Map<String, Object> in JSON format. In addition to the usual check for a 200 response, we also use Mockito to verify that the controller called the expected method in the WorkOrderService.

@Test
void patchWorkOrder_updateSingleField_isUpdatedAndOk() throws Exception {
  Map<String, Object> patch = new HashMap<>();
  patch.put("mechanicNotes", "Patched up your bike");
  String jsonInput = objectMapper.writeValueAsString(patch);

  when(workOrderService.patchWorkOrder(anyLong(), any())).thenReturn(existingWorkOrderDto);

  mockMvc.perform(patch(WORK_ORDERS_BASE_PATH + WORK_ORDER_EXISTING_SUB_PATH)
                  .contentType(MediaType.APPLICATION_JSON)
                  .content(jsonInput))
          .andExpect(status().isOk());

  verify(workOrderService, times(1)).patchWorkOrder(anyLong(), any());
}

Unit testing the service is a little more involved, because we need to assert that the expected values were passed to the JpaRepository. To accomplish this we again make use of Mockito’s ArgumentCaptor<T>, for both parameters passed to the repository method.

@Test
void patchWorkOrder_singleField_updatesField() throws Exception {
  when(workOrderRepository.findById(anyLong())).thenReturn(Optional.of(existingWorkOrder));

  Long existingId = existingWorkOrderDto.getId();
  String diffMechanicNotes = "Altered Notes";
  Map<String, Object> patch = new HashMap<>();
  patch.put("mechanicNotes", diffMechanicNotes);

  ArgumentCaptor<WorkOrder> workOrderDtoArgumentCaptor = ArgumentCaptor.forClass(WorkOrder.class);
  ArgumentCaptor<Long> idArgumentCaptor = ArgumentCaptor.forClass(Long.class);

  // Call the SUT method
  workOrderService.patchWorkOrder(existingId, patch);

  // mockito verify and capture arguments
  verify(workOrderRepository, times(1)).findById(idArgumentCaptor.capture());
  verify(workOrderRepository, times(1)).save(workOrderDtoArgumentCaptor.capture());

  // assert that the values on the captured arguments are the expected values
  assertThat(idArgumentCaptor.getValue()).isEqualTo(existingId);
  WorkOrder capturedWorkOrder = workOrderDtoArgumentCaptor.getValue();
  assertThat(capturedWorkOrder.getMechanicNotes()).isEqualTo(diffMechanicNotes);
}

It’s Good To Have Options

If you’re not a fan of using a Map<String, Object> with reflection to accept nearly any input parameters, another option is JSON Patch which relates to a set of RFCs from the IETF. JSON Patch defines a set of operations like add, remove and more.

JSON Patch defines a JSON document structure for expressing a sequence of operations to apply to a JavaScript Object Notation (JSON) document; it is suitable for use with the HTTP PATCH method. The “application/json-patch+json” media type is used to identify such patch documents.

JavaScript Object Notation (JSON) Patch

Moving Forward

Now that we can handle updates to selective fields in our WorkOrder data, all that is left is to decide how we want to handle deleting data.


Article Series

Software Projects

Recent Posts

Categories

About

Enthusiastic and motivated software engineer