My favorites | 中文(简体) | Sign in

事务

App Engine 数据库支持事务。事务是一项操作或一系列操作,要么全部成功,要么全部失败。通过使用 Python 函数对象以及 db.run_in_transaction() 函数,应用程序可在单个事务中执行多项操作。

使用事务

事务是一项数据库操作或一系列数据库操作,要么全部成功,要么全部失败。如果事务成功完成,则会对数据库产生所有预期的作用。如果事务失败,则不会起任何作用。

每项数据库写入操作都是不可再分割的。要么执行 put()delete(),要么不执行。如果有太多用户试图同时修改一个实体,那么这种高占用率将可能引发操作失败。当应用程序达到配额限制时,也可能会引发操作失败。数据库内部错误也是引发操作失败的原因。在上述所有情况下,操作将不起作用,且数据库 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()。请注意,每个根实体都属于单独的实体组,因此,单个事务不能创建多个根实体或在多个根实体上进行操作。有关实体组的说明,请参阅键和实体组

事务不能使用 QueryGqlQuery 执行查询。但是,事务可使用键和 db.get() 检索数据库实体。Key 可传递给事务函数,或者在该函数中构建,包括指定键名或 ID 以及 Key.from_path()Model.get_by_key_name()Model.get_by_id()

应用程序不能在单个事务中多次创建或更新实体。

注意:截止到本文档编写之时,仍存在一个问题使用户无法在单个事务中创建新的根实体及其子孙实体。在此问题得到解决之前,根实体和子孙实体必须在不同的事务中创建。

事务函数中允许使用其他所有 Python 代码。除了数据库操作,该事务函数不应具有副作用。如果某数据库操作因其他用户同时在实体组中更新实体而失败,该事务函数则可进行多次调用。如果发生这种情况,数据库 API 将以固定次数重新尝试调用该事务。如果以上操作全部失败,db.run_in_transaction() 将抛出 TransactionFailedError

同样,事务函数不应具有由该事务成功与否所决定的副作用,除非调用此事务函数的代码明确如何撤消这些作用。例如,如果事务存储一个新数据库实体并保存该创建的实体的 ID 以供以后使用,然后该事务失败,则保存的 ID 无法引用预期的实体,因为该实体创建被回滚。在这种情况下,调用代码一定要谨慎,切勿使用已保存的 ID。

事务的用途

以上示例说明了事务的一个用途:使用与属性当前值相关的新属性值更新实体。

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(),该方法的参数为键名、可选的父项以及传递给 Model 构造函数的参数(在该名称和路径的实体不存在的情况下)。获取尝试和创建会在同一事务中进行,因此(如果该事务成功完成)该方法会始终返回表示实际实体的 Model 实例。

提示:应当尽可能快地执行事务,以减少该事务所用实体被更改从而需要重试该事务的可能性。在该事务以外尽可能多地准备数据,然后执行该事务的数据库操作,该操作要求数据处于稳定状态。应用程序应当为在事务中所使用的对象准备 Key,然后使用 db.get() 以抓取该事务中的实体。