2026, Jan 12 12:05
Идемпотентный POST во Flask с PostgreSQL: ON CONFLICT RETURNING для 201/200
Идемпотентный POST во Flask с PostgreSQL: UNIQUE и ON CONFLICT DO NOTHING RETURNING, чтобы вернуть 201 при создании и 200 при повторах без дубликатов и гонок.
Когда мобильный клиент повторяет POST из‑за нестабильной сети, ваш аккуратный однострочный insert может тихо размножить строки. Для эндпоинта загрузки, который пишет в PostgreSQL, это означает дубликаты, зашумленную аналитику и последующую ручную чистку. Задача проста: сделать маршрут идемпотентным, вернуть 201 Created при первом запросе, 200 OK при корректных повторах — и при этом оставить решение компактным и идиоматичным.
Воспроизводим проблему
Следующий маршрут принимает JSON и вставляет запись. Он работает, но повторные POST с одним и тем же телом создают дубликаты.
from flask import Flask, request, jsonify
svc = Flask(__name__)
@svc.post("/upload")
def push_photo():
data = request.get_json()
dbh.execute(
"INSERT INTO photos (user_id, filename, uploaded_at) VALUES (%s, %s, NOW())",
(data["user_id"], data["filename"]),
)
return jsonify({"status": "ok"}), 201
Что на самом деле происходит
Иногда клиенты повторяют POST /upload, когда сеть дает сбои. Без гарантии уникальности в базе каждый повтор — это новая вставка, и дубликаты накапливаются. Одно лишь ограничение уникальности (UNIQUE) предотвращает дубликат, но выдает клиенту «сырую» ошибку SQL. Обертывание вставки в ON CONFLICT DO NOTHING скрывает ошибку, однако обработчик больше не различает «создано» и «уже существовало», поэтому невозможно вернуть 201 в первый раз и 200 при повторе.
Минимальное и идиоматичное решение
Самый простой и надежный подход — обеспечить уникальность в таблице и использовать upsert, который сообщает, произошло ли добавление. В PostgreSQL это удобно делается через INSERT … ON CONFLICT … DO NOTHING RETURNING. Уникальный ключ не допускает дубликатов; предложение RETURNING говорит, была ли создана новая строка; а код статуса напрямую следует из этого сигнала.
Сначала убедитесь, что интересующая вас пара уникальна на уровне базы данных, чтобы дубликаты физически не могли появиться:
ALTER TABLE photos
ADD CONSTRAINT photos_user_filename_key UNIQUE (user_id, filename);
Затем используйте одиночный INSERT с ON CONFLICT DO NOTHING RETURNING, чтобы определить, удалось ли вставить запись. Если оператор вернул строку — это новая вставка, возвращайте 201. Если не вернул ничего — строка уже существовала, возвращайте 200.
Итоговый маршрут
from flask import Flask, request, jsonify
svc = Flask(__name__)
@svc.post("/upload")
def push_photo():
body = request.get_json()
sql = (
"INSERT INTO photos (user_id, filename, uploaded_at) "
"VALUES (%s, %s, NOW()) "
"ON CONFLICT (user_id, filename) DO NOTHING "
"RETURNING 1"
)
result = dbh.execute(sql, (body["user_id"], body["filename"]))
created = result.fetchone() is not None
status_code = 201 if created else 200
return jsonify({"status": "ok"}), status_code
Почему это важно
Этот паттерн держит идемпотентность там, где ей и место: на границе между приложением и базой данных. Ограничение уникальности гарантирует корректность при повторах и гонках. Upsert с RETURNING дает точный сигнал «создано» против «уже было» без дополнительной инфраструктуры. Отображение этого сигнала на HTTP‑семантику делает API предсказуемым для клиентов. Можно было бы перехватывать ошибку базы и возвращать 200 при дубликатах, но опора на чистый путь через RETURNING прямее и избавляет от разборов ошибок.
Итоги
Для идемпотентных POST‑загрузок во Flask с PostgreSQL опирайтесь на базу данных. Задайте UNIQUE для естественного идентификатора, используйте INSERT … ON CONFLICT … DO NOTHING RETURNING, чтобы узнать, создана ли строка, и переводите это в 201 в первый раз и 200 при повторах. В итоге получаем небольшой, надежный эндпоинт, который устойчив к сетевым повторам без добавления очередей, кэшей или внешних сервисов.