В чому різниця між 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.