トランザクション

Cloud Datastore はトランザクションをサポートします。トランザクションは不可分な一連のオペレーションから構成されます。つまり、トランザクション内のオペレーションが部分的に実行されることはありません。アプリケーションは、1 つのトランザクションで複数のオペレーションと計算を実行できます。

トランザクションの使用

トランザクションは、Cloud Datastore が 1 つまたは複数のエンティティに行う一連のオペレーションです。トランザクションのオペレーションは不可分で、部分的に実行されることはありません。トランザクション内のオペレーションはすべて実行されるか、まったく実行されないかのいずれかになります。トランザクションの持続時間は最大 60 秒で、30 秒後に 10 秒間のアイドル時間が発生します。

次の場合、オペレーションが失敗します。

  • 1 つのエンティティ グループに同時に実行される変更の数が多すぎる場合。
  • トランザクションがリソース制限を超過している場合。
  • Cloud Datastore で内部エラーが発生した場合。

いずれの場合も、Cloud Datastore API で例外が発生します。

トランザクションは Cloud Datastore のオプション機能です。Cloud Datastore オペレーションを実行するために、必ずしもトランザクションを使用する必要はありません。

次の例では、Joe という名前の Employee エンティティで、vacationDays という名前のフィールドを更新しています。

Java 8

DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
Transaction txn = datastore.beginTransaction();
try {
  Key employeeKey = KeyFactory.createKey("Employee", "Joe");
  Entity employee = datastore.get(employeeKey);
  employee.setProperty("vacationDays", 10);

  datastore.put(txn, employee);

  txn.commit();
} finally {
  if (txn.isActive()) {
    txn.rollback();
  }
}

Java 7

DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
Transaction txn = datastore.beginTransaction();
try {
  Key employeeKey = KeyFactory.createKey("Employee", "Joe");
  Entity employee = datastore.get(employeeKey);
  employee.setProperty("vacationDays", 10);

  datastore.put(txn, employee);

  txn.commit();
} finally {
  if (txn.isActive()) {
    txn.rollback();
  }
}

コードの説明を簡単にするため、トランザクションがアクティブな状態でロールバックを実行する finally ブロックが省略されている場合があります。本番環境のコードでは、すべてのトランザクションを明示的に commit またはロールバックする必要があります。

エンティティ グループ

どのエンティティも 1 つのエンティティ グループに属しています。このグループは、1 回のトランザクションで操作可能な 1 つ以上のエンティティから構成されます。App Engine では、エンティティ グループの関係に基づいて、複数のエンティティが分散ネットワークの同じ場所に格納されます。トランザクションでは、1 つのエンティティ グループに Cloud Datastore オペレーションが設定されます。このオペレーションはすべてグループとして適用されます。トランザクションが失敗した場合には、どのオペレーションも適用されません。

アプリケーションでエンティティを作成するときに、別のエンティティを新しいエンティティの親に設定できます。新しいエンティティに親を割り当てると、このエンティティは親エンティティと同じグループに属します。

親のないエンティティは、ルート エンティティとなります。子を持つエンティティに親を指定することもできます。エンティティからルートまでの親エンティティの連鎖が、そのエンティティのパスになります。パスのメンバーがエンティティの祖先になります。エンティティの親はエンティティの作成時に定義され、後で変更することはできません。

特定のルート エンティティを祖先とするエンティティはすべて同じエンティティ グループに属します。グループ内のすべてのエンティティは同じ Cloud Datastore ノードに保存されます。1 回のトランザクションで同じグループ内の複数のエンティティを修正できます。また、グループ内の既存のエンティティを新しいエンティティの親にし、1 回のトランザクションでエンティティをグループに追加することもできます。次のコードは、さまざまな型のエンティティに対するトランザクションを示しています。

Java 8

DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
Entity person = new Entity("Person", "tom");
datastore.put(person);

// Transactions on root entities
Transaction txn = datastore.beginTransaction();

