Написание удобной и культурной программной обёртки для работы с транзакциями является любимым занятием многих. Транзакции надо уметь корректно начинать, заканчивать, откатывать, причем они ещё могут быть и вложенными. Python — язык многогранный (или, как говорят слабые духом, «мультипарадигмальный»), поэтому способов сделать такую обёртку на нём — несть числа.

В качестве фундамента я обычно использую питоновский DB API (а именно, модуль psycopg2). Сам DB API достаточно низкоуровневный и просто молит нас об улучшении. Что мы можем сделать полезного?

Для начала можно сделать обычный декоратор, который будет открывать и закрывать транзакции:

@transactional(db)
def do_database_stuff():
    return db.fetchall('select * from employee')

здесь db — небольшой вспомогательный класс, который следит за открыванием и закрыванием курсоров, а также содержит весь код connection management.

Выглядит такой декоратор не совсем здорово. С одной стороны, он все делает правильно и корректно закрывает транзакции при любом исходе, однако для декорирования каждую транзакцию придется оформлять как отдельную функцию. Это несколько замусоривает код.

Попробуем записать это как лямбда-выражение:

transactional(db)(lambda:db.fetchall('select * from employee'))()

Не самая понятная в мире запись. Кстати, в ней легко ошибиться и пропустить последнюю пару скобок.

Чтобы сделать всем приятное, Python 2.5 предлагает специальный новый синтаксис для исполняемых блоков, требующих контроль входа/выхода. Такая блоки очень часто встречаются при управлении системными ресурсами: файлами, сокетами, семафорами и т.п.

Выглядеть это будет так:

from __future__ import with_statement
 
with db.transaction() as tx:
    def employees = db.fetchall('select * from employee')

Это лишь один из вариантов. Именованный объект tx, указанный в with-выражении, соответствует нашей транзакции. В него можно поместить какие-то полезные методы.

Как происходит взаимодействие with-выражения с ассоциированным объектом (в нашем случае tx)? Есть два соглашения:

  1. Перед входом в with-блок всегда происходит вызов специального метода obj.__enter__()
  2. Любой выход из with-блока всегда происходит через вызов специального метода obj.__exit__(type, value, traceback), где все три параметра будут заполнены только при возникновении ошибки (я думаю, не стоит объяснять, что это тип исключения, объект исключения и стек вызовов соответственно).

В нашем случае все достаточно просто:

  • Помещаем в метод __enter__ код открытия транзакции и любую логику connection management, какую нам вздумается.
  • Помещаем в метод __exit__ код закрытия транзакции. Простейший вариант — в случае успешного выполнения всегда выполнять commit, если же with-блок выбросил исключение, всегда пытаться выполнить rollback. В реальной жизни, конечно, этого мало — приходится еще следить за тем, какое именно исключение выброшено.

Довольно удобно, что вызов return внутри with-блока точно также будет «фильтроваться» методом __exit__.

Какие недостатки у транзакций, оформленных в виде with-блока? Прежде всего, такой блок с точки зрения Python не является функцией. Например, мы не можем повторить выполнение блока в методе __exit__ (в случае использования декораторов можно легко добавить, например, логику retry).

  • Sharing

    Facebooktwittergoogle_plusredditpinterestlinkedinmailby feather