トランザクション

App Engineデータストアはトランザクションをサポートします。トランザクションは、すべて成功するかすべて失敗するか の命令の集合です。アプリケーションは1つのトランザクションで複数の命令と計算を実行できます。

トランザクションを使用する

トランザクションでは1つの、あるいは複数の命令はすべて成功するか、すべて失敗します。 トランザクションが成功すると、意図したすべての事柄がデータストアに反映されます。 トランザクションが失敗すると、すべての事柄が反映されません。

すべてのデータストアへの書き込み命令はアトミックです。エンティティの生成、更新、削除は、 行われるか行われないかのどちらかです。 コネクションが増え、多くのユーザーが同時にエンティティを修正しようとすると、 命令は高い確率で失敗するでしょう。 また、アプリケーションが(資源の)割り当て限度に届いてしまったため、命令が失敗するかもしれません。 また、データストアの内部エラーが発生するかもしません。 いずれのケースでも、命令の効果は反映されず、データストアのAPIは例外を発生させます。

JDOトランザクションAPIを用いた例です。ClubMemberクラスは、インクリメントフィールド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つのエンティティグループに対して構成し、 すべての命令はそのグループに対し、成功するか失敗するかのいずれかです。

アプリケーションがエンティティを生成すると、そのエンティティは別のエンティティを親として割り当てることができます。 生成されたエンティティが親を指定することで、親エンティティと同じグループに属することになります。

親を持たないエンティティはルートエンティティです。あるエンティティの親であるエンティティもまた、親を持てます。 ルートまでつながるエンティティのチェーンを、エンティティのパスと呼び、メンバーのパスに含まれるエンティティを 祖先とします。あるエンティティの親は生成時に定義され、後で変更することはできません。

あるルートエンティティ以下のすべてのエンティティは、同じエンティティグループです。 すべてのグループ内のエンティティは、同じデータストアノード上に保存されます。 1つのトランザクション内では1つのグループ内の複数のエンティティのみを修正でき、 また、新しいエンティティをそのグループ内の1つのエンティティの子として追加できます。

エンティティグループとエンティティの生成

App EngineのJODの実装では、エンティティグループを使ってOwnedな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のオブジェクト(親エンティティををもつ)を生成する場合、 エンティティグループの親キーフィールドを使用しなければなりません。 親キーフィールドに親のキーをセットし、オブジェクトキーフィールドはnullにしておきます。 オブジェクトが保存されるとき、データストアがキーフィールドに、 エンティティグループの親を含む完全なキーを割り当てます。

クラスがエンティティグループの親フィールドを持つ場合、 クエリ内でパーレンとフィールドに対し、イコールフィルターを使用することが出来ます。 (親キーへのイコール以外の比較フィルターはサポートされていません)

トランザクションで何ができるのか

データストアは1つのトランザクションで行えることに、幾つかの制限を課します。

すべてのトランザクション上でのデータストア命令は、同じエンティティグループに対してでなければなりません。 これはクエリやキーによるエンティティの取り出し、更新、削除を含みます。 各ルートエンティティは別のエンティティグループに属していることに注意してください。 そのために、1つのトランザクションでは複数個のルートエンティティを作成できません。 エンティティグループの説明は、エンティティグループの項目を参照ください。

アプリケーションはトランザクション中にクエリを実行できますが、祖先に対するフィルタを使ったものしか実行できません。 アプリケーションはトランザクション中にキーを用いてエンティティを取得できます。 キーはトランザクションの前に用意しておくか、String、IDを使ったキーをトランザクション中作成します。

アプリケーションは1つのトランザクションの中では1回しかエンティティを作成・更新できません。

JDOはすべてのアクションを1つのトランザクションの中の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;
                }
            }
        }
   

トランザクション内で1つ以上のエンティティグループを更新しようとすると、JDOFatalUserExceptionがスローされます。 親を持たないエンティティは、自分自身のエンティティグループに属していることに注意してください。 そのため、1つのトランザクションで複数の親のないエンティティを作成できません。

1つのトランザクションで1つのエンティティを複数回操作すると(例えば、makePersistent()の複数回呼び出し) JDOFatalUserExceptionがスローされます。 そうせずに、トランザクション内ではオブジェクトを操作し、commit()で変更を適用させます。

独立性と整合性

データストアのアイソレーションレベルは、トランザクションの外ではリードコミッティッドに似ています。 一方、トランザクション内では、シリアライザブルです(snapshot isolation)。 アイソレーションレベルに関しては、 トランザクションアイソレーションの記事を参照ください。

