HTTP PUT против HTTP PATCH в REST API

HTTP PUT против HTTP PATCH в REST API

1. обзор

В этой быстрой статье мы рассмотрим различия между командами HTTP PUT и PATCH и семантику этих двух операций.

Мы будем использовать Spring для реализации двух конечных точек REST, которые поддерживают эти два типа операций, а также для лучшего понимания различий и правильного способа их использования.

2. Когда использовать Put и когда Patch?

Начнем с простого и немного простого утверждения.

Когда клиенту необходимо полностью заменить существующий ресурс, он может использовать PUT. Когда они делают частичное обновление, они могут использовать HTTP PATCH.

Например, при обновлении одного поля ресурса отправка полного представления ресурса может быть громоздкой и использовать много ненужной полосы пропускания. В таких случаях семантика PATCH имеет гораздо больше смысла.

Еще один важный аспект, который следует учитывать, - этоidempotence; PUT is idempotent; PATCH can be, but isn’t required to. Итак, в зависимости от семантики реализуемой нами операции, мы также можем выбрать ту или иную на основе этой характеристики.

3. Реализация логики PUT и PATCH

Допустим, мы хотим реализовать REST API для обновленияHeavyResource с несколькими полями:

public class HeavyResource {
    private Integer id;
    private String name;
    private String address;
    // ...

Во-первых, нам нужно создать конечную точку, которая обрабатывает полное обновление ресурса, используя PUT:

@PutMapping("/heavyresource/{id}")
public ResponseEntity saveResource(@RequestBody HeavyResource heavyResource,
  @PathVariable("id") String id) {
    heavyResourceRepository.save(heavyResource, id);
    return ResponseEntity.ok("resource saved");
}

Это стандартная конечная точка для обновления ресурсов.

Теперь предположим, что поле адреса часто обновляется клиентом. В этом случаеwe don’t want to send the whole HeavyResource object with all fields, но нам нужна возможность обновлять только полеaddress - через метод PATCH.

Мы можем создать DTOHeavyResourceAddressOnly для представления частичного обновления поля адреса:

public class HeavyResourceAddressOnly {
    private Integer id;
    private String address;

    // ...
}

Далее мы можем использовать метод PATCH для отправки частичного обновления:

@PatchMapping("/heavyresource/{id}")
public ResponseEntity partialUpdateName(
  @RequestBody HeavyResourceAddressOnly partialUpdate, @PathVariable("id") String id) {

    heavyResourceRepository.save(partialUpdate, id);
    return ResponseEntity.ok("resource address updated");
}

С помощью этого более детализированного DTO мы можем отправить только поле, которое нам нужно обновить - без накладных расходов на отправку всегоHeavyResource.

Если у нас есть большое количество этих операций частичного обновления, мы также можем пропустить создание собственного DTO для каждого выхода - и использовать только карту:

@RequestMapping(value = "/heavyresource/{id}", method = RequestMethod.PATCH, consumes = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity partialUpdateGeneric(
  @RequestBody Map updates,
  @PathVariable("id") String id) {

    heavyResourceRepository.save(updates, id);
    return ResponseEntity.ok("resource updated");
}

Это решение даст нам больше гибкости в реализации API; однако мы также теряем несколько вещей - например, проверку.

4. Тестирование PUT и PATCH

Наконец, давайте напишем тесты для обоих методов HTTP. Сначала мы хотим протестировать обновление полного ресурса с помощью метода PUT:

mockMvc.perform(put("/heavyresource/1")
  .contentType(MediaType.APPLICATION_JSON_VALUE)
  .content(objectMapper.writeValueAsString(
    new HeavyResource(1, "Tom", "Jackson", 12, "heaven street")))
  ).andExpect(status().isOk());

Выполнение частичного обновления достигается с помощью метода PATCH:

mockMvc.perform(patch("/heavyrecource/1")
  .contentType(MediaType.APPLICATION_JSON_VALUE)
  .content(objectMapper.writeValueAsString(
    new HeavyResourceAddressOnly(1, "5th avenue")))
  ).andExpect(status().isOk());

Мы также можем написать тест для более общего подхода:

HashMap updates = new HashMap<>();
updates.put("address", "5th avenue");

mockMvc.perform(patch("/heavyresource/1")
    .contentType(MediaType.APPLICATION_JSON_VALUE)
    .content(objectMapper.writeValueAsString(updates))
  ).andExpect(status().isOk());

5. Обработка частичных запросов со значениямиNull

Когда мы пишем реализацию для метода PATCH, нам нужно указать контракт о том, как обрабатывать случаи, когда мы получаемnull как значение для поляaddress вHeavyResourceAddressOnly.

Предположим, что клиент отправляет следующий запрос:

{
   "id" : 1,
   "address" : null
}

Затем мы можем обработать это, установив значение поляaddress наnull или просто проигнорировав такой запрос, рассматривая его как без изменений.

Мы должны выбрать одну стратегию для обработкиnull и придерживаться ее при каждой реализации метода PATCH.

6. Заключение

В этом кратком руководстве мы сосредоточились на понимании различий между методами HTTP PATCH и PUT.

Мы реализовали простой контроллер Spring REST для обновления ресурса с помощью метода PUT и частичного обновления с помощью PATCH.

Реализация всех этих примеров и фрагментов кода можно найти вGitHub project - это проект Maven, поэтому его должно быть легко импортировать и запускать как есть.