db4o その4(Query by Example)

db4oのクエリは大きく分けて3種類あると書いたが、今回からそれぞれのクエリについて書こうと思う。

  • Query by Example (以降QBE)
  • Native Query (以降NQ)
  • Simple Object Database Access Query(以降SODA)

クエリのサンプルに関してのコードは前々回で紹介したIPlayerインタフェースの実装クラスPlayerImplのインスタンスが、以下のようにDBに前もって登録されていることを前提に書いている。
サンプルコードの動作確認プラットホームは Sunの最新のJSDK(Java6)を使用しているが、db4oは.NETに対応しているし、両プラットホームでは極めて近い実装を提供するように考慮されているので、.NET2.0 C#で同様のコードにポーティングするのは極めて簡単だろうと思う。

ObjectContainer db = Db4o.openFile("stats.db");
try {
    //from NYY Roster Batting Stats at 04/17
    IPlayer player = new PlayerImpl();
    player.setName("D.Jeter");
    player.setTeam("NYY");
    player.setPosition("SS");
    player.setHits(14);
    player.setAverage(0.341);
    db.set(player);

    player = new PlayerImpl();
    player.setName("A.Rodriguez");
    player.setTeam("NYY");
    player.setPosition("3B");
    player.setHits(14);
    player.setAverage(0.35);
    db.set(player);

    player = new PlayerImpl();
    player.setName("B.Abreu");
    player.setTeam("NYY");
    player.setPosition("OF");
    player.setHits(12);
    player.setAverage(0.286);
    db.set(player);

    :
    :以下NYYの選手14人を登録
    :
    db.commit();
} finally {
    db.close();
}

db4oの検索結果の殆どはObjectSet等のコレクションが戻るので、標準出力に結果を表示するためのユーティリティメソッドを用意する。

public static void printResult(Collection collection, String title) {
    System.out.println(title);
    System.out.println("result count = " + collection.size());
    for( Object o : collection ){
        System.out.println(o);
    }
}
  • QBE

QBEは簡単に言うと、型のプロトタイプ(Exampleと訳される)をクエリに与えることで検索対象を絞り込む方法だ。例えば、上記IPlayerインタフェースにおけるnameフィールドが"D.Jeter"であるオブジェクトを検索するQBEのコードは以下のようにIPlayerインタフェースを実装するクラスのインスタンスを生成した後、setNameメソッドで条件の"D.Jeter"をセットした後にgetメソッドを使う。

IPlayer player = new PlayerImpl();
player.setName("D.Jeter");
printResult(db.get(player), "QBE for name = 'D.Jeter'");

クエリの実行結果

QBE for name = 'D.Jeter'
result count = 1
  PlayerImpl[ name=D.Jeter, team=NYY, position=SS, hits=14, average=0.318]

さしずめSQLならば

SELECT * FROM PLAYER WHERE NAME = 'D.Jeter'

となるだろうか。
もちろん、プロトタイプのフィールドがプリミティブ(double)でも同じだ。

IPlayer stats = new PlayerImpl();
stats.setAverage(0.2); // Averageが0.2の選手だけが対象になる
printResult(db.get(stats), "QBE for Average = 0.2"); 

クエリの実行結果

QBE for Average = 0.2
result count = 2
  PlayerImpl[ name=J.Phelps, team=NYY, position=1B, hits=2, average=0.2]
  PlayerImpl[ name=J.Giambi, team=NYY, position=DH, hits=8, average=0.2]

これは、SQL文風に書くと

SELECT * FROM PLAYER WHERE AVERAGE = 0.2

となるだろうか。
数値フィールドを扱うのであれば範囲で絞り込む、つまり「打率が0.2〜0.3の間」等というクエリを書きたくなるがQBEでは残念ながらできない。
今度はTeamが"NYY"であるオブジェクト(元データはNYYのロスタなので結果として全てのデータ)を取得するQBEのコードは以下のようになる。

IPlayer player = new PlayerImpl();
player.setTeam("NYY");
printResult(db.get(player), "QBE for team = 'NYY'");

クエリの実行結果