Entity tom = datastore.get(person.getKey());
tom.setProperty("age", 40);
datastore.put(txn, tom);
txn.commit();

// Transactions on child entities
txn = datastore.beginTransaction();
tom = datastore.get(person.getKey());
Entity photo = new Entity("Photo", tom.getKey());

// Create a Photo that is a child of the Person entity named "tom"
photo.setProperty("photoUrl", "http://domain.com/path/to/photo.jpg");
datastore.put(txn, photo);
txn.commit();

// Transactions on entities in different entity groups
txn = datastore.beginTransaction();
tom = datastore.get(person.getKey());
Entity photoNotAChild = new Entity("Photo");
photoNotAChild.setProperty("photoUrl", "http://domain.com/path/to/photo.jpg");
datastore.put(txn, photoNotAChild);

// Throws IllegalArgumentException because the Person entity
// and the Photo entity belong to different entity groups.
txn.commit();

Java 7

DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
Entity person = new Entity("Person", "tom");
datastore.put(person);

// Transactions on root entities
Transaction txn = datastore.beginTransaction();

Entity tom = datastore.get(person.getKey());
tom.setProperty("age", 40);
datastore.put(txn, tom);
txn.commit();

// Transactions on child entities
txn = datastore.beginTransaction();
tom = datastore.get(person.getKey());
Entity photo = new Entity("Photo", tom.getKey());

// Create a Photo that is a child of the Person entity named "tom"
photo.setProperty("photoUrl", "http://domain.com/path/to/photo.jpg");
datastore.put(txn, photo);
txn.commit();

// Transactions on entities in different entity groups
txn = datastore.beginTransaction();
tom = datastore.get(person.getKey());
Entity photoNotAChild = new Entity("Photo");
photoNotAChild.setProperty("photoUrl", "http://domain.com/path/to/photo.jpg");
datastore.put(txn, photoNotAChild);

// Throws IllegalArgumentException because the Person entity
// and the Photo entity belong to different entity groups.
txn.commit();

特定のエンティティ グループにエンティティを作成する

アプリケーションで作成した新しいエンティティをエンティティ グループに割り当てるには、他のエンティティのキーを指定します。次の例では、MessageBoard エンティティに作成したキーを使用して、MessageBoard と同じエンティティ グループにある Message エンティティを作成し、永続化しています。

Java 8

DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();

String messageTitle = "Some Title";
String messageText = "Some message.";
Date postDate = new Date();

Key messageBoardKey = KeyFactory.createKey("MessageBoard", boardName);

Entity message = new Entity("Message", messageBoardKey);
message.setProperty("message_title", messageTitle);
message.setProperty("message_text", messageText);
message.setProperty("post_date", postDate);

Transaction txn = datastore.beginTransaction();
datastore.put(txn, message);

txn.commit();

Java 7

DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();

String messageTitle = "Some Title";
String messageText = "Some message.";
Date postDate = new Date();

Transaction txn = datastore.beginTransaction();

Key messageBoardKey = KeyFactory.createKey("MessageBoard", boardName);

Entity message = new Entity("Message", messageBoardKey);
message.setProperty("message_title", messageTitle);
message.setProperty("message_text", messageText);
message.setProperty("post_date", postDate);
datastore.put(txn, message);

txn.commit();

クロスグループ トランザクションを使用する

クロスグループ トランザクション(XG トランザクション)は、複数のエンティティ グループにオペレーションを実行します。クロスグループ トランザクションでは、1 つのグループに対するトランザクションとは異なり、複数のエンティティ グループのエンティティを更新してもトランザクションが失敗することはありません。

クロスグループ トランザクションの使い方は 1 つのグループに対するトランザクションと類似していますが、次のように TransactionOptions を使用して、トランザクションの開始時にクロスグループ トランザクションであることを指定する必要があります。

Java 8

DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
TransactionOptions options = TransactionOptions.Builder.withXG(true);
Transaction txn = datastore.beginTransaction(options);

