db4o その6(SODA Query)

前回はQBEに引き続きNative Query(NQ)について言及したが、NQは評価時には一番低レベルなクエリであるSimple Object Data Access(以降SODA)に変換されて実行されるとある。今回はそのSODAについて言及する。
なお、今回も使用するサンプルデータと実体化されるクラス、ユーティリティコードは前回と同じであり、動作確認も同様にSun Java JDK 6を使用しているので.NET 2.0 C#にポーティングするのは簡単だろう。

・Object Data Access(SODA)

SODA自体は新しいものではなく、オープンソースで開発されているJavaC#で使える汎用オブジェクトグラフをトラバースしアクセスするクエリAPIである。
S.O.D.A. - Simple Object Database Access

db4oのSODAはquery(Query)メソッドが返すQueryインタフェースで行う操作として定義されている。

interface Query {
    Constraint constrain (Object constraint);
    Constraints constraints();
    Query descend (String fieldName);
    ObjectSet execute ();
    Query orderAscending ();
    Query orderDescending ();
    Query sortBy(QueryComparator comparator);
}

constrainメソッドのパラメタには検索対象の型(classオブジェクト)か制約としての値、又は評価論理式をプログラマが実装するEvaluationインタフェースを指定することで対応したConstraintインタフェースを返す。
Constraintインタフェースは制約を操作するインタフェースであり、制約を記述すると共に制約の結合とその論理演算を行うことができる。

interface Constraint {
    Constraint and (Constraint with);
    Constraint or (Constraint with);
    Constraint equal ();
    Constraint greater ();
    Constraint smaller ();
    Constraint identity ();
    Constraint like ();
    Constraint contains ();
    Constraint startsWith(boolean caseSensitive);
    Constraint endsWith(boolean caseSensitive);
    Constraint not ();
    Object getObject();
}

一番シンプルなのはQBE同様にconstrainメソッドにクエリ対象の型を指定する方法だ。SODAの場合クエリが実行されるのはQuery#executeメソッドが呼ばれたタイミングである。

Query query = db.query();
query.constrain(IPlayer.class);
printResult(query.execute(), "SODA for Type = IPlayer.class");

クエリの実行結果

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]
  :
  : 〜 省略

絞り込まれた各オブジェクトのフィールドに対しての制約を指定するには、executeメソッドが実行される前のQueryインタフェースのdescendメソッドによりメンバフィールドに降りる必要がある。
例えばIPlayer型のオブジェクトで、尚且つ選手名が"D.Jeter"のインスタンスを絞り込む場合はdescendメソッドで"name"フィルードを指定し、戻り値のConstraintインタフェースに比較対象の文字列を指定している。

Query query = db.query();
query.constrain(IPlayer.class);
query.descend("name").constrain("D.Jeter");
printResult(query.execute(), "SODA for Type = IPlayer.class & name = 'D.Jeter'");

クエリ結果は省略する。
descendメソッドを使う構文の特徴は公開されていないメンバフィールドに直接文字列でアクセスすることである。これにより型セイフではなくなるし、開発環境のサポートも得られなくなるが、クエリを動的に構成することも可能になる。
NQで記述した例でもあった「打率が.333以上の選手」というクエリだが、SODAでは以下のように記述する。

Query query = db.query();
query.constrain(IBattingStats.class);
Query queryAverage = query.descend("average");
queryAverage.constrain(new Double(0.333)).greater().equal();
printResult(query.execute(), "SODA for Type = IPlayer.class & average >= 0.333");

クエリの実行結果

SODA for Type = IPlayer.class & average >= 0.333
result count = 4
  PlayerImpl[ name=K.Thompson, team=NYY, position=OF, hits=1, average=0.5]
  PlayerImpl[ name=R.Cano, team=NYY, position=2B, hits=14, average=0.333]
  PlayerImpl[ name=A.Rodriguez, team=NYY, position=3B, hits=14, average=0.35]
  PlayerImpl[ name=J.Posada, team=NYY, position=C, hits=13, average=0.342]

この例ではdescendメソッドで戻ったQueryインタフェースに対して打率の制約を与えているが、フィールドの型はdoubleでありそのままではconstrainメソッドに渡せない。従ってJavaの場合はラッパオブジェクトを与えることでクエリを記述できる。また、greater()メソッドだけでは">="の条件にはならないので、更にequal()メソッドを連結している。
次はSODAでの制約の連結と論理演算の例を見てみよう。例として、「守備位置が"一塁手"又は"二塁手"又は"三塁手"の選手」というクエリをSODAで記述してみよう。

