db4o その10(NQの不具合とキャッシュ)

db4oのネィティブクエリ(NQ)だが、使い方によっては意図しない結果になる不具合?があることが判ったので書いておく。

今までのサンプルでも使用した"roster.db"で以下の順で処理を行うとする。

    1. 守備位置が"SS"である選手をNQで検索 (D.Jeterが結果に掛かるはず)
    2. 守備位置が"SS"である選手の守備位置を"3B"に変更してコミット
    3. 守備位置が"SS"である選手をNQで検索 (D.Jeterは結果に掛からないはず)

この通りの処理を以下のコードで実行してみよう。

ObjectServer os = Db4o.openServer("roster.db", 0);
ObjectContainer db = os.openClient();
try {
    printResult(nqForPosition(db, "SS"), "NQ for Pos = 'SS'");
    ObjectSet players = sodaForPlayerName(db, "D.Jeter");
    IPlayer player = players.next();
    player.setPosition("3B");
    db.set(player);
    db.commit();
    System.out.println(" -- D.Jeter Pos = 'SS' to '3B' updated. -- ");
    printResult(nqForPosition(db, "SS"), "NQ for Pos = 'SS'");
} catch (Exception e) {
    db.rollback();
} finally {
    db.close();
    os.close();
}
public static ObjectSet sodaForPlayerName(ObjectContainer db, String playerName) {
    Query query = db.query();
    query.constrain(IPlayer.class);
    query.descend("name").constrain(playerName);
    return query.execute(); 
}
public static ObjectSet nqForPosition(ObjectContainer db, final String position) {
    ObjectSet rosters = db.query(new Predicate(){
        @Override
        public boolean match(IPlayer candidate) {
        	return ( candidate.getPosition().equals(position));
        }
    });
    return rosters;
}
public static void printResult(java.util.Collection collection, String title) {
    System.out.println("");
    System.out.println(title);
    System.out.println("result count = " + collection.size());
    for( Object o : collection ){
        System.out.println(o);
    }
}

実行結果

NQ for Pos = 'SS'
result count = 1
 PlayerImpl[ name=D.Jeter, team=NYY, position=SS, stats{..}]
 -- D.Jeter Pos = 'SS' to '3B' updated. -- 
NQ for Pos = 'SS'
result count = 1
 PlayerImpl[ name=D.Jeter, team=NYY, position=3B, stats{..}]

これはおかしい。

更新処理を行って"D.Jeter"の守備位置を"3B"に変えており、それにより更新処理の後のNQによる問合せでは守備位置が"3B"に変わっているのが解るが、それならば何故そもそもクエリの検索条件である「守備位置が"SS"のオブジェクト」に引っ掛かるのだろう。

期待される結果

NQ for Pos = 'SS'
result count = 1
 PlayerImpl[ name=D.Jeter, team=NYY, position=SS, stats{..}]
 -- D.Jeter Pos = 'SS' to '3B' updated. -- 
NQ for Pos = 'SS'
result count = 0

のはずだ。

では今度は、クエリをNQでは無くSODAにして実行してみよう。(コードは変更部分だけを追記する)

ObjectServer os = Db4o.openServer("roster.db", 0);
ObjectContainer db = os.openClient();
try {
    printResult(sodaForPosition(db, "SS"), "SODA for Pos = 'SS'");
    ObjectSet players = sodaForPlayerName(db, "D.Jeter");
    IPlayer player = players.next();
    player.setPosition("3B");
    db.set(player);
    db.commit();
    System.out.println(" -- D.Jeter Pos = 'SS' to '3B' updated. -- ");
    printResult(sodaForPosition(db, "SS"), "SODA for Pos = 'SS'");
} catch (Exception e) {
    db.rollback();
} finally {
    db.close();
    os.close();
}
public static ObjectSet sodaForPosition(ObjectContainer db, String position) {
    Query query = db.query();
    query.constrain(IPlayer.class);
    query.descend("position").constrain(position);
    return query.execute(); 
}

