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

Можно ли изменять несколько разнотипных объектов методом PATCH?

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

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

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

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

Можно ли изменять несколько разнотипных объектов методом PATCH?

Технический ответ: можно, но это нарушает REST соглашения и приводит к сложности в архитектуре.

Проблемы с PATCH для множественных разнотипных объектов

1. Нарушение REST принципов

REST рекомендует, чтобы каждый HTTP-метод воздействовал на один ресурс:

PATCH /api/users/123         ✅ Изменить одного пользователя
PATCH /api/users/123/profile ✅ Изменить профиль пользователя
PATCH /api/users/123 + /posts/456  ❌ Это нарушает REST

2. Семантика PATCH

PATCH означает частичное изменение одного ресурса, определённого в URL:

PATCH /api/resource/{id}
Content-Type: application/merge-patch+json

{
  "field1": "value1",
  "field2": "value2"
}

Здесь ясно: мы изменяем ресурс с ID, не произвольный набор объектов.

Когда это может быть нужно?

Сценарий 1: Обновление связанных данных

# Нужно обновить пользователя И его профиль одновременно
PATCH /api/users/123

{
  "name": "John",
  "email": "john@example.com",
  "profile": {      # Вложенный объект
    "bio": "Developer",
    "avatar_url": "https://..."
  }
}

Решение: Это приемлемо, если вложенный объект — часть одного ресурса (one-to-one relationship).

Сценарий 2: Обновление несвязанных объектов

# Нужно обновить пользователя И отдельный пост одновременно
PATCH /api/bulk-update  # ❌ Плохо

{
  "user": {"id": 123, "name": "John"},
  "post": {"id": 456, "title": "New Title"}
}

Это проблематично! Давайте разберёмся почему.

Проблемы bulk update PATCH

Проблема 1: Неясная обработка ошибок

# Что если первый объект обновился успешно, а второй — нет?
PATCH /api/bulk-update

{
  "user": {"id": 123, "name": "John"},    # ✅ Успешно
  "post": {"id": 999, "title": "...."}     # ❌ Ошибка (ID не существует)
}

# Ответ: 207 Multi-Status? 400 Bad Request? 500 Internal Error?

Проблема 2: Отсутствие ACID гарантий

Если требуется транзакция (либо оба обновились, либо ничего):

@app.patch('/api/bulk-update')
def bulk_update(request):
    user_data = request.json['user']
    post_data = request.json['post']
    
    try:
        user = update_user(user_data)      # 1. Обновили пользователя
        post = update_post(post_data)      # 2. Сервер упал здесь!
        # Пользователь обновлён, пост — нет. Данные в противоречии!
    except Exception as e:
        # Как откатить только то, что обновилось?
        return {"error": str(e)}

Проблема 3: Сложность версионирования

# Какой версии API этот PATCH?
# /api/v1/bulk-update или /api/v2/bulk-update?
PATCH /api/bulk-update

# Если API эволюционирует, это усложняется
PATCH /api/v2/bulk-update  # Отличается структура данных?

Проблема 4: Сложность кеширования и логирования

# Кэш зависит от ресурса:
Cache-Key: user-123
Cache-Key: post-456

# Но при PATCH /api/bulk-update оба кэша нужно инвалидировать
# Это сложная логика

Правильные подходы

Подход 1: Отдельные PATCH запросы (рекомендуемый)

# Клиент
import httpx

async with httpx.AsyncClient() as client:
    # Запрос 1
    await client.patch('/api/users/123', json={'name': 'John'})
    
    # Запрос 2
    await client.patch('/api/posts/456', json={'title': 'New Title'})

Преимущества:

  • Чистая REST архитектура
  • Ясная семантика
  • Простое кэширование
  • Легко добавлять версионирование

Недостатки:

  • Два HTTP запроса вместо одного

Подход 2: Транзакционный batch endpoint (если критична производительность)

# Специальный endpoint для batch операций
POST /api/batch
Content-Type: application/json

{
  "requests": [
    {
      "method": "PATCH",
      "path": "/api/users/123",
      "body": {"name": "John"}
    },
    {
      "method": "PATCH",
      "path": "/api/posts/456",
      "body": {"title": "New Title"}
    }
  ]
}

Ответ:
{
  "responses": [
    {"status": 200, "body": {"id": 123, "name": "John"}},
    {"status": 200, "body": {"id": 456, "title": "New Title"}}
  ]
}

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

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import List, Dict, Any

