Перейти к содержанию

Транзакции

Оберните блок записей, чтобы они закоммитились вместе или откатились вместе.

from endocore.orm import atomic

with atomic():
    account.balance -= 100
    account.save()
    ledger = Ledger.objects.create(amount=-100)
    # коммит при чистом выходе; откат, если исключение выходит наружу

Если блок бросает исключение, всё внутри откатывается, а исключение распространяется дальше.

Вложенные блоки используют savepoints

Внутренний atomic() становится SAVEPOINT, поэтому можно поймать внутренний сбой и сохранить более раннюю работу внешней транзакции:

with atomic():
    do_a()                      # часть внешней транзакции
    try:
        with atomic():          # SAVEPOINT
            do_b()              # падает
    except SomeError:
        pass                    # откат до savepoint; do_a() уцелела
    do_c()
# внешний коммит: do_a() и do_c() сохранены, do_b() откачена

По алиасам

from endocore.orm import atomic
with atomic("default"):         # конкретный алиас соединения
    ...

Async-код: aatomic()

В async-обработчике используйте async-близнеца — он берёт соединение из пула в worker-потоке, поэтому ожидание исчерпанного пула никогда не блокирует event loop:

from endocore.orm import aatomic

async def handler(request):
    async with aatomic():
        acc = await Account.objects.aget(pk=1)
        acc.balance -= 100
        await acc.asave()
        await Ledger.objects.acreate(amount=-100)
    # коммит при чистом выходе; откат, если исключение выходит наружу

a*-вызовы внутри блока присоединяются к транзакции: владение транзакцией отслеживается токеном в contextvars, а asyncio.to_thread переносит его в тредпул. Вложенные aatomic() становятся SAVEPOINT'ами, как и у atomic(). (a*-вызовы одной транзакции выполняйте последовательно — без gather.)

Вызов обычного with atomic(): в потоке event loop'а даёт RuntimeWarning: если пул исчерпан, ожидание свободного соединения заблокировало бы весь loop, а не только текущий запрос.

Как коммитятся записи вне atomic()

Вне блока atomic() каждая запись (save, create, update/delete на queryset'е, bulk_*) коммитится немедленно. Внутри блока коммит откладывается до выхода из самого внешнего atomic().

Замечания о конкурентности

  • Транзакция закрепляет за собой одно соединение из пула на весь блок, поэтому конкурентные запросы не могут вклиниться внутрь неё своими стейтментами.
  • У каждого алиаса — небольшой пул соединений: configure(..., pool_size=N) (по умолчанию: SQLite 1, PostgreSQL 5). С пулом 1 у SQLite транзакции сериализуются, а автокоммит-записи ждут открытую транзакцию; с PostgreSQL до pool_size транзакций идут по-настоящему параллельно.
  • Ожидание свободного соединения из пула прекращается через pool_timeout секунд (по умолчанию 30) и бросает PoolTimeoutError — по-настоящему исчерпанный или зависший пул падает быстро и громко, а не подвешивает всех вызывающих навсегда.
  • SQLite по природе однописательный; PostgreSQL хорошо справляется с конкурентными записями.