db4o その16 (コミット時コールバックの罠)

先日言及したコミット時コールバックに関してだが、例えばコミット完了時に「トランザクションのコンテキスト内で追加されたオブジェクト」を全てtoString()メソッドでダンプするには以下のように記述する。

ObjectContainer db = Db4o.openFile("roster.db");
try {
    EventRegistry registry = EventRegistryFactory.forObjectContainer(db);
    registry.committed().addListener(new EventListener4() {
        @Override
        public void onEvent(Event4 e, EventArgs args) {
            CommitEventArgs commitArgs = (CommitEventArgs)args;
            Iterator iterator = commitArgs.added().iterator();
            int count = 1;
            while (iterator.moveNext()) {
                ObjectInfo info = (ObjectInfo)iterator.current();
                Object o = info.getObject();
                System.out.println( count + " class = " + o.getClass().getName() + " " + o);
            }
        }
    });

    〜 データベース登録などの処理

    db.commit();
} catch (Exception e) {
    db.rollback();
} finally {
    db.close();
}

これを実行すると標準出力には以下のように出力された。

1 class = PlayerImpl PlayerImpl[ name=D.Jeter, team=NYY, position=SS, stats{...}]
2 class = java.util.ArrayList  [BattingStatsImpl[year=2007, games=10, atBat=44, runs=9, hits=14, homerun=0, runsBattedIn=3, onBasePercentage=0.4, slugging=0.341, average=0.318]]
3 class = BattingStatsImpl  BattingStatsImpl[year=2007, games=10, atBat=44, runs=9, hits=14, homerun=0, runsBattedIn=3, onBasePercentage=0.4, slugging=0.341, average=0.318]
  :
  :
  :
19 class = PlayerImpl PlayerImpl[ name=null, team=null, position=null, ]
20 class = java.util.ArrayList  
21 class = BattingStatsImpl  BattingStatsImpl[ year=0, games=0, atBat=0, runs=0, hits=0, homerun=0, runsBattedIn=0, onBasePercentage=0.0, slugging=0.0, average=0.0]
22 class = PlayerImpl   PlayerImpl[ name=null, team=null, position=null, ]
23 class = java.util.ArrayList  
24 class = BattingStatsImpl  BattingStatsImpl[ year=0, games=0, atBat=0, runs=0, hits=0, homerun=0, runsBattedIn=0, onBasePercentage=0.0, slugging=0.0, average=0.0]
  :
  :
  以下、繰り返し

1〜3までは予想通りの出力であり、それぞれデータベースにコミットした

1.Playerクラスのインスタンス
2.同インスタンス中のフィールドである"stats"として登録されたjava.util.ArrayListクラスのインスタンス
3.同"stats"フィールドに追加されたBattingStatsImplクラスのインスタンス

であるが、おかしいのが番号19以降である。型名は正しいが全てのインスタンスとその全てのフィールドが空なのである。

19 class = PlayerImpl PlayerImpl[ name=null, team=null, position=null, ]
20 class = java.util.ArrayList  []
21 class = BattingStatsImpl  BattingStatsImpl[ year=0, games=0, atBat=0, runs=0, hits=0, homerun=0, runsBattedIn=0, onBasePercentage=0.0, slugging=0.0, average=0.0]
:

そんな馬鹿なと思い、実行後のデータベースの内容をdb4objects ObjectManagerで見てみたが、きちんと全てのデータが登録されている。

コミット時コールバックはまだ試用レベルでしかないので、最初は開発中のバグだと思ったのだがそうではない。実はコミット時コールバックでリストされる変更差分のオブジェクト、それもcommittedイベント時の変更差分自体がデーターベースに反映済みのオブジェクトの内容であるということ。つまり「取得されたオブジェクトのメンバ全てがアクティブ化されるとは限らない」というdb4oの原則(仕様)はこの変更差分のオブジェクトセットにも有効なのである。

原因が解れば修正するのは簡単だ。変更差分のオブジェクト全てを使いたい場合は、イベントコールバックの通知時に取得したオブジェクトを参照する前に「アクティブ化」してやればよい。

・修正後のソース

final ObjectContainer db = Db4o.openFile("roster.db");
try {
    EventRegistry registry = EventRegistryFactory.forObjectContainer(db);
    registry.committed().addListener(new EventListener4() {
        @Override
        public void onEvent(Event4 e, EventArgs args) {
            CommitEventArgs commitArgs = (CommitEventArgs)args;
            Iterator iterator = commitArgs.added().iterator();
            int count = 1;
            while (iterator.moveNext()) {
                ObjectInfo info = (ObjectInfo)iterator.current();
                Object o = info.getObject();
                db.activate(o, Integer.MAX_VALUE);
                System.out.println( count + " class = " + o.getClass().getName() + " " + o);
            }
        }
    });

    〜 データベース登録などの処理

    db.commit();
} catch (Exception e) {
    db.rollback();
} finally {
    db.close();
}

この場合はコミット時に追加されたオブジェクト"全て"をアクティブ化するが、そもそもクラス毎にアクティブ化を実施するかを前もって設定することもできる。

Db4o.configure().objectClass(PlayerImpl.class).cascadeOnActivate(true);
Db4o.configure().objectClass(BattingStatsImpl.class).cascadeOnActivate(true);
Db4o.configure().objectClass(ArrayList.class).cascadeOnActivate(true);

db4oに限らずOODBを使う場合に注意すべきことだが、ネストされたメンバをトラバースしていくことは直接処理コストに跳ね返る操作なので、必要に応じてアクティブ化すべきだ。