• Официальный сайт SDK
  • Сайт с примерами кода

Транзакции

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

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

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

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

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

CmZyb20gZ29vZ2xlLmFwcGVuZ2luZS5leHQgaW1wb3J0IGRiCmNsYXNzIEFjY3VtdWxhdG9yKGRiLk1vZGVsKToKY291bnRlciA9IGRiLkludGVnZXJQcm9wZXJ0eSgpCmRlZiBpbmNyZW1lbnRfY291bnRlcihrZXksIGFtb3VudCk6Cm9iaiA9IGRiLmdldChrZXkpCm9iai5jb3VudGVyICs9IGFtb3VudApvYmoucHV0KCkKcSA9IGRiLkdxbFF1ZXJ5KCJTRUxFQ1QgKiBGUk9NIEFjY3VtdWxhdG9yIikKYWNjID0gcS5nZXQoKQpkYi5ydW5faW5fdHJhbnNhY3Rpb24oaW5jcmVtZW50X2NvdW50ZXIsIGFjYy5rZXkoKSwgNSkK=

Метод 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.

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

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

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

CmRlZiBpbmNyZW1lbnRfY291bnRlcihrZXksIGFtb3VudCk6Cm9iaiA9IGRiLmdldChrZXkpCm9iai5jb3VudGVyICs9IGFtb3VudApvYmoucHV0KCkK=

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

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

CmNsYXNzIFNhbGVzQWNjb3VudChkYi5Nb2RlbCk6CmFkZHJlc3MgPSBkYi5Qb3N0YWxBZGRyZXNzUHJvcGVydHkoKQpwaG9uZV9udW1iZXIgPSBkYi5QaG9uZU51bWJlclByb3BlcnR5KCkKZGVmIGNyZWF0ZV9vcl91cGRhdGUocGFyZW50X29iaiwgYWNjb3VudF9pZCwgYWRkcmVzcywgcGhvbmVfbnVtYmVyKToKb2JqID0gZGIuZ2V0KEtleS5mcm9tX3BhdGgoIlNhbGVzQWNjb3VudCIsIGFjY291bnRfaWQsIHBhcmVudD1wYXJlbnRfb2JqKSkKaWYgbm90IG9iajoKb2JqID0gU2FsZXNBY2NvdW50KHBhcmVudD1wYXJlbnRfb2JqLAphZGRyZXNzPWFkZHJlc3MsCnBob25lX251bWJlcj1waG9uZV9udW1iZXIpCmVsc2U6Cm9iai5hZGRyZXNzID0gYWRkcmVzcwpvYmoucGhvbmVfbnVtYmVyID0gcGhvbmVfbnVtYmVyCm9iai5wdXQoKQo==

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

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

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