Используем Хранимые Процедуры в EF Core. Окончание
Начало
Продолжение
3. Хранимая процедура с валидацией
Допустим, вам нужно изменить количество доступных билетов, но вы хотите предотвратить состояния гонки и проверить операцию:
CREATE OR REPLACE PROCEDURE
tt.adjust_available_quantity(
ticket_type_id uuid,
delta numeric
)
LANGUAGE plpgsql AS $$
DECLARE
v_qty numeric;
v_avail numeric;
v_new_avail numeric;
BEGIN
SELECT quantity, available_quantity
INTO v_qty, v_avail
FROM tt.ticket_types
WHERE id = ticket_type_id
FOR UPDATE;
IF NOT FOUND THEN
RAISE EXCEPTION 'Тип билета % не найден', ticket_type_id;
END IF;
v_new_avail := v_avail + delta;
IF v_new_avail < 0 THEN
RAISE EXCEPTION 'Нельзя уменьшить < 0';
END IF;
IF v_new_avail > v_qty THEN
RAISE EXCEPTION 'Нельзя превышать общее количество';
END IF;
UPDATE tt.ticket_types
SET available = v_new_avail
WHERE id = ticket_type_id;
END;
$$;
Процедура делает несколько важных вещей:
- «Запирает» строку для обновления (FOR UPDATE), чтобы другая транзакция не могла её обновить, пока не завершится хранимая процедура;
- Проверяет бизнес-правила перед внесением изменений;
- Выводит понятные сообщения об ошибках при возникновении проблем;
- Сохраняет всё атомарно в одном запросе к БД.
Всё это можно было бы реализовать на C# с ручным управлением транзакциями и явной блокировкой, но это сложнее и подвержено ошибкам. Позвольте БД делать то, в чём она хороша. Вот как это можно вызвать из EF Core:
try
{
await dbContext.Database.ExecuteSqlAsync(
$"""
CALL tt.adjust_available_quantity({ticketTypeId},{quantity})
""");
}
catch (Exception e)
{
// обработка ошибки
}
Процедура не возвращает значение, но, если она выбрасывает исключение (с помощью RAISE EXCEPTION), PostgreSQL передаст его в ваш код C#. Вы можете перехватить его и вернуть корректный ответ об ошибке.
Представления (view)
Представления БД подобны функциям без параметров. Это сохранённые запросы, к которым можно обращаться по имени. Можно выполнять запросы к ним, используя SqlQuery<T>, как к функциям:
var results = await dbContext.Database
.SqlQuery<ActiveCustomerDto>(
$"SELECT * FROM tt.active_customers")
.ToListAsync();
Или можно сопоставлять их с типами сущностей в DbContext для полной поддержки LINQ. Представления отлично подходят для часто используемых запросов, не требующих параметров. Их также можно добавить в DbContext. Функции же обеспечивают гибкость параметризации.
Итого
EF Core не заставляет вас выбирать между LINQ и чистым SQL. Вы можете использовать и то, и другое. Используйте функции, когда нужно вернуть данные, процедуры, когда нужно изменить данные со сложной логикой, и чистые SQL-запросы, когда LINQ не может эффективно удовлетворить ваши требования. Сочетание удобства EF Core и мощности БД даёт гибкость в выборе подходящего инструмента для каждого сценария.
Источник: https://www.milanjovanovic.tech/blog/using-stored-procedures-and-functions-with-ef-core-and-postgresql