Транзакции

Хранилище App Engine поддерживает работу с транзакциями. Транзакция - это операция или набор операций, которые либо все будут произведены успешно, либо ни одна из них не будет применена. Приложение может определять несколько операций в одной транзакции с помощью указания их в функции языка Python и последующем вызове метода db.run_in_transaction().

Использование транзакций

Транзакция - это операция или набор операций, которые либо все будут произведены успешно, либо ни одна из них не будет применена. Если транзакция завершилась успешно, то все ее операции будут применены к данным хранилища. Если в процессе транзакции произошла хотя бы одна ошибка, то ни одна из ее операций не будет применена.

Каждая операция по сохранению данных является атомарной. Методы put() и delete() могут быть успешно выполнены, а могут и вернуть ошибку. Обычно это случается, когда при высокой пользовательской активности происходит попытки одновременного изменения одного и того же набора данных. Другой случай неудачного завершения операций - достижение приложением отведенной ему квоты на хранение данных. Также это может быть внутренняя ошибка в системе хранилища. Во всех этих случаях, операции не будут производить изменения данных и интерфейс Datastore API выдаст исключение.

Приложение может задать выполнение набора выражений и операций с хранилищем в одной транзакции, таким образом, если при выполнении хотя бы одной из них выдастся исключение, ни одно изменение не будет применено к данным. Приложение задает требуемые операции с помощью функции языка Python, после чего вызывает метод db.run_in_transaction() с именем этой функции в качестве параметра:

from google.appengine.ext import db

class Accumulator(db.Model):
  counter = db.IntegerProperty()

def increment_counter(key, amount):
  obj = db.get(key)
  obj.counter += amount
  obj.put()

q = db.GqlQuery("SELECT * FROM Accumulator")
acc = q.get()

db.run_in_transaction(increment_counter, acc.key(), 5)

Метод db.run_in_transaction() принимает в качестве своих параметров объект функции и позиционные и именованные аргументы для передачи в функцию. Если функция вернет какое-то значение, оно также будет возвращено методом db.run_in_transaction().

Если функция успешно завершается, то транзакция будет зафиксирована и все изменения применены к данным хранилища. Если во время выполнения функции произойдет выдача исключения, транзакция произведет "откат" и ни одно изменение не будет применено к данным.

Если функция в процессе работы выдаст исключение Rollback, но метод db.run_in_transaction() вернет значение None. Для любого другого исключения, произошедшего в функции, метод db.run_in_transaction() повторно сгенерирует это исключение.

Что можно сделать с помощью транзакции

Хранилище накладывает некоторые ограничения на работу транзакций.

Все операции одной транзакции должны выполняться с объектами, принадлежащими одной группе. Это включает в себя использование методов db.get(), put() и delete(). Обратите внимание, что каждый корневой объект принадлежит отдельной группе объектов, таким образом, внутри одной транзакции не существует возможности проводить операции над несколькими такими объектами. Для получения дополнительной информации о группах объектов, смотрите раздел Ключи и группы объектов.

Транзакция не может выполнять запросы с помощью интерфейсов Query или GqlQuery. Однако, внутри транзакции возможно извлечение объектов с помощью их ключей, переданных методу db.get(). Массив ключей может быть передан параметром в функцию транзакции или сформирован внутри ее с помощью задания параметров ключей или идентификаторов объектов в функциях Key.from_path(), Model.get_by_key_name() или Model.get_by_id().

Приложение не может создать или изменять один объект более одного раза в течении всей транзакции.

Примечание: Как было описано выше, также существует проблема невозможности создания нового корневого объекта и зависимых от него объектов в одной транзакции. До тех пор, пока эта проблема не будет устранена, корневой объект должен быть создать в отдельной транзакции от его дочерних объектов.

В функции транзакции допускается использовать весь остальной код языка Python. Однако, функция не должна иметь в своем составе операции, не относящиеся к работе с данными хранилища. Функция может быть вызвана системой несколько раз в том случае, если хотя бы одна из операций была завершена с ошибкой по причине блокировки данных другим пользователем. Когда происходит такая ситуация, интерфейс Datastore API несколько раз подряд пытается выполнить транзакцию. Если все они будет неуспешными, то функция db.run_in_transaction() выдаст исключение TransactionFailedError.

По этой причине, функция транзакции не должна содержать частей кода, которые подразумевают ее успешное завершение, так как она может быть в любой момент откачена. К примеру, если в хранилище помещается новый объект и переменной присваивается его автоматический идентификатор для дальнейшего использования, при откате транзакции эта переменная будет содержать значение, указывающее на несуществующий объект. Код, вызывающий транзакцию, должен выполнять проверки и использовать идентификатор нового объекта только в случае успешной фиксации транзакции.

Применение транзакций

Этот пример демонстрирует процесс использования транзакций: увеличение существующего числового значения свойства объекта.

def increment_counter(key, amount):
  obj = db.get(key)
  obj.counter += amount
  obj.put()

Этот код требуется выполнять в рамках одной транзакции, так как между операциями db.get(key) и obj.put() может произойти изменение объекта другим пользователем. Без использования транзакции запрос пользователя будет использовать значение obj.counter до его изменения и операция obj.put() перезапишет последнее. Исполнение этого кода в рамках транзакции гарантирует, что объект не будет изменен между двумя обращениями к хранилищу. Если объект изменяется в результате другой транзакции, то она будет повторена несколько раз, до тех пор, пока все ее операции не будут проведены успешно.

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

class SalesAccount(db.Model):
  address = db.PostalAddressProperty()
  phone_number = db.PhoneNumberProperty()

def create_or_update(parent_obj, account_id, address, phone_number):
  obj = db.get(Key.from_path("SalesAccount", account_id, parent=parent_obj))
  if not obj:
    obj = SalesAccount(parent=parent_obj,
                       address=address,
                       phone_number=phone_number)
  else:
    obj.address = address
    obj.phone_number = phone_number

  obj.put()

Как и ранее транзакция необходима в этом коде для предотвращения случаев, когда другой пользователь одновременно попытается создать или изменить объект с тем же account_id. Без транзакции, если два пользователя одновременно попытаются создать один объект, то один из них получит ошибку. С использованием транзакции, второй пользователь повторит попытку фиксации транзакции, получит данные, что объект уже существует, и обновит его.

Эту часто используемую операцию "создать-или-обновить" удобнее использовать с помощью встроенного метода Model.get_or_insert(), который принимает в качестве своих параметров ключ объекта и опционально значение родителя, а также аргументы, которые требуется передать конструктору модели для его создания, если такой объект еще не существует. Попытка извлечения и создания объекта происходит в рамках одной транзакции, таким образом (если она была завершена успешно) метод всегда возвращает экземпляр модели, которая сопоставлена с актуальным объектом.

Подсказка: Транзакция должна быть выполнена настолько быстро, насколько это возможно для уменьшения вероятности того, что в этот же момент с объектом захотят оперировать другие транзакции, которые могут завершиться с ошибкой. Если это возможно, выполняйте подготовку данных и их вычисление вне транзакции, после чего проводите в ней только операции, зависящие от состояния данных хранилища. Если приложению требуется подготовить список ключей объектов для использования внутри транзакции, то получайте эти данные с помощью метода db.get() тоже внутри этой транзакции.