実行結果

SODA for Pos = 'SS'
result count = 1
 PlayerImpl[ name=D.Jeter, team=NYY, position=SS, stats{..}]
 -- D.Jeter Pos = 'SS' to '3B' updated. -- 
SODA for Pos = 'SS'
result count = 0

今度は予想通りの結果だ。
NQで意図しない結果になったのは何故だろう。ひょっとしてNQの実行時評価の対象となっているオブジェクトがキャッシュされているのではないかと思い、変更前のクエリを実行しないようにしてみた。

ObjectServer os = Db4o.openServer("roster.db", 0);
ObjectContainer db = os.openClient();
try {
    ObjectSet players = sodaForPlayerName(db, "D.Jeter");
    IPlayer player = players.next();
    player.setPosition("3B");
    db.set(player);
    db.commit();
    System.out.println(" -- D.Jeter Pos = 'SS' to '3B' updated. -- ");
    printResult(nqForPosition(db, "SS"), "NQ for Pos = 'SS'");
} catch (Exception e) {
    db.rollback();
} finally {
    db.close();
    os.close();
}

実行結果

 -- D.Jeter Pos = 'SS' to '3B' updated. -- 
SODA for Pos = 'SS'
result count = 0

思ったとおり今度は上手く行く。(守備位置"SS"の選手はNQクエリに掛からない)

ここからは推測だが、やはりNQはその検索対象となるオブジェクトを可能であればキャッシュしているのではないか。NQは実際にはSODAに変換された実行されるが、最初の問合せと更新後の問合せではSODAへの変換及び最適化時に、条件等に変更無いと判断されてキャッシュされているオブジェクトが使われたのだ。

この問題を回避するためには、以下の複数の方法が有効だ。

  • セッションを一度終了する

db4oのセッションを終了することでキャッシュされているデータは全て破棄されるため、期待通りの結果となる。

ObjectServer os = Db4o.openServer("roster.db", 0);
ObjectContainer db = os.openClient();
printResult(nqForPosition(db, "SS"), "NQ for Pos = 'SS'");
ObjectSet players = sodaForPlayerName(db, "D.Jeter");
IPlayer player = players.next();
player.setPosition("3B");
db.set(player);
db.commit();
System.out.println(" -- D.Jeter Pos = 'SS' to '3B' updated. -- ");
db.close();
ObjectContainer db = os.openClient();
printResult(nqForPosition(db, "SS"), "NQ for Pos = 'SS'");

ObjectContainerインタフェースをクローズして再オープンするということはI/O処理を伴うので、ある程度の処理コストが発生するのは避けられないだろう。

  • キャッシュの破棄を明示的に行う

ExtObjectContainerインタフェースには不要なオブジェクトを破棄するために使用するpurgeメソッドが用意されているので、これを使う。

ObjectServer os = Db4o.openServer("roster.db", 0);
ObjectContainer db = os.openClient();
printResult(nqForPosition(db, "SS"), "NQ for Pos = 'SS'");
ObjectSet players = sodaForPlayerName(db, "D.Jeter");
IPlayer player = players.next();
player.setPosition("3B");
db.set(player);
db.commit();
System.out.println(" -- D.Jeter Pos = 'SS' to '3B' updated. -- ");
db.ext().purge();
printResult(nqForPosition(db, "SS"), "NQ for Pos = 'SS'");

purgeメソッドはObjectContainer内部の参照されているオブジェクトやそのメタデータを走査するため、セッションの終了と同様にそれなりの処理コストは発生するだろう。

  • NQを使わずSODAを使う

SODAではこの問題が発生しないことを確認しているため、NQを使わずにSODAを使うことで問題を回避できる。この方法では上記二つのように処理コストの発生は気にしなくとも良いが、SODAを使うのであればなんのためのNQだということになってしまう。

この問題、正直なところNQ(Native Query)の不具合だと思うが取りあえず問題を回避するための方法は知っておく必要があるだろう。