Можно ли изменять несколько разнотипных объектов методом PATCH?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Можно ли изменять несколько разнотипных объектов методом 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 архитектуру и приводит к проблемам:
- Используй отдельные PATCH запросы — простое и чистое решение
- Для производительности используй специальный /api/batch endpoint — явный, с транзакциями
- Для сложных операций рассмотри GraphQL — встроенная поддержка множественных объектов
Не пытайся втиснуть всё в один PATCH запрос — это приводит к сложности, ошибкам и проблемам с поддержкой.