Совет дня №38
Иногда бизнес-логику проще выразить на SQL – нравится вам это или нет. При этом окружающие теряют лицо: кричат “боже, хранимки!” и ведут себя недостойно. Бояться тут нечего, а истерика — от незнания.
Я не говорю, что нужно писать хранимки. Речь о том, чтобы поручить базе ту работу, что делает ваш Питон или Джава. Часто SQL выигрывает: он короче, точнее, не требует многих обращений к базе.
Нюанс в том, что меняется парадигма мышления. Когда вы пишете на SQL, то должны думать реляционно. Если писать хранимки императивно как на Питоне, это будет ужасно.
Давайте вспомним пример, который я описывал в статье про рейтинг пользователей. Постановка такая: у пользователей есть история начисления баллов. Также есть глобальный рейтинг. Как только пользователю начисляются баллы, он видит их в истории. Одновременно меняется его позиция в рейтинге.
На какой-нибудь питонячей ORM это выглядит так:
user_id = 51315
points = 100
points_entry = UserPoints.create(user_id=user_id, points=points)
points_entry.save()
rating_entry = UserRating.get_or_create(user_id=user_id)
rating_entry.points += points
rating_entry.save()
Сперва мы добавили запись в историю баллов. Затем выбрали или создали запись в глобальном рейтинге. Прибавили баллы, записали. Все это нужно делать в транзакции, иначе есть риск потерять баллы (по аналогии с задачей про семейную пару и терминал).
Предположим, руководитель сказал: “ребят, переносим логику в БД”, но не
удосужился объяснить, что это значит. Окей, программист пишет процедуру на
plpgsql:
create procedure add_points(int user_id, int points)
begin
create_points_history(user_id, points);
rating_entry = get_user_rating_by_user_id(user_id);
if (rating_entry is NULL) then
insert_user_rating(user_id, points);
else
update_user_rating(user_id, points);
endif;
end;
Эта процедура – построчная калька с Питона. Далее пишутся функции
get_user_rating_by_user_id, get_user_rating_by_user_id и другие. Получается
два экрана SQL, который трудно понять и поддерживать. Запросов по-прежнему
много. Один раз увидев такое, разработчик получает душевную травму. При слове
“хранимки” у него начинается истерика.
Проблема в том, что автор процедуры использует SQL не по назначению. Он пишет на
нем императивно, а SQL создан для операций над отношениями. Прочитайте еще раз:
SQL создан для операций над отношениями. Единица операции — таблица. Если
ваш SQL полон функций get_user_by_id, get_profile_by_id, то это
ошибка. Копаешь граблями – получишь заведомо плохой результат. Нельзя
пересаживать программиста с императивного Питона на SQL. Пока мозги не
перестроились, хорошего результата не ждите.
Достойное решение на SQL выглядит примерно так:
with update_history as (
insert into user_history (user_id, points)
values ($1, $2)
)
insert into user_rating(user_id, points)
values ($1, $2)
on conflict user_id do update
set points += excluded.points
returning user_id, points
Здесь все просто: часть update_history добавляет запись в историю
баллов. Основная часть пытается вставить пользователя и баллы в рейтинг, а если
пользователь уже есть в нем, прибавляет баллы. Запрос возвращает пользователя и
новую сумму баллов.
Этот запрос выполняется транзакционно, обе части видят один снимок данных. Подчеркну: совершенно незачем выносить его в хранимку. Достаточно хранить запрос в проекте и вызывать его. Хранимки нет, но работа делегируется базе.
Итого: не пишите хранимки только затем, чтобы они были. Думайте о том, как делегировать работу базе, а не писать бороду SQL-функций. Если вы работаете с базой, мыслите реляционно. Экраны императивного SQL говорят о том, что вы делаете что-то не так.
Нашли ошибку? Выделите мышкой и нажмите Ctrl/⌘+Enter