app = FastAPI()

class BatchRequest(BaseModel):
    method: str
    path: str
    body: Dict[str, Any]

class BatchPayload(BaseModel):
    requests: List[BatchRequest]

@app.post('/api/batch')
async def batch_operations(payload: BatchPayload):
    responses = []
    
    for request in payload.requests:
        try:
            if request.method == 'PATCH' and request.path.startswith('/api/users/'):
                user_id = request.path.split('/')[-1]
                result = await patch_user(int(user_id), request.body)
                responses.append({
                    'status': 200,
                    'body': result
                })
            elif request.method == 'PATCH' and request.path.startswith('/api/posts/'):
                post_id = request.path.split('/')[-1]
                result = await patch_post(int(post_id), request.body)
                responses.append({
                    'status': 200,
                    'body': result
                })
        except Exception as e:
            responses.append({
                'status': 400,
                'error': str(e)
            })
    
    return {'responses': responses}

Подход 3: GraphQL (если много связанных объектов)

Для сложных операций с множественными объектами используй GraphQL:

mutation UpdateUserAndPost($userId: ID!, $postId: ID!, $userData: UserInput!, $postData: PostInput!) {
  updateUser(id: $userId, data: $userData) {
    id
    name
  }
  updatePost(id: $postId, data: $postData) {
    id
    title
  }
}

Преимущества GraphQL:

  • Одна операция в одном запросе
  • Точное определение, какие поля нужны
  • Встроенная обработка ошибок
  • Типизированный API

Подход 4: Вложенные ресурсы (для связанных объектов)

Если пост привязан к пользователю:

# Вместо обновления пользователя И поста отдельно
PATCH /api/users/123/posts/456

{
  "title": "New Title"
}

Здесь ясно: мы обновляем пост (456), привязанный к пользователю (123).

Практический пример: правильное решение

from fastapi import FastAPI, HTTPException
from sqlalchemy.orm import Session
from datetime import datetime

app = FastAPI()

# ❌ Плохо
@app.patch('/api/bulk-update')
def bulk_update(payload: dict, db: Session):
    # Несеман тичный PATCH для множественных объектов
    user = db.query(User).filter(User.id == payload['user_id']).first()
    user.name = payload['user_name']
    db.add(user)
    # Если тут ошибка, user уже changed
    db.commit()
    return {"updated": True}

# ✅ Хорошо: отдельные операции
@app.patch('/api/users/{user_id}')
def update_user(user_id: int, data: UserUpdateSchema, db: Session):
    user = db.query(User).filter(User.id == user_id).first()
    if not user:
        raise HTTPException(404)
    user.name = data.name
    db.commit()
    return user

@app.patch('/api/posts/{post_id}')
def update_post(post_id: int, data: PostUpdateSchema, db: Session):
    post = db.query(Post).filter(Post.id == post_id).first()
    if not post:
        raise HTTPException(404)
    post.title = data.title
    db.commit()
    return post

# ✅ Отличное решение: batch с транзакцией
@app.post('/api/batch')
async def batch_update(payload: BatchPayload, db: Session):
    results = []
    
    try:
        for req in payload.requests:
            if 'user' in req.path:
                user_id = int(req.path.split('/')[-1])
                user = db.query(User).filter(User.id == user_id).first()
                user.name = req.body['name']
                results.append({'id': user_id, 'status': 'ok'})
            elif 'post' in req.path:
                post_id = int(req.path.split('/')[-1])
                post = db.query(Post).filter(Post.id == post_id).first()
                post.title = req.body['title']
                results.append({'id': post_id, 'status': 'ok'})
        
        # Все успешно — коммитим ВСЕ изменения
        db.commit()
        return {'status': 'success', 'results': results}
        
    except Exception as e:
        # Ошибка — откатываем ВСЕ изменения
        db.rollback()
        raise HTTPException(500, detail=str(e))

Вывод

PATCH для множественных разнотипных объектов технически возможен, но нарушает REST архитектуру и приводит к проблемам:

  1. Используй отдельные PATCH запросы — простое и чистое решение
  2. Для производительности используй специальный /api/batch endpoint — явный, с транзакциями
  3. Для сложных операций рассмотри GraphQL — встроенная поддержка множественных объектов

Не пытайся втиснуть всё в один PATCH запрос — это приводит к сложности, ошибкам и проблемам с поддержкой.