Transaction Script (М. Фаулер) организует бизнес-логику как одну процедуру на каждый конкретный пользовательский сценарий. Внутри этой процедуры выполняются все проверки, изменения данных и вызовы внешних сервисов. Логика работает в рамках единой атомарной транзакции: если на любом шаге возникнет исключение, всё откатывается. Такой подход прост и хорошо подходит там, где предметная область ещё не требует сложной объектной модели или когда важна прозрачность операций.
Active Record (AR) — это способ хранить данные, при котором объект модели одновременно инкапсулирует строку таблицы и методы чтения/записи, а заодно может содержать часть доменной логики. Фреймворки вроде Django поставляются с AR-ORM «из коробки», поэтому связка «TS + AR» получается практически бесплатно.
Контексты из предыдущего примера: bookings
, payments
, flights
.
# bookings/services.py
from django.db import transaction
from django.utils import timezone
from bookings import exceptions
from bookings.models import Booking, Seat, Flight
from payments.services import charge_card # отдельный сервис оплаты
@transaction.atomic()
def book_flight(user, flight_id, seat_class, card_token) -> Booking:
"""
Оформить бронь и сразу списать деньги.
Все шаги проходят в одной транзакции. Если что-то пойдёт не так,
изменения не попадут в БД.
"""
# 1. Блокируем нужные строки, чтобы избежать гонки мест
flight = Flight.objects.select_for_update().get(id=flight_id)
seat = (
Seat.objects
.select_for_update() # блокировка строк
.filter(flight=flight,
seat_class=seat_class,
status=Seat.Status.AVAILABLE)
.first()
)
if seat is None:
raise exceptions.NoAvailableSeat
# 2. Помечаем место занятым
seat.status = Seat.Status.RESERVED
seat.save(update_fields=["status"])
# 3. Создаём запись бронирования
booking = Booking.objects.create(
user=user,
flight=flight,
seat=seat,
price=flight.get_price(seat_class),
booked_at=timezone.now(),
status=Booking.Status.PENDING,
)
# 4. Списываем оплату внешним сервисом
payment = charge_card(
user_id=user.id,
amount=booking.price,
currency="RSD",
card_token=card_token,
reference=str(booking.id),
)
# 5. Обновляем бронь и подтверждаем
booking.payment_id = payment.id
booking.status = Booking.Status.CONFIRMED
booking.save(update_fields=["payment_id", "status"])
return booking
Оркестрирует сценарий: проверяет наличие места, создаёт бронь, вызывает внешний платёжный сервис, подтверждает результат.
Ключевые моменты
@transaction.atomic
или контекст-менеджер with transaction.atomic():
собирает все операции в единую транзакцию. Если выбросить исключение, Django сделает rollback.select_for_update()
защищает от состояния «двойная продажа» места.book_flight
, не зная деталей. Такой сервис-слой в Django активно обсуждают и рекомендуют как способ вынести бизнес-логику из контроллеров.Каждая модель (Flight
, Seat
, Booking
) сама читает и пишет себя:
objects.get()
и objects.filter()
формируют SELECT
.
select_for_update()
превращается в SQL FOR UPDATE
, блокируя строки.
save()
и objects.create()
выполняют INSERT
или UPDATE
.
Таким образом скрипт не знает SQL-деталей — он оперирует доменными сущностями.