← Назад к вопросам

Зачем нужен PATCH запрос?

1.2 Junior🔥 141 комментариев
#REST API и HTTP

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Зачем нужен PATCH запрос

PATCH и PUT часто путают. Оба используют HTTP метод POST-подобный для обновления, но они принципиально отличаются философией и применением.

PATCH vs PUT: ключевое различие

PUT: Полное замещение (Replace)

PUT /users/123 с телом {"name": "Alice", "email": "alice@example.com"}

Текущее состояние:
{
  "id": 123,
  "name": "Bob",
  "email": "bob@example.com",
  "age": 30,
  "verified": true
}

После PUT:
{
  "id": 123,  // id НЕ меняется (primary key)
  "name": "Alice",  // ← заменено
  "email": "alice@example.com",  // ← заменено
  "age": null,  // ← стёрто (не было в запросе)
  "verified": null  // ← стёрто
}

Проблема: потеряем age и verified!

PATCH: Частичное обновление (Merge)

PATCH /users/123 с телом {"name": "Alice", "email": "alice@example.com"}

Текущее состояние:
{
  "id": 123,
  "name": "Bob",
  "email": "bob@example.com",
  "age": 30,
  "verified": true
}

После PATCH:
{
  "id": 123,  // не меняется
  "name": "Alice",  // ← обновлено
  "email": "alice@example.com",  // ← обновлено
  "age": 30,  // ← осталось (не было в запросе, не меняется)
  "verified": true  // ← осталось
}

Преимущество: сохраняем остальные поля!

Реальный пример: обновление профиля пользователя

# ❌ PUT опасен для частичных обновлений
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

app = FastAPI()

class UserUpdate(BaseModel):
    name: str | None = None
    email: str | None = None
    age: int | None = None
    verified: bool | None = None

@app.put("/users/{user_id}")
def update_user_put(user_id: int, data: UserUpdate):
    user = get_user(user_id)
    
    # PUT ожидает ВСЕХ полей
    # Если data.age == None, мы сотрём age
    user.name = data.name  # Если None — стираем
    user.email = data.email  # Если None — стираем
    user.age = data.age  # ❌ Проблема
    user.verified = data.verified  # ❌ Проблема
    
    db.session.commit()
    return user

# Клиент хочет только обновить name:
PUT /users/123
{"name": "Alice"}

# Результат: age и verified становятся NULL!
# Потеря данных!

# ✅ PATCH правильен для частичных обновлений
@app.patch("/users/{user_id}")
def update_user_patch(user_id: int, data: UserUpdate):
    user = get_user(user_id)
    
    # PATCH: обновляем только переданные поля
    if data.name is not None:
        user.name = data.name
    if data.email is not None:
        user.email = data.email
    if data.age is not None:
        user.age = data.age
    if data.verified is not None:
        user.verified = data.verified
    
    db.session.commit()
    return user

# Клиент отправляет:
PATCH /users/123
{"name": "Alice"}

# Результат: только name меняется, остальное остаётся!
# Безопасно!

Правильная реализация PATCH

from typing import Any
from pydantic import BaseModel

class UserUpdate(BaseModel):
    """Update schema для PATCH"""
    name: str | None = None
    email: str | None = None
    age: int | None = None
    verified: bool | None = None

@app.patch("/users/{user_id}")
def patch_user(user_id: int, update_data: UserUpdate):
    user = User.query.get(user_id)
    if not user:
        raise HTTPException(status_code=404, detail="User not found")
    
    # Способ 1: Через dict.update (чистый и Pythonic)
    update_dict = update_data.model_dump(exclude_unset=True)  # Только переданные поля
    for key, value in update_dict.items():
        setattr(user, key, value)
    
    db.session.commit()
    return user

# Способ 2: SQLAlchemy update
from sqlalchemy import update

@app.patch("/users/{user_id}")
def patch_user_v2(user_id: int, update_data: UserUpdate):
    update_dict = update_data.model_dump(exclude_unset=True)
    
    db.session.execute(
        update(User).where(User.id == user_id).values(**update_dict)
    )
    db.session.commit()
    
    return User.query.get(user_id)

# Способ 3: Явно для каждого поля (verbose, но понятно)
@app.patch("/users/{user_id}")
def patch_user_v3(user_id: int, update_data: UserUpdate):
    user = User.query.get(user_id)
    
    # Обновляем только если значение передано
    if update_data.name is not None:
        user.name = update_data.name
    if update_data.email is not None:
        user.email = update_data.email
    if update_data.age is not None:
        user.age = update_data.age
    if update_data.verified is not None:
        user.verified = update_data.verified
    
    db.session.commit()
    return user

Различия в таблице

АспектPUTPATCH
СемантикаПолное замещениеЧастичное обновление
ТребованияВсе поля обязательныТолько изменённые поля
БезопасностьМожет привести к потере данныхБезопаснее
ИдемпотентностьИдемпотентенМожет быть не идемпотентен*
Тело запросаПолный объектТолько изменённые поля
ИспользованиеЗамена всего ресурсаЧастичное обновление
ПримерPUT /resource → весь объектPATCH /resource → только изменения

