事务
App Engine 数据存储区支持事务。事务是一项操作或一系列操作,要么全部成功,要么全部失败。应用程序可以在单个事务中执行多个操作和计算。
使用事务
事务是一项或一系列数据存储区操作,这些操作要么全部成功,要么全部失败。如果事务成功完成,则会对数据存储区产生所有预期的作用。如果事务失败,则不会起任何作用。
每个数据存储区写入操作都是原子操作。要么试图创建、更新或删除实体,要么不执行。如果有太多用户试图同时修改一个实体,那么这种高占用率将可能引发操作失败。当应用程序达到配额限制时,也可能会引发操作失败。数据存储区内部错误也是引发操作失败的原因。在上述所有情况下,操作将不起作用,且数据存储区 API 将引发异常。
以下是一个使用 JDO 事务 API 递增名为 counter
的字段的示例,该字段位于名为 ClubMembers
的对象(未显示类)中:
import javax.jdo.Transaction; import ClubMembers; // not shown // ... // PersistenceManager pm = ...; Transaction tx = pm.currentTransaction(); try { tx.begin(); ClubMembers members = pm.getObjectById(ClubMembers.class, "k12345"); members.incrementCounterBy(1); pm.makePersistent(members); tx.commit(); } finally { if (tx.isActive()) { tx.rollback(); } }
实体组
每个实体都属于一个实体组,它是可以在一个事务中控制的一组实体(一个或多个)。实体组关系会让 App Engine 在分布式网络的相同部分中存储若干实体。事务会针对实体组设置数据存储区操作,且所有操作都会以组的形式应用。如果事务失败,则全都不应用。
当应用程序创建一个实体时,它将另一个实体分配为新实体的父实体。向新实体分配父实体会将新实体放置在与父实体相同的实体组。
没有父实体的实体是根实体。作为另一个实体的父实体的实体也可以有父实体。从某实体到根的父实体链是该实体的路径,路径的成员是该实体的祖先。实体的父实体是在创建该实体时定义的,且以后不能再更改。
每个采用指定根实体作为祖先的实体都在相同的实体组中。一个组中的所有实体都存储在相同的数据存储区节点中。一个事务可以修改一个组中的多个实体,或向组添加新实体(方法是以组中的现有实体作为新实体的父实体)。
使用实体组创建实体
JDO 接口的 App Engine 实现使用实体组表示有主的一对一或一对多关系。这使得对某一个对象的更改以及对其子对象的更改可在同一事务中发生。请参阅关系。
对于其他情况,可通过将对象的主键字段设置为完整键(包括父实体的键),显式地使用父实体组来创建实体。对象的主键字段必须是 Key 实例或编码的键字符串(而不是简单的 Long 或 String 类型的键名称)。
当使用应用程序分配的字符串 ID 时,可通过将实体的键字段设置为完整键值(包括父实体的键)使用父实体组来创建对象。要使用父实体组来创建键值,可使用 KeyFactory.Builder 类,如下所示:
import javax.jdo.annotations.IdentityType; import javax.jdo.annotations.IdGeneratorStrategy; import javax.jdo.annotations.PersistenceCapable; import javax.jdo.annotations.Persistent; import javax.jdo.annotations.PrimaryKey; import com.google.appengine.api.datastore.Key; import com.google.appengine.api.datastore.KeyFactory; @PersistenceCapable(identityType = IdentityType.APPLICATION) public class AccountInfo { @PrimaryKey @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY) private Key key; public void setKey(Key key) { this.key = key; } } // ... KeyFactory.Builder keyBuilder = new KeyFactory.Builder(Customer.class.getSimpleName(), "custid985135"); keyBuilder.addChild(AccountInfo.class.getSimpleName(), "acctidX142516"); Key key = keyBuilder.getKey(); AccountInfo acct = new AccountInfo(); acct.setKey(key); pm.makePersistent(acct);
可使用一个字段与对象的键分开访问父实体组键,如下所示:
// ... @Persistent @Extension(vendorName="datanucleus", key="gae.parent-pk", value="true") private Key customerKey;
要使用系统生成的数字 ID 和父实体组创建对象,则必须使用父实体组键字段(如 customerKey
,见上文)。将父实体的键分配到父键字段,然后保留将对象的键字段设置为 null。保存对象时,数据存储区将使用完整键(包括父实体组)填充键字段。
如果一个类具有父实体组字段,则您可在对父字段的查询中使用相等过滤条件。(不支持父键上的不等过滤条件。)
事务的功能
数据存储区对单个事务中可完成的功能施加了许多限制。
事务中的所有数据存储区操作必须在同一实体组中的实体上进行。这包括通过键、更新实体和删除实体来检索实体。请注意,每个根实体都属于单独的实体组,因此,单个事务不能创建多个根实体或在多个根实体上进行操作。
应用程序在事务过程中不能执行查询。但是,应用程序可以在事务过程中使用键检索数据存储区实体,并保证抓取的实体与事务的其余实体一致。您可以在事务之前准备键,或者在事务内部根据键名或 ID 生成键。
应用程序不能在单个事务中多次创建或更新实体。
JDO 将在单个事务中执行调用 tx.begin()
与调用 tx.commit()
之间的所有操作。如果某个操作因所请求的实体组正在被其他进程使用而失败,则 JDO 将引发 JDODataStoreException 或 JDOException,由 java.util.ConcurrentModificationException 引起。
在使用开放式并发的系统中,通常应用程序要在放弃之前多次尝试事务。JDO 仅执行事务一次;应用程序必须根据需要重复事务。例如:
for (int i = 0; i < NUM_RETRIES; i++) { pm.currentTransaction().begin(); ClubMembers members = pm.getObjectById(ClubMembers.class, "k12345"); members.incrementCounterBy(1); try { pm.currentTransaction().commit(); break; } catch (JDOCanRetryException ex) { if (i == (NUM_RETRIES - 1)) { throw ex; } } }
试图在同一事务中更新多个实体组将引发 JDOFatalUserException。请注意,每个没有父实体组的对象都驻留在其自己的实体组中,因此无法在单个事务中创建多个无父实体。
试图在单个事务中多次更新同一实体(如通过重复调用 makePersistent()
)将引发 JDOFatalUserException。而应在事务中只修改持久对象,并允许调用 commit()
以应用更改。
事务的用途
该示例说明了事务的一个用途:使用与属性当前值相关的新属性值更新实体。
Key k = KeyFactory.createKey("Employee", "k12345"); Employee e = pm.getObjectById(Employee.class, k); e.counter += 1; pm.makePersistent(e);
这需要使用事务,因为在此代码抓取对象之后和保存修改的对象之前,其他用户可能会更新值。如果不使用事务,该用户的请求将在其他用户的更新前使用 counter
的值,并且保存时将覆盖此新值。如果使用事务,应用程序将得知其他用户的更新。 如果实体在事务期间进行了更新,则该事务将失败,同时引发异常。应用程序可重复事务以使用新数据。
事务的另一个常见用途,是使用命名的键更新实体,或当实体不存在时创建实体:
// PersistenceManager pm = ...; Transaction tx = pm.currentTransaction(); String id = "jj_industrial"; String companyName = "J.J. Industrial"; try { tx.begin(); Key k = KeyFactory.createKey("SalesAccount", id); SalesAccount account; try { account = pm.getObjectById(Employee.class, k); } catch (JDOObjectNotFoundException e) { account = new SalesAccount(); account.setId(id); } account.setCompanyName(companyName); pm.makePersistent(account); tx.commit(); } finally { if (tx.isActive()) { tx.rollback(); } }
同以前一样,如果其他用户尝试使用相同的字符串 ID 创建或更新实体,则需要使用事务来处理这种情况。在不使用事务的情况下,如果实体不存在,且两个用户都尝试创建该实体,第二个用户将覆盖第一个用户的实体,且不知道发生了此情况。 如果使用事务,第二个用户的尝试会发生原子失败,并且可由应用程序重试,以抓取新实体并进行更新。
提示:应当尽可能快地执行事务,以减少该事务所用实体被更改从而需要重试该事务的可能性。在该事务以外尽可能多地准备数据,然后执行该事务的数据存储区操作,该操作要求数据处于稳定状态。应用程序应该为事务内部所使用的对象准备键,然后抓取事务内部的实体。
禁用事务和传输现有的 JDO App
我们推荐使用的 JDO 配置将名为 datanucleus.appengine.autoCreateDatastoreTxns
的属性设置为 true
。这是 App Engine 特定的属性,它告知 JDO 实现将数据存储区事务与在应用程序代码中管理的 JDO 事务相关联。如果您要从头开始构建一个新应用程序,这可能是您应采用的做法。但是,如果您已经有了一个将要在 App Engine 上运行的基于 JDO 的应用程序,则可能要使用其他持久配置,将该属性值设置为 false
:
<?xml version="1.0" encoding="utf-8"?> <jdoconfig xmlns="http://java.sun.com/xml/ns/jdo/jdoconfig" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://java.sun.com/xml/ns/jdo/jdoconfig"> <persistence-manager-factory name="transactions-optional"> <property name="javax.jdo.PersistenceManagerFactoryClass" value="org.datanucleus.store.appengine.jdo.DatastoreJDOPersistenceManagerFactory"/> <property name="javax.jdo.option.ConnectionURL" value="appengine"/> <property name="javax.jdo.option.NontransactionalRead" value="true"/> <property name="javax.jdo.option.NontransactionalWrite" value="true"/> <property name="javax.jdo.option.RetainValues" value="true"/> <property name="datanucleus.appengine.autoCreateDatastoreTxns" value="false"/> </persistence-manager-factory> </jdoconfig>
为了了解它有用的原因,请记住,只能对事务中属于同一个实体组的对象进行操作。使用传统数据库生成的应用程序通常可以使用全局事务,这样您可以对事务内部所有的记录集进行更新。因为 App Engine 数据存储区不支持全局事务,所以使用全局事务的代码会引发异常。您只要禁用数据存储区事务即可,而不需要浏览整个代码库(可能很大)并删除所有事务管理代码。当然,这不能证实代码对多记录修改的原子性所作的假设,但是可使应用程序正常工作,这样您便可以根据需要专注于逐步重构事务代码,而不是一次性完成。