Entity a = new Entity("A");
a.setProperty("a", 22);
datastore.put(txn, a);

Entity b = new Entity("B");
b.setProperty("b", 11);
datastore.put(txn, b);

txn.commit();

Java 7

DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
TransactionOptions options = TransactionOptions.Builder.withXG(true);
Transaction txn = datastore.beginTransaction(options);

Entity a = new Entity("A");
a.setProperty("a", 22);
datastore.put(txn, a);

Entity b = new Entity("B");
b.setProperty("b", 11);
datastore.put(txn, b);

txn.commit();

トランザクションで実行可能な処理

Cloud Datastore では、1 つのトランザクション内で実行できる処理に制限を設定しています。

1 つのグループに対するトランザクションの場合、トランザクション内の Cloud Datastore オペレーションは、同じエンティティ グループのエンティティに実行する必要があります。クロスグループ トランザクションの場合には、処理を実行できるエンティティ グループの数が 25 に制限されています。エンティティのクエリで祖先を使用したり、キーを使用してエンティティを取得したりできます。また、エンティティの更新や削除も実行できます。ルート エンティティはそれぞれ別のエンティティ グループに属しているため、1 つのトランザクション内で複数のルート エンティティを作成したり、操作したりするには、クロスグループ トランザクションを使用する必要があります。

1 つ以上の共通エンティティ グループで複数のトランザクションがエンティティを同時に変更しようとすると、変更を commit した最初のトランザクションは成功しますが、他のすべてのトランザクションは commit に失敗します。このため、エンティティ グループを使用する場合、グループ内のエンティティに同時に実行できる書き込みの数が制限されます。トランザクションが開始されると、Cloud Datastore はトランザクションで使用されるエンティティ グループの最終更新時間をチェックして、楽観的同時実行制御を使用します。エンティティ グループのトランザクションを commit する時点で、Cloud Datastore はトランザクションで使用されたエンティティ グループの最終更新時間を再度チェックします。最初のチェック以降に変更が行われていると、例外をスローします。エンティティ グループの詳細については、Cloud Datastore の概要をご覧ください。

アプリでは、トランザクション内でクエリを実行できますが、実行するには、祖先フィルタを使用する必要があります。また、トランザクション内でキーを使用してデータストア エンティティを取得できます。このキーは、トランザクションの開始前に準備することも、トランザクション内でキー名または ID を指定して作成することもできます。

分離と整合性

トランザクションの外部では、データストアの分離レベルは「read committed(commit 済みデータの読み取り)」に最も近くなります。トランザクション内部では、「serializable(シリアル化可能)」分離レベルが強制的に適用されます。つまり、このトランザクションによって読み取りまたは変更が行われているデータを、他のトランザクションが同時に変更することはできません。分離レベルの詳細については、シリアル化可能な分離に関する Wiki ページとトランザクション分離に関する記事をご覧ください。

トランザクションではすべての読み取りに、トランザクション開始時におけるデータストアの最新かつ一貫した状態が反映されます。トランザクション内の各クエリと get は、トランザクション開始時のデータストアに対する単一の一貫したスナップショットを参照することが保証されています。トランザクションのエンティティ グループ内の各エンティティとインデックス行は完全に更新されるため、クエリは完全で正確な結果セットとしてエンティティを返すことができます。トランザクション外部で生じる可能性のある、偽陽性または偽陰性といった誤検知(トランザクション分離を参照)が生じることはありません。

こうした一貫性のあるスナップショット ビューは、トランザクション内の書き込み後の読み取りにまで拡張されています。大半のデータベースと異なり、データストア トランザクション内のクエリと get は、トランザクション内の以前の書き込み結果を参照しません。特にトランザクション内でエンティティが変更または削除された場合は、クエリまたは get により、トランザクション開始時のオリジナル バージョンのエンティティが返されます。または、トランザクション開始時にこのエンティティが存在しなかった場合は何も返されません。

トランザクションの用法