QBE for team = 'NYY'
result count = 14
  PlayerImpl[ name=J.Phelps, team=NYY, position=1B, hits=2, average=0.2]
  PlayerImpl[ name=M.Cairo, team=NYY, position=OF, hits=0, average=0.0]
  PlayerImpl[ name=W.Nieves, team=NYY, position=C, hits=0, average=0.0]
  PlayerImpl[ name=K.Thompson, team=NYY, position=OF, hits=1, average=0.5]
  PlayerImpl[ name=D.Jeter, team=NYY, position=SS, hits=14, average=0.318]
  PlayerImpl[ name=B.Abreu, team=NYY, position=OF, hits=12, average=0.286]
  PlayerImpl[ name=R.Cano, team=NYY, position=2B, hits=14, average=0.333]
  PlayerImpl[ name=J.Giambi, team=NYY, position=DH, hits=8, average=0.2]
  PlayerImpl[ name=A.Rodriguez, team=NYY, position=3B, hits=14, average=0.35]
  PlayerImpl[ name=M.Cabrera, team=NYY, position=OF, hits=7, average=0.179]
  PlayerImpl[ name=J.Posada, team=NYY, position=C, hits=13, average=0.342]
  PlayerImpl[ name=J.Damon, team=NYY, position=OF, hits=8, average=0.286]
  PlayerImpl[ name=D.Mientkiewicz, team=NYY, position=1B, hits=3, average=0.115]
  PlayerImpl[ name=H.Matsui, team=NYY, position=OF, hits=3, average=0.25]

上記の取り出したオブジェクトの並びを見ると、順不同に見えるがdb4oはデフォルトではインデクスは生成されないため、データが登録された順に取り出されるとは限らない。
getメソッドは以上のようにプロタイプとしての型のインスタンスを指定するメソッドの他に、型自体を指定することもできる。

printResult(db.get(PlayerImpl.class), "QBE for PlayerImpl.class");

クエリの実行結果

QBE for PlayerImpl.class
result count = 14
  PlayerImpl[ name=J.Phelps, team=NYY, position=1B, hits=2, average=0.2]
  : 省略

なお、プラットホーム(Java又は.NET)にネィティブなOODBなのだから当たり前だが、以下のように具象クラスではなくインタフェースで型を指定しても同じ結果が返ることを確認できる。

printResult(db.get(IPlayer.class), "QBE for IPlayer.class");

クエリの実行結果

QBE for IPlayer.class
result count = 14
  PlayerImpl[ name=J.Phelps, team=NYY, position=1B, hits=2, average=0.2]
  : 以降省略

こういうのを見ると(どこかではバイナリデータのシリアライズ/デシリアライズが行われているはずだが)本当にオブジェクトが格納されていることを実感できる。
このように非常にシンプルで理解し易いQBEだが万能ではない。以下チュートリアルドキュメントからの抜粋だがQBEはこれらの弱点を持つと書かれている。

  • db4oはテンプレートオブジェクトの全フィールドをリフレクションによって取得しなくてはならない

これはリフレクションに対するのパフォーマンスへの懸念だろうか。もしそうであればJavaに対しては当たっているが、.NET2.0に関してはリフレクションによるプロパティの取得は非常に速いため、クリティカルな部分でなければ大して気にする必要は無いだろうと思う。

  • より複雑なクエリを実行できない (AND、OR、NOT等)

これは自明だろう。Javaにしろ.NETにしろ通常のオブジェクトはクエリのために最適化されているわけでは無いのでフィールドやプロパティの値で条件を絞り込むことはできてもフィールドやプロパティの値が特定の範囲にあるものだけを抽出したり、論理演算を使うことはできない。

  • 0や""(空白)、NULL値はデフォルト値なので条件指定できない

プロトタイプをクエリに使うということは型の初期値を指定しても条件として役に立たないということだ。

  • オブジェクトの各フィールドがデフォルト値に初期化されなければならないので、デフォルト値を許容しないようにはできない

これは正直良く判らない。原文は"You need to be able to create objects without initialized fields. That means you can not initialize fields where they are declared. You can not enforce contracts that objects of a class are only allowed in a well-defined initialized state."だが、ようは「フィールドの明示的な初期化無しにオブジェクトを生成できる必要がある」ってことだろうか。

これもよくわからん。原文は"You need a constructor to create objects without initialized fields."だが、結局は一つ上と同じことを言っているのだろうか。それとも「引数パラメタ無しのデフォルトコンストラクタが必要だ」ということなのだろうか。


いずれにせよ、SQLで書くと

SELECT * FROM PLAYER WHERE TEAM = 'NYY' AND NAME = 'D.Jeter'

等の単純な値一致だけのクエリなのであればQBEは単純で判り易い方法だ。

QBEでは絞り込めない複雑な条件や範囲による絞込みでは、次回に説明する予定のNative Queryを使うしか無いが、謹製のDAOではやはり同じようにプロトタイプを用意してそれをSQLのパラメタにバインドしていた私は、シンプルなQBEが好きである。