トランザクション内でのクエリやエンティティの取得は、整合性のとれた1つだけのデータストアのスナップショットに対して 行われることが保障されます。特に、トランザクションのエンティティグループでのでのエンティティ・インデックス列は完全に更新されるため、 クエリは完全で正確な結果エンティティを返し、 トランザクションアイソレーション で述べられているようなfalse positivesやfoase negativesは、トランザクションの外側とは違い起こることがありません。

この変化しないスナップショットは、トランザクション内における書き込み後の読み込みにも拡張されます。 ほとんどのデータベースとは異なり、クエリやエンティティの取得は、トランザクション内で以前に行った書き込み内容を取得しません。 特に、トランザクションの中でエンティティが修正・削除されると、クエリ・エンティティの取得は、トランザクション開始所の エンティティを返すか、エンティティが無ければ何も返しません。

トランザクションの使用

トランザクション使用のサンプルデモです。 エンティティのプロパティを、現在のプロパティ値に応じて変更しています。


        Key k = KeyFactory.createKey("Employee", "k12345");
        Employee e = pm.getObjectById(Employee.class, k);
        e.counter += 1;
        pm.makePersistent(e);
   

このサンプルはトランザクションが必要です。なぜなら、ユーザーが値を取得して保存するまでのあいだに、 別のユーザーが更新する可能性があるからです。 トランザクションがなければ、ユーザーリクエストはcounterの値を、他のユーザーが更新する前に取得し、 新しい値を上書きすることになります。トランザクションがあれば、アプリケーションは他のユーザーの更新を知らせられます。 エンティティがトランザクション中に更新されると、トランザクションは例外を投げます。 アプリケーションは新しいデータを使用するために、トランザクションを再実行します。

別の一般的なトランザクションの使用方法は、named keyによるエンティティの更新や、エンティティの作成です。


        // 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人のユーザーが同じ文字列型IDのエンティティを作成し、1人目の内容を2人目が上書きしてしまいます。 トランザクションがあれば、2人目の試みは失敗し、アプリケーションは新しく更新されたエンティティを取得することができます。

Tip:トランザクションによって使用されているエンティティが変更されてしまい、再試行する可能性を減らすためにも、 トランザクションは、可能な限り速く実行されるべきです。 出来る限り、データはトランザクションの外側で用意し、一貫した状態を必要とするデータストア操作をトランザクションで実行すべきです。 アプリケーションは、トランザクションの内側で使用するキーを用意しておき、その後トランザクションの内側でエンティティを取得すべきです。

トランザクションはデータストアの整合性のとれたスナップショットを使用します。 これは、整合性のとれたページ出力やデータエクスポートを行うために、複数の読み込みをしなければならないときに便利です。 これらの種類のトランザクションは書き込みを行わないため、リードオンリートランザクションと呼ばれることがあります。 リードオンリートランザクションのコミット・ロールバックは共にno-opsです。【?】


        // PersistenceManager pm = ...;
        Transaction tx = pm.currentTransaction();
        User user = userService.currentUser();
        List accounts = new ArrayList();

        try {
            tx.begin();
        
            Query query = pm.newQuery("select from Customer " +
                                      "where user == userParam " +
                                      "parameters User userParam");
            List customers = (List)
            query.execute(user);
        
            query = pm.newQuery("select from Account " +
                                "where parent-pk == keyParam " +
                                "parameters Key keyParam");
            for (Customer customer : customers) {
                accounts.addAll((List)
                query.execute(customer.key));
            }
        
        } finally {
            if (tx.isActive()) {
                tx.rollback();
            }
        }
   

トランザクションの無効化と既存のJDOアプリの移植

我々の推奨するJDOの設定は、datanucleus.appengine.autoCreateDatastoreTxnsというプロパティをtrueにすることです。 これはApp Engine特有のプロパティで、JDOの実装に、データストアのトランザクションとアプリケーションコードで運用されている JDOトランザクションを関連づけさせます。新規にアプリケーションを作成する場合は、これはあなたの望むとこだと思われます。 しかしすでに存在しているJDOベースのアプリケーションをApp Engine上で動作させたい場合、 プロパティ値が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のデータストアはグローバルトランザクションをサポートしておらず、例外をスローします。 ソースコードをくまなく調べ、トランザクションを管理しているコードを取り除くのではなく、 単純にデータストアトランザクションを無効にするだけで済みます。 この方法はもちろん、あなたのコードの複数レコードの修正をアトミックにするというわけではありません。 しかし、アプリケーションは動作するようになり、トランザクションコードをすべてではなく、 必要に応じて順番に修正することができるようになります。

戻る 原文