この例では、トランザクションを使用して、現在の値より新しいプロパティ値でエンティティを更新します。Cloud Datastore API はトランザクションを再試行しないため、ロジックを追加して、別のリクエストが同じ MessageBoard または任意の Messages を同時に更新したときにトランザクションを再試行します。

Java 8

int retries = 3;
while (true) {
  Transaction txn = datastore.beginTransaction();
  try {
    Key boardKey = KeyFactory.createKey("MessageBoard", boardName);
    Entity messageBoard = datastore.get(boardKey);

    long count = (Long) messageBoard.getProperty("count");
    ++count;
    messageBoard.setProperty("count", count);
    datastore.put(txn, messageBoard);

    txn.commit();
    break;
  } catch (ConcurrentModificationException e) {
    if (retries == 0) {
      throw e;
    }
    // Allow retry to occur
    --retries;
  } finally {
    if (txn.isActive()) {
      txn.rollback();
    }
  }
}

Java 7

int retries = 3;
while (true) {
  Transaction txn = datastore.beginTransaction();
  try {
    Key boardKey = KeyFactory.createKey("MessageBoard", boardName);
    Entity messageBoard = datastore.get(boardKey);

    long count = (Long) messageBoard.getProperty("count");
    ++count;
    messageBoard.setProperty("count", count);
    datastore.put(txn, messageBoard);

    txn.commit();
    break;
  } catch (ConcurrentModificationException e) {
    if (retries == 0) {
      throw e;
    }
    // Allow retry to occur
    --retries;
  } finally {
    if (txn.isActive()) {
      txn.rollback();
    }
  }
}

このコードがオブジェクトをフェッチした後、変更済みオブジェクトを保存する前に値が別のユーザーによって更新されてしまう可能性があるため、トランザクションが必要になります。トランザクションを使用しないと、最初のユーザーのリクエストに対し、もう 1 人のユーザーによる更新前の count 値が使用され、保存によって新しい値が上書きされてしまいます。トランザクションを使用することで、もう 1 人のユーザーによる更新がアプリケーションに通知されます。トランザクションでエンティティが更新されると、トランザクションが失敗し、ConcurrentModificationException を返します。アプリケーションはトランザクションを繰り返し、新しいデータを使用します。

トランザクションのもう 1 つの一般的な用法は、名前付きキーによってエンティティを取得することです。名前付きキーがまだ存在しない場合は、新規に作成します。

Java 8

Transaction txn = datastore.beginTransaction();
Entity messageBoard;
Key boardKey;
try {
  boardKey = KeyFactory.createKey("MessageBoard", boardName);
  messageBoard = datastore.get(boardKey);
} catch (EntityNotFoundException e) {
  messageBoard = new Entity("MessageBoard", boardName);
  messageBoard.setProperty("count", 0L);
  boardKey = datastore.put(txn, messageBoard);
}
txn.commit();

Java 7

Transaction txn = datastore.beginTransaction();
Entity messageBoard;
Key boardKey;
try {
  boardKey = KeyFactory.createKey("MessageBoard", boardName);
  messageBoard = datastore.get(boardKey);
} catch (EntityNotFoundException e) {
  messageBoard = new Entity("MessageBoard", boardName);
  messageBoard.setProperty("count", 0L);
  boardKey = datastore.put(txn, messageBoard);
}
txn.commit();

同じ文字列 ID を持つエンティティを別のユーザーが作成または更新しようとする状況を処理するには、前の例と同様、ここでもトランザクションが必要です。トランザクションを使用しない場合は、エンティティがまだ存在していない時点で 2 人のユーザーが同時にこれを作成しようとすると、1 人目のユーザーが知らないうちに 2 人目のユーザーが作成したエンティティが 1 人目のエンティティを上書きしてしまいます。トランザクションを使用すると、2 つ目の試行は自動的に失敗します。必要であれば、アプリケーションはエンティティの取得を試み、更新を行います。

