В чём разница между PUT и PATCH
Оба метода — PUT и PATCH — используются в REST API для обновления существующих ресурсов на сервере, но имеют принципиально разную семантику и правила обработки данных.
🟢 Junior Level
Оба метода — PUT и PATCH — используются в REST API для обновления существующих ресурсов на сервере, но имеют принципиально разную семантику и правила обработки данных.
Главные различия
PUT(Полная замена ресурса):- Клиент передает полное представление ресурса.
- Все поля, которые не были переданы в теле запроса
PUT, сервер обязан удалить или сбросить вnull(значения по умолчанию). - Метод является строго идемпотентным (повторный запуск запроса с тем же телом не меняет состояние базы данных).
PATCH(Частичное обновление ресурса):- Клиент передает только те поля, которые требуется изменить.
- Все остальные поля ресурса остаются без изменений.
- Экономит сетевой трафик при работе с большими объектами.
- По спецификации не гарантирует идемпотентность (зависит от логики операции).
Наглядное сравнение запросов
Исходное состояние пользователя в базе данных:
{
"id": 42,
"name": "Alex",
"age": 25,
"email": "alex@example.com"
}
Запрос методом PUT (Полная замена):
PUT /api/v1/users/42 HTTP/1.1
Content-Type: application/json
{
"name": "Alex",
"age": 26
}
Результат в БД: поле email стерто в null, так как клиент не прислал его в запросе полной замены!
Запрос методом PATCH (Частичное обновление):
PATCH /api/v1/users/42 HTTP/1.1
Content-Type: application/json
{
"age": 26
}
Результат в БД: обновлено только поле age, поле email сохранило значение "alex@example.com".
🟡 Middle Level
Сравнительная таблица спецификаций (RFC 9110 vs RFC 5789)
| Критерий | PUT (RFC 9110) |
PATCH (RFC 5789) |
|---|---|---|
| Семантика действия | Замещение целевого ресурса предоставленным состоянием | Применение набора изменений к целевому ресурсу |
| Идемпотентность | ✅ Строго гарантируется спецификацией | ❌ Не гарантируется (зависит от типа патча) |
| Безопасность (Safe) | ❌ Нет (модифицирует данные) | ❌ Нет (модифицирует данные) |
| Стандартный Content-Type | application/json, application/xml |
application/merge-patch+json или application/json-patch+json |
| Аналогия в SQL | UPDATE users SET name=?, age=?, email=? |
UPDATE users SET age=? |
Две спецификации стандарта PATCH
В индустрии существует два стандарта реализации метода PATCH:
1. JSON Merge Patch (RFC 7386) — Декларативный подход
Клиент отправляет частичный JSON-документ с измененными полями (Content-Type: application/merge-patch+json).
Сервер просто накладывает пришедшие ключи поверх существующего объекта.
Недостаток: сложно отличить «поле не передано (оставить как есть)» от «поле передано со значением null (стереть значение в БД)».
2. JSON Patch (RFC 6902) — Императивный подход
Клиент отправляет массив атомарных инструкций (Content-Type: application/json-patch+json):
[
{ "op": "replace", "path": "/age", "value": 26 },
{ "op": "remove", "path": "/temporaryToken" },
{ "op": "add", "path": "/tags/-", "value": "premium" }
]
Поддерживает операции: add, remove, replace, move, copy, test.
🔴 Senior Level
Реализация PUT и PATCH в Spring Boot 3
package com.example.controller;
import com.example.dto.ReplaceUserRequest;
import com.example.dto.UserResponseDto;
import com.example.service.UserService;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/v1/users")
public class UserController {
private final UserService userService;
private final ObjectMapper objectMapper;
public UserController(UserService userService, ObjectMapper objectMapper) {
this.userService = userService;
this.objectMapper = objectMapper;
}
// 1. PUT: Полная замена (все поля валидируются, отсутствующие сбрасываются)
@PutMapping("/{id}")
public ResponseEntity<UserResponseDto> replaceUser(@PathVariable Long id,
@RequestBody ReplaceUserRequest request) {
return ResponseEntity.ok(userService.replaceUser(id, request));
}
// 2. PATCH: Частичное обновление через JSON Merge Patch
@PatchMapping(path = "/{id}", consumes = "application/merge-patch+json")
public ResponseEntity<UserResponseDto> patchUser(@PathVariable Long id,
@RequestBody JsonNode patchNode) {
return ResponseEntity.ok(userService.patchUser(id, patchNode));
}
}
package com.example.service;
import com.example.dto.ReplaceUserRequest;
import com.example.dto.UserResponseDto;
import com.example.entity.User;
import com.example.repository.UserRepository;
import com.fasterxml.jackson.databind.JsonNode;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Transactional
public UserResponseDto replaceUser(Long id, ReplaceUserRequest req) {
User user = userRepository.findById(id).orElseThrow();
// PUT перезаписывает ВСЕ поля, включая null для непереданных
user.setName(req.name());
user.setAge(req.age());
user.setEmail(req.email()); // Если req.email() == null, поле затирается!
return UserResponseDto.fromEntity(user);
}
@Transactional
public UserResponseDto patchUser(Long id, JsonNode patchNode) {
User user = userRepository.findById(id).orElseThrow();
// Анализируем только реально пришедшие в JSON узлы:
if (patchNode.has("name")) {
user.setName(patchNode.get("name").asText());
}
if (patchNode.has("age")) {
user.setAge(patchNode.get("age").asInt());
}
if (patchNode.has("email")) {
// Если в JSON пришел явный "email": null — затираем, иначе не трогаем
user.setEmail(patchNode.get("email").isNull() ? null : patchNode.get("email").asText());
}
return UserResponseDto.fromEntity(user);
}
}
Проблема “Three-State Null Problem” в Java DTO
При реализации PATCH библиотека Jackson по умолчанию преобразует и отсутствующее поле, и поле со значением null в обычный null в Java объекте.
Для строгого разделения трех состояний используют библиотеку JsonNullable:
public class PatchUserDto {
// 1. Поле отсутствует в JSON -> jsonNullable.isUndefined() == true
// 2. В JSON пришел "email": null -> jsonNullable.isPresent() && jsonNullable.get() == null
// 3. В JSON пришел "email": "a@b.c" -> jsonNullable.isPresent() && jsonNullable.get() == "a@b.c"
private JsonNullable<String> email = JsonNullable.undefined();
}
Почему PATCH может быть НЕИДЕМПОТЕНТНЫМ
Хотя PUT всегда идемпотентен, метод PATCH может нарушать идемпотентность в зависимости от выполняемой операции:
- Идемпотентный PATCH:
{ "op": "replace", "path": "/status", "value": "ACTIVE" }Сколько бы раз ни повторялся запрос, статус останется
ACTIVE. - Неидемпотентный PATCH:
{ "op": "add", "path": "/tags/-", "value": "vip" }Каждый повторный вызов будет добавлять новый элемент
"vip"в конец массиваtags(["vip", "vip", "vip"]). - Операция инкремента:
{ "op": "increment", "path": "/balance", "value": 100 }Каждый повторный вызов увеличивает баланс еще на 100 единиц.
🎯 Шпаргалка для интервью
30-секундный ответ
PUT(RFC 9110) предназначен для полной замены целевого ресурса. Клиент обязан передать все поля сущности; опущенные поля сервер обязан занулить или сбросить. Метод всегда идемпотентен.PATCH(RFC 5789) предназначен для частичного обновления. Передаются только изменяемые поля (экономия сетевого трафика). Метод не гарантирует идемпотентность (например, добавление элемента в массив или операция инкремента).- Реализуется по двум стандартам: JSON Merge Patch (RFC 7386) (декларативное слияние) и JSON Patch (RFC 6902) (массив атомарных команд
add,remove,replace).
4 каверзных вопроса с ответами
-
Что обязан сделать сервер по спецификации HTTP, если клиент в запросе
PUTне прислал одно из необязательных полей? Ответ: Сервер обязан перезаписать существующий ресурс переданным представлением целиком. Неприсланные поля должны быть сброшены вnullили значения по умолчанию. Если требуется обновить только часть полей, не затрагивая остальные, клиент обязан использоватьPATCH. -
Приведите пример неидемпотентного запроса
PATCH. Ответ: Инструкция добавления элемента в список[{"op": "add", "path": "/items/-", "value": "item1"}]или команда увеличения счетчика[{"op": "increment", "value": 5}]. При каждом повторе такого запроса состояние массива или счетчика в базе данных будет изменяться. -
В чём разница между стандартами RFC 7386 и RFC 6902 для метода PATCH? Ответ: RFC 7386 (JSON Merge Patch) — декларативный подход: клиент шлет обычный частичный JSON, и сервер объединяет его с оригиналом. RFC 6902 (JSON Patch) — императивный подход: клиент передает массив точных операций (
replace,add,remove,move,copy,test) с путями JSON Pointer. -
Как в Java решить проблему различия «поле не передано» и «поле явно передано как null» при обработке PATCH? Ответ: С помощью обертки
JsonNullable<T>(библиотекаjackson-databind-nullable) либо путем парсинга тела запроса в древовидную структуру JacksonJsonNodeс проверкой методовpatchNode.has("field")иnode.isNull().
Красные флаги (чего категорически нельзя говорить)
- ❌ «
PUTиPATCH— это абсолютно одно и то же, просто разные названия» — фундаментальная ошибка понимания семантики HTTP. - ❌ «
PATCHвсегда идемпотентен, так же как иPUT» —PATCHне гарантирует идемпотентность по RFC 5789. - ❌ «
PUTобновляет только те поля, которые прислали, а остальные оставляет нетронутыми» — это описаниеPATCH, а неPUT. - ❌ «Для
PATCHне существует официальных стандартов, каждый пишет как хочет» — существуют официальные спецификации RFC 7386 и RFC 6902.