*Про идемпотентность позже.

Реальные примеры

Пример 1: Обновление статуса заказа

# ✅ PATCH
PATCH /orders/456
{"status": "shipped", "tracking_number": "12345ABC"}

# Остальные поля (items, customer, total_price) остаются без изменений

# ❌ PUT был бы неправильным:
PUT /orders/456
{"status": "shipped", "tracking_number": "12345ABC"}
# Потеряем items, customer, total_price!

Пример 2: Обновление настроек пользователя

# ✅ PATCH
PATCH /settings/user/123
{"theme": "dark", "notifications": false}

# Остальные настройки (language, timezone, privacy) не меняются

# PUT опасен:
PUT /settings/user/123
{"theme": "dark", "notifications": false}
# Потеряем language и timezone!

Пример 3: Обновление товара в каталоге

# ✅ PATCH для изменения цены
PATCH /products/789
{"price": 99.99}

# Сохранится description, images, category и т.д.

# PUT для полной замены:
PUT /products/789
{
    "name": "Product Name",
    "description": "Long description",
    "price": 99.99,
    "category": "Electronics",
    "images": [...],
    "stock": 100
}
# Используется когда хочешь полностью переписать товар

Проблема: идемпотентность

# PUT идемпотентен (повтор даёт тот же результат)
PUT /users/123 {"name": "Alice", "age": 30}
# Результат: {id: 123, name: "Alice", age: 30, ...}

PUT /users/123 {"name": "Alice", "age": 30}
# Результат: ТОТ ЖЕ {id: 123, name: "Alice", age: 30, ...}
# Идемпотентно!

# PATCH НЕ идемпотентен в общем случае
PATCH /counters/1 {"value": "+1"}  # Increment
// Результат: {value: 5}

PATCH /counters/1 {"value": "+1"}  // Повторили
// Результат: {value: 6}  // ДРУГОЙ результат!
// Не идемпотентно!

# Но PATCH с абсолютными значениями идемпотентен:
PATCH /counters/1 {"value": 5}  // SET, не INCREMENT
// Результат: {value: 5}

PATCH /counters/1 {"value": 5}  // Повторили
// Результат: {value: 5}  // ТОТ ЖЕ!
// Идемпотентно!

REST стандарты (RFC 7231, 5789)

PUT (RFC 7231):
- Замещает весь ресурс
- Идемпотентен
- Требует полное состояние

PATCH (RFC 5789):
- Применяет частичные изменения
- Может не быть идемпотентным
- Требует JSON Patch (RFC 6902) или JSON Merge Patch (RFC 7386)

JSON Merge Patch (стандартный способ)

Current:
{"name": "Alice", "age": 30, "email": "alice@example.com"}

PATCH запрос с JSON Merge Patch:
{"age": 31}

Результат:
{"name": "Alice", "age": 31, "email": "alice@example.com"}

# Merge происходит на уровне JSON объекта

Реализация в FastAPI:

from fastapi import FastAPI
from pydantic import BaseModel

class UserUpdate(BaseModel):
    name: str | None = None
    age: int | None = None
    email: str | None = None

@app.patch("/users/{user_id}")
def patch_user(user_id: int, update: UserUpdate):
    user = get_user(user_id)
    
    # JSON Merge Patch: объедини переданные поля
    update_data = update.model_dump(exclude_unset=True)
    for key, value in update_data.items():
        setattr(user, key, value)
    
    save_user(user)
    return user

Когда использовать что

Используй PUT когда:

  • Клиент отправляет полное состояние ресурса
  • Все поля обязательны
  • Хочешь идемпотентное обновление
  • Замещаешь весь ресурс
# Пример: загрузка файла конфигурации
PUT /config
{
    "database_url": "postgresql://...",
    "api_key": "secret",
    "debug_mode": true
    // ВСЕ параметры
}

Используй PATCH когда:

  • Клиент хочет изменить несколько полей
  • Не все поля обязательны
  • Хочешь сохранить остальные поля
  • Защищен от потери данных
# Пример: обновление профиля пользователя
PATCH /profile
{
    "bio": "New bio",
    "avatar_url": "..."
    // Только изменённые поля, остальное остаётся
}

Вывод

PATCH нужен для защиты от потери данных при частичных обновлениях.

Основные преимущества:

  • ✅ Безопаснее (не теряем поля)
  • ✅ Эффективнее (отправляем только изменённое)
  • ✅ Удобнее для клиента (не нужно знать все поля)
  • ✅ Семантически правильнее (частичное изменение)

Основное различие:

  • PUT = полная замена
  • PATCH = частичное обновление

В 90% случаев современных API используется PATCH для любых обновлений. PUT оставлен для специальных случаев полной замены ресурса.

Зачем нужен PATCH запрос? | PrepBro