トランザクションが失敗した場合は、成功するまでアプリケーションにトランザクションを再試行させることができます。またはアプリケーション ユーザーのインターフェース レベルまでエラーを伝達し、ユーザーにこのエラーを処理させることもできます。すべてのトランザクションに対し、再試行ループを作成する必要はありません。

これで、トランザクションを使用して Cloud Datastore の一貫性のあるスナップショットを読み取れるようになりました。こうした手法は、整合性が要求されるページの表示やデータのエクスポートに、複数回の読み取りが必要となるような場合に役立ちます。このような種類のトランザクションは、書き込みを伴わないため、読み取り専用トランザクションとも呼ばれます。読み取り専用の単一グループによるトランザクションは、同時変更によって失敗することはないため、失敗した場合の再試行を実装する必要はありません。ただし、クロスグループ トランザクションは、同時変更によって失敗する可能性があるため、再試行が必要になります。読み取り専用トランザクションの commit とロールバックは自動的に実行されます。

Java 8

DatastoreService ds = DatastoreServiceFactory.getDatastoreService();

// Display information about a message board and its first 10 messages.
Key boardKey = KeyFactory.createKey("MessageBoard", boardName);

Transaction txn = datastore.beginTransaction();

Entity messageBoard = datastore.get(boardKey);
long count = (Long) messageBoard.getProperty("count");

Query q = new Query("Message", boardKey);

// This is an ancestor query.
PreparedQuery pq = datastore.prepare(txn, q);
List<Entity> messages = pq.asList(FetchOptions.Builder.withLimit(10));

txn.commit();

Java 7

DatastoreService ds = DatastoreServiceFactory.getDatastoreService();

// Display information about a message board and its first 10 messages.
Key boardKey = KeyFactory.createKey("MessageBoard", boardName);

Transaction txn = datastore.beginTransaction();

Entity messageBoard = datastore.get(boardKey);
long count = (Long) messageBoard.getProperty("count");

Query q = new Query("Message", boardKey);

// This is an ancestor query.
PreparedQuery pq = datastore.prepare(txn, q);
List<Entity> messages = pq.asList(FetchOptions.Builder.withLimit(10));

txn.commit();

トランザクション タスクをキューに追加する

タスクをキューに追加する処理を、Cloud Datastore トランザクションの一部として行うことができます。このようにすると、タスクがキューに追加される(かつ、キューへの追加が保証される)のは、トランザクションが正常に commit された場合のみとなります。トランザクションが commit されると、タスクはキューに登録されることが保証されます。キューに登録されても、タスクがすぐに実行されることが保証されるわけではありません。また、タスク内で実行される処理はすべて元のトランザクションとは別に実行されます。タスクは成功するまで再試行されます。これは、トランザクションの関連でキューに登録されたすべてのタスクに適用されます。

トランザクション タスクは、Cloud Datastore 以外のアクション(たとえば、購入確認のメールの送信など)を Cloud Datastore トランザクションに追加できるので便利です。また、データストアの処理をトランザクションに結合することもでき、たとえば、トランザクションが成功した場合にのみ他のエンティティ グループに対する変更をトランザクションの外部で commit することができます。

アプリケーションが 1 つのトランザクションの中でタスクキューに挿入できるトランザクション タスクは 5 個までです。トランザクション タスクの名前をユーザーが指定することはできません。

Java 8

DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
Queue queue = QueueFactory.getDefaultQueue();
Transaction txn = datastore.beginTransaction();
// ...

queue.add(txn, TaskOptions.Builder.withUrl("/path/to/handler"));

// ...

txn.commit();

Java 7

DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
Queue queue = QueueFactory.getDefaultQueue();
Transaction txn = datastore.beginTransaction();
// ...

queue.add(TaskOptions.Builder.withUrl("/path/to/handler"));

// ...

txn.commit();
このページは役立ちましたか?評価をお願いいたします。

フィードバックを送信...

Java 8 の App Engine スタンダード環境