App Engine 数据存储区支持事务。事务是一项操作或一系列操作,要么全部成功,要么全部失败。应用程序可以在单个事务中执行多个操作和计算。
事务是一项或一系列数据存储区操作,这些操作要么全部成功,要么全部失败。如果事务成功完成,则会对数据存储区产生所有预期的作用。如果事务失败,则不会起任何作用。
每个数据存储区写入操作都是原子操作。要么试图创建、更新或删除实体,要么不执行。如果有太多用户试图同时修改一个实体,那么这种高占用率将可能引发操作失败。当应用程序达到配额限制时,也可能会引发操作失败。数据存储区内部错误也是引发操作失败的原因。在上述所有情况下,操作将不起作用,且数据存储区 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() 将重新引发该异常。
数据存储区对单个事务中可完成的功能施加了许多限制。
事务中的所有数据存储区操作必须在同一实体组中的实体上进行。这包括通过键、更新实体和删除实体来检索实体。请注意,每个根实体都属于单独的实体组,因此,单个事务不能创建多个根实体或在多个根实体上进行操作。 有关实体组的说明,请参阅键和实体组。
应用程序在事务过程中不能执行查询。但是,应用程序可以在事务过程中使用键检索数据存储区实体,并保证抓取的实体与事务的其余实体一致。您可以在事务之前准备键,或者在事务内部根据键名或 ID 生成键。
应用程序不能在单个事务中多次创建或更新实体。
事务函数中允许使用其他所有 Python 代码。除了数据存储区操作,该事务函数不应具有副作用。如果某数据存储区操作因其他用户同时在实体组中更新实体而失败,该事务函数则可进行多次调用。如果发生这种情况,数据存储区 API 将以固定次数重新尝试调用该事务。如果以上操作全部失败,db.run_in_transaction() 将引发 TransactionFailedError。可使用 db.run_in_transaction_custom_retries() 代替 db.run_in_transaction() 来调整重试事务的次数。
同样,事务函数不应具有由该事务成功与否所决定的副作用,除非调用此事务函数的代码明确如何撤消这些作用。例如,如果事务存储一个新数据存储区实体并保存该创建的实体的 ID 以供以后使用,然后该事务失败,则保存的 ID 无法引用预期的实体,因为该实体创建被回滚。在这种情况下,调用代码一定要谨慎,切勿使用已保存的 ID。
该示例说明了事务的一个用途:使用与属性当前值相关的新属性值更新实体。
def increment_counter(key, amount): obj = db.get(key) obj.counter += amount obj.put()
这需要使用事务,因为在此代码抓取对象之后和保存修改的对象之前,其他用户可能会更新值。如果不使用事务,该用户的请求将在其他用户的更新前使用 counter 的值,并且保存时将覆盖此新值。如果使用事务,应用程序将得知其他用户的更新。 如果实体在事务期间进行了更新,则该事务会重试,直到所有步骤都完成且没有中断。
事务的另一个常见用途,是使用命名的键更新实体,或当实体不存在时创建实体:
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(key_name=account_id,
parent=parent_obj,
address=address,
phone_number=phone_number)
else:
obj.address = address
obj.phone_number = phone_number
obj.put()
同以前一样,如果其他用户尝试使用相同的字符串 ID 创建或更新实体,则需要使用事务来处理这种情况。在不使用事务的情况下,如果实体不存在,且两个用户都尝试创建该实体,第二个用户将覆盖第一个用户的实体,且不知道发生了此情况。 如果使用事务,第二个用户的尝试会重新执行(请注意,现在该实体已存在)并更新该实体。
创建或更新非常有用,以致存在一个针对它的内置方法:Model.get_or_insert(),该方法的参数为键名、可选的父项以及传递给 Model 构造函数的参数(在该名称和路径的实体不存在的情况下)。获取尝试和创建会在同一事务中进行,因此(如果该事务成功完成)该方法会始终返回表示实际实体的 Model 实例。
提示:应当尽可能快地执行事务,以减少该事务所用实体被更改从而需要重试该事务的可能性。在该事务以外尽可能多地准备数据,然后执行该事务的数据存储区操作,该操作要求数据处于稳定状态。应用程序应该为事务内部所使用的对象准备键,然后抓取事务内部的实体。