Google Code が利用できる言語: English - Español - 日本語 - 한국어 - Português - Pусский - 中文(简体) - 中文(繁體)
App Engine データストアは「トランザクション」をサポートします。トランザクションとは、完全に成功または失敗した一連の操作のことです。アプリケーションは、複数の操作と演算を単一のトランザクションで実行できます。
「トランザクション」とは、完全に成功または失敗した一連のデータストア操作のことです。トランザクションが成功すると、意図した効果がすべてデータストアに適用されます。トランザクションが失敗すると、意図した効果は適用されません。
データストアの書き込み操作はすべてアトミックです。エンティティの作成、更新、削除は「実行される」か「実行されない」かのどちらかです。操作が失敗する原因として、同時にエンティティを変更しようとするユーザーが多すぎて競合頻度が高くなっていることが考えられます。また、アプリケーションが割り当て制限に達したために操作が失敗する場合もあります。そのほか、データストアの内部エラーが原因の場合もあります。いずれの場合にも、操作の効果は適用されず、Datastore API は例外を発行します。
JDO トランザクション API を使用し、ClubMembers という名前のオブジェクト(クラスは非表示)の counter というフィールドの値を増やす例を次に示します。
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();
}
}
すべてのエンティティは、一度のトランザクションで操作できる、1 つ以上のエンティティのセットである、「エンティティ グループ」に属しています。App Engine はエンティティ グループの関係に基づいて、複数のエンティティを分散ネットワークの同じ場所に格納します。トランザクションがエンティティ グループのデータストア オペレーションを設定し、すべてのオペレーションはグループとして適用されます。トランザクションが失敗した場合は、まったく適用されません。
アプリケーションがエンティティを作成すると、別のエンティティを新しいエンティティの「親」として割り当てることができます。新しいエンティティに親を割り当てることは、新しいエンティティを親エンティティと同じグループに入れることになります。
親を持たないエンティティは、「ルート」 エンティティとなります。別のエンティティの親であるエンティティも、親を持つことができます。あるエンティティからルートまでの親エンティティの連鎖は、エンティティの「パス」となり、パスのメンバーはエンティティの「祖先」となります。エンティティの親はエンティティの作成時に定義され、その後変更することはできません。
指定されたルート エンティティを祖先とするエンティティはすべて、同じエンティティ グループとなります。同じグループ内のエンティティはすべて、同じデータストア ノードに格納されます。1 回のトランザクションで、1 つのグループ内にある複数のエンティティを修正したり、新しいエンティティの親をグループ内に存在させることでそのエンティティをグループに追加することができます。
JDO インターフェースでの App Engine の実装は、エンティティ グループを使用した 1 対 1 または 1 対多の関係を表します。これにより、オブジェクトへの変更と子オブジェクトへの変更を同じトランザクション内で実行できます。詳しくは、関係をご覧ください。
他の状況では、オブジェクトの主キー フィールドに親キーを含めた完全キーを設定することで、明示的なエンティティ グループの親を持つエンティティを作成できます。オブジェクトの主キー フィールドは、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 値のままとします。オブジェクトが保存されると、データストアはエンティティ グループの親を含む完全キーでキー フィールドを生成します。
クラスにエンティティ グループの親フィールドがある場合、親フィールドのクエリに等式フィルタを使用できます(親キーへの不等式フィルタはサポートしていません)。
データストアには 1 つのトランザクションで実行できることに制限があります。
1 つのトランザクションでのデータストア操作は、すべてが同じエンティティ グループ内のエンティティを対象とします。これには、キーによるエンティティの取得、エンティティの更新、エンティティの削除が含まれます。なお、各ルート エンティティは個別のエンティティ グループに属するので、1 つのトランザクションが複数のルート エンティティについて作成や操作を行うことはできません。
アプリケーションは、トランザクション実行中にクエリを実行できません。しかし、トランザクション中にキーを使用してデータストア エンティティを取得することは可能です。このとき、フェッチされたエンティティは他のトランザクションの内容と一致していることを保証します。トランザクション実行前にキーを準備するか、キー名または ID を使用してトランザクション内にキーを構築できます。
アプリケーションは、1 つのトランザクションで何度もエンティティの作成や更新を行うことはできません。
JDO は、tx.begin() へのコールと tx.commit() へのコールの間のすべてのアクションを 1 つのトランザクションから実行します。要求したエンティティ グループを他のプロセスが使用中のためにアクションが失敗した場合、JDO は java.util.ConcurrentModificationException から発生する JDODataStoreException または JDOException を発行します。
オプティミスティック コンカレンシを使用したシステムでは、通常はトランザクションが失敗となるまで、アプリケーションは何度か再試行を行います。JDO はトランザクションを 1 度しか実行しません。アプリケーションは、必要に応じてトランザクションを繰り返し実行する必要があります。次に例を示します。
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;
}
}
}
同じトランザクションで 1 つ以上のエンティティ グループを更新しようとすると、JDOFatalUserException が発生します。エンティティ グループの親のない各オブジェクトも、自身のエンティティ グループの中に存在します。1 つのトランザクションから親のない複数のエンティティを作成することはできないのは、この理由です。
1 つのトランザクションから同じエンティティを複数回更新しようとすると(makePersistent() コールの繰り返しなど)、JDOFatalUserException が発生します。その代わり、トランザクション内の永続オブジェクトのみを変更し、変更を適用する commit() へのコールを許可します。
この例では、トランザクションの 1 つの用途として、相対的な新しいプロパティ値を使って現在のエンティティのプロパティ値を更新します。
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 を使ってエンティティの作成や更新を行おうとしている場合です。トランザクションを使用していない場合、エンティティが存在せず、2 人のユーザーが作成しようとした場合、2 人目のユーザーが 1 人目のユーザーの更新内容を上書きしてしまいますが、上書きしたことに気づきません。 トランザクションを使用している場合、2 回目の作成はアトミック的に失敗し、アプリケーションが新しいエンティティの取得と更新を再試行します。
ヒント: トランザクションで使用するエンティティが変更されて、トランザクションの再試行が必要になる可能性を減らすため、トランザクションができるだけ速く実行されるようにしてください。トランザクションの外部データをできるだけ多く準備してからトランザクションを実行し、一定の状態に基づいてデータストア操作が行われるようにします。アプリケーションは、トランザクション内部で使用するオブジェクトのキーを準備してから、トランザクション内部のエンティティをフェッチするようにしてください。
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 データストアはグローバル トランザクションをサポートしていないため、グローバル トランザクションが利用可能なことを前提としているコードでは例外が発生します。(非常に大きなコードの可能性のある)コードベース全体を参照し、すべてのトランザクション管理コードを削除する代わりに、データストア トランザクションを無効にすることができるのです。データストア トランザクションを無効にしても、複数レコードの変更におけるアトミック性についてコードがアドレスを推測することには影響しません。しかし、アプリケーションを動作させる段階において、トランザクションのコードのリファクタをすべて一度に実行するのではなく、必要に応じて一つずつ増やすように変更できます。