Query queryForInfielders = db.query();
queryForInfielders.constrain(IPlayer.class);
Query queryPosition = queryForInfielders.descend("position");
Constraint firstBaseMan = queryPosition.constrain("1B");
Constraint secondBaseMan = queryPosition.constrain("2B");
Constraint thirdBaseMan = queryPosition.constrain("3B");
firstBaseMan.or(secondBaseMan).or(thirdBaseMan);
printResult(queryForInfielders.execute(), "SODA for Type = IPlayer.class & position = '1B'or'2B'or'3B' ");

クエリ結果

SODA for Type = IPlayer.class & position = '1B'or'2B'or'3B'
result count = 4
  PlayerImpl[ name=J.Phelps, team=NYY, position=1B, hits=2, average=0.2]
  PlayerImpl[ name=R.Cano, team=NYY, position=2B, hits=14, average=0.333]
  PlayerImpl[ name=A.Rodriguez, team=NYY, position=3B, hits=14, average=0.35]
  PlayerImpl[ name=D.Mientkiewicz, team=NYY, position=1B, hits=3, average=0.115]

descendメソッドでフィールド"position"まだ降りているのは同じだが、その後"一塁手""二塁手""三塁手"という制約を別々に生成し、それをorメソッドで連結している。当然だがandメソッドを使えば制約をandで結合することも可能だ。

  • SODA Evaluation

前述したようにconstrainメソッドにはEvaluationインタフェースも渡すことが出来、その場合は現在選択対象のインスタンスをEvaluationインタフェースのメソッドで評価することでクエリ結果に含めるか否かを決定する。
Evaluationインタフェースは以下のように唯一つのメソッドを持ち、パラメタとして渡されてきたCandidateインタフェースを操作することによりオブジェクトの取得やクエリの絞込みを行う。

interface Evaluation extends java.io.Serializable {
    void evaluate(Candidate candidate);
}

上記の「打率が.333以上の選手」というクエリをEvaluationインタフェースを実装することで記述してみよう。

Query query = db.query();
query.constrain(IBattingStats.class);
Query queryAverage = query.descend("average");
queryAverage.constrain( new Evaluation(){
    @Override
    public void evaluate(Candidate candidate) {
        Double average =(Double)candidate.getObject();
        candidate.include(average.doubleValue() >= 0.333);
    }
});
printResult(query.execute(), "SODA for Type = IPlayer.class & average >= 0.333");

evaluateメソッドにおいてはCandidate#getObjectメソッドにより対象オブジェクトをデータベースから取り出し、Candidate#includeメソッドでクエリの結果セットにオブジェクトを追加する実装を行う。(結果は省略)

このようにSODAでEvaluationインタフェースを実装する方法は非常に柔軟なクエリが書けるが、残念ながらその柔軟さを得た代わりにそれなりの代償を支払わなくてはならないようだ。

Resources » Reference » Advanced db4o Techniques » SODA Evaluations » Drawbacks

  • オブジェクト取り出しのオーバヘッド

SODAは通常、クエリ対象のオブジェクトをインスタンス化せずに、直接DBから探すことが可能だが、Evaluationインタフェースを使うことで対象のオブジェクトをインスタンス化した(Candidate#getObjectメソッド)後に評価が行われるため、インスタンス化のオーバヘッドが避けられない。(これはQBEや最適化の行われなかったNQでも同様のことがいえる)

  • 公開メンバのみのアクセス

プライベートフィールドに直接アクセスして条件判定を行うことができない(一般的には公開メンバにしかアクセスできない)。

クライアントサーバモードにおけるクライアントがSODA Evaluationを実行するとサーバ->クライアント間でオブジェクトのシリアライズ/デシリアライズ処理が発生するのを避けられない。



どれも納得だが、最後に関してはどうしてJavaにおいてのみなのだろう。.NETプラットホームではシリアライズ/デシリアライズが不要な理由が知りたいものだ。(リモーティング時にバイナリシリアライザ(フォーマッタ)を使用してDBのレコードイメージをやりとりしているから、位しか理由が思いつかないが)

SODAは、QBEやNQに比べると可読性に劣るがオブジェクトに対する殆どのクエリはこれで書けるだろう。ただ、これがSQLにとって変わるものかと言われれば否と答える。SQLのポピュラリティは半端ではないし現状ではやはりSQLが一番だと思う。
だからといってdb4oのようなOODBSQLに対応しなくては使い物にならないなどとは思わない。適材適所であり、SQLが必要ならばその領域はRDBに任せれば良いのである。OODBにはもっと違う使い道がある。