Google App Engineでトランザクションを使う


本サイトでは、過去に何度かGoogle App Engineのデータストアの利用方法について解説してきました。
今回はそのデータストアへアクセスする際にトランザクションを利用する方法について解説します。

普段SQLなどのデータベースを使っている方にはおなじみの技術だと思いますが、(データベースにおける)トランザクションとは、データベースへのデータの保存、取得、更新など一連の処理を、一つの処理として扱うことです。
例えば、トランザクションでまとめられた一連の処理の途中でエラーが発生した場合、下図のようにすべての処理を取り消してデータベース変更前の状態を保ちます。

トランザクションを用いることで、一連の処理として扱うべき操作が途中で中断してしまうことで発生するデータの不整合を防ぐことができます。
例えばテーブルAからテーブルBへデータを移動するような場合、Aからの取得と削除およびBへの挿入は一連の処理としておかないと、Aから取得と削除はしたが、Bへの挿入中にエラーとなってしまった場合、データは単にAから失われてしまいます。
ポイントとしては
DatastoreServiceクラスのbeginTransactionメソッドでトランザクションを開始
Transactionクラスのcommitメソッドでトランザクション処理の確定
Transactionクラスのrollbackメソッドでトランザクション中の処理の取り消し

それでは具体的にソースコードを解説していきます。

トランザクションを実装する

それでは早速、トランザクションを実装してみます。
まずはトランザクションを実装していない場合のデータの登録を見てみましょう。
NonTransactionServlet.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class ImageUploaderServlet extends HttpServlet {
    public void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws IOException {
 
        DatastoreService dss = DatastoreServiceFactory.getDatastoreService();
 
        Key key = KeyFactory.createKey("QuerySpace", name);
        // エンティティ生成
        Entity entity = new Entity(key);
        // エンティティにデータセット
        entity.setProperty("name", "seit");
        entity.setProperty("age", "25");
        entity.setProperty("gender", "man");
        // データストアに保存
        dss.put(entity);
    }
}

至ってシンプルです。QuerySpaceというカインドに対して、生成したエンティティを保存しています。
(※カインドはテーブル、エンティティは行を意味します。詳細はLow Level APIでGAEのデータストアにアクセスする を参考にしてください。)

ではこのソースコードにトランザクションを実装してみます。
以下がそのソースコードになります。
TransactionServlet.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class ImageUploaderServlet extends HttpServlet {
    public void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws IOException {
 
        DatastoreService dss = DatastoreServiceFactory.getDatastoreService();
 
        Transaction transaction = dss.beginTransaction();
        try{
            Key key = KeyFactory.createKey("QuerySpace", name);
            // エンティティ生成
            Entity entity = new Entity(key);
            // エンティティにデータセット
            entity.setProperty("name", "seit");
            entity.setProperty("age", "25");
            entity.setProperty("gender", "man");
            // データストアに保存
            dss.put(entity);
 
            transaction.commit();
        }catch(Exception e){
            resp.setContentType("text/plain");
            try {
                resp.getWriter().println(e.getMessage());
            } catch (IOException e1) {
                e1.printStackTrace();
            }
        }finally{
            if(transaction.isActive()){
                transaction.rollback();
            }
        }
    }
}

ハイライトになっている箇所が主に変更された点です。
7行目でDatastoreServiceクラスのbeginTransactionメソッドを用いてトランザクションを開始しています。
19行目でTransactionクラスのcommitメソッドを用いてトランザクションを完了しています。
データストアへの保存の最中にExceptionが発生しなければ、データの保存が確定されます。
保存中にExeptionが発生した場合には、コミットが行われずに28〜30行目の処理に入ります。
28〜30行目では、トランザクションがアクティブ(つまりトランザクションが完了していない)場合に、ロールバックを行うことで、データベースの状態をトランザクション開始前の状態にまで戻しています。

以上が基本的なトランザクションの使い方になります。

トランザクションの有効性を確認する

それではせっかくなので、トランザクション中にエラーが発生した場合、正常にロールバックが行われていることを確認してみます。
データストアには501文字以上の文字列を保存できないという制限があります。
この制限を利用し、データの保存時にExceptionを発生させることで、ロールバックを行います。

まずは以下のソースコードを見て下さい。
TransactionServlet.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class ImageUploaderServlet extends HttpServlet {
    public void doGet(HttpServletRequest req, HttpServletResponse resp)
        throws IOException {
        String fifty = "";
        DatastoreService dss = DatastoreServiceFactory.getDatastoreService();
 
        Transaction transaction = dss.beginTransaction();
        try{
            Key key = KeyFactory.createKey("QuerySpace", name);
            // エンティティ生成
            Entity entity1 = new Entity(key);
            // エンティティにデータセット
            entity1.setProperty("name", "seit");
            entity1.setProperty("age", "25");
            entity1.setProperty("gender", "man");
            // データストアに保存
            dss.put(entity1);
 
            // こちらは失敗する
            Entity entity2 = new Entity(key);
            entity2.setProperty("name", fifty);
            entity2.setProperty("age", 25);
            entity2.setProperty("gender", "woman");
            dss.put(entity2);
 
            transaction.commit();
        }catch(Exception e){
            resp.setContentType("text/plain");
            try {
                resp.getWriter().println(e.getMessage());
            } catch (IOException e1) {
                e1.printStackTrace();
            }
        }finally{
            if(transaction.isActive()){
                transaction.rollback();
            }
        }
    }
}

注目は21行目のString型の変数fiftyをエンティティにセットしているところです。
fiftyには半角英数503文字の文字列が定義されています。
まず、トランザクションを行わなかった場合のデータストアの結果が以下になります。

entity1は正常に保存されていますが、entity2は503文字の文字列保存に失敗し、エラーとなって保存されていません。
トランザクションを用いない場合、このようにデータベースが処理が途中の状態になってしまいます。
では上記ソースコード(トランザクションあり)の場合はどうでしょうか。

entity1とentity2の両方が保存されていないことがわかります。
これは、21行目でentity2の保存時にExceptionが発生したために、commitメソッドが呼び出されないまま処理がfinally句(29〜33行目)に移り、commitメソッドが呼ばれていないためにトランザクション処理中(アクティブ)だと判断され、ロールバック(31行目)が行われたことを示しています。
ロールバックが行われたことで、トランザクション開始前の状態に戻されています。
つまり、entity1とentity2の両方が保存されているか、entity1とentity2の両方が保存されていないかのどちらかの状態に限定することができます。

デプロイできなくなった際の対処方

アプリケーションをデプロイしてトランザクションの動作を確認したりする場合に、トランザクションを終了できずにエラーとなってしまう場合があります。
この場合、トランザクション処理が実行中のままになってしまい、再度アプリケーションをデプロイしようとしても失敗してしまいます。
そういうときは以下のコマンドを実行してみましょう(Mac OS Xで実行する場合)。

$ sudo sh ./appengine-java-sdk-1.4.2/bin/appcfg.sh rollback ./workspace/QuerySample/war

(sudo  sh  [appcfgコマンドパス]  rollback  [ロールバック対象アプリケーションのwarフォルダロケーション])

これで実行中のトランザクションに対してロールバックを実行することができます。

まとめ

トランザクションはデータストアのオプション機能です。
トランザクションを用いなくとも、データの追加、更新、削除などは行うことは可能ですが、データストアアクセス時の安全性をあげるためにも、使用することをおすすめします。