db4o その7(オブジェクト階層へのクエリ)

現在までにdb4oのクエリにおける3つの手法について言及してきた。

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

これらクエリの例として今まではフラットな構造のオブジェクトに限定してきたが、いつもそのようなオブジェクトばかりとは限らない。今回はdb4oにおいて階層を持つオブジェクトを対象にクエリを実行したい場合はどのような記述を行うのか言及する。

今まで書いてきた内容、そしてこれから書く内容はどれもこれもdb4oのリファレンスドキュメントから手に入る情報ばかりであることをお断りしておく。私は覚えが悪いのでわざわざこのようにチュートリアルとは違う型を起こしていろいろと試しているが、普通はdb4oのサイトに上がっているチュートリアルかリファレンスを読めば充分だろうと思う。

サンプルに関しては今までと同様にSun Java6 SDKを用いて行うが、オブジェクトの階層に明示的にアクセスするために今まで使用していたインタフェースから打撃成績(BattingStats)に関するデータを分離し、更に詳細なデータを追加して年度毎に管理するように設計を変更することにした。

  • IBattingStatsインタフェース

選手の打撃成績を操作するためのインタフェース。成績は年毎に管理するために年(Year)フィールドを追加した。

interface IBattingStats {
    int getYear();
    void setYear(int year);
    int getGames(); //試合数
    void setGames(int games);
    int getAtBat(); //打数
    void setAtBat(int atBat);
    int getRuns(); //出塁
    void setRuns(int runs);
    int getHits(); //安打
    void setHits(int hits);
    int getRunsBattedIn(); //打点
    void setRunsBattedIn(int runsBattedIn);
    int getHomerun(); //本塁打
    void setHomerun(int homerun);
    double getAverage(); //打率
    void setAverage(double average);
    double getOnBasePercentage(); //出塁率
    void setOnBasePercentage(double onBasePercentage);
    double getSlugging(); //強打率
    void setSlugging(double slugging);
}
  • IPlayerインタフェース

打撃成績に関するデータはIBattingStatsに移動したため、こちらは選手自身のデータに限定することにした。なお、打撃成績は年度毎に管理するためにリストで表すこととし、通常のアクセサ以外に年度毎に打撃成績を取得するためのメソッドを追加した。

interface IPlayer {
    String getName(); //選手名
    void setName(String name);
    String getTeam(); //チーム名
    void setTeam(String team);
    String getPosition(); //守備位置
    void setPosition(String position);
    List getStats(); //打撃成績
    void set(List stats);
    IBattingStats getStats(int year); //任意の年の打撃成績
}

両インタフェースともJava(Java5以降)を念頭に書いているが、アクセサをプロパティに置き換えることで簡単に.NET C#(.NET2.0)にポーティングできるのを確認している。なお、実際にデータベースに登録されるインスタンスは単純にこれらのインタフェースを実装したBattingStatsImplとPlayerImplクラスとする(単純実装なので内容は省略)
実際のインスタンスの生成はMLBのサイトから引っ張ってきたデータをCSVに落としてそこから読みこんでいるが、コードで行うとすれば以下のようになることだろう。

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");
    IBattingStats stats = new BattingStatsImpl();
    stats.setYear(2007);
    stats.setGames(10);
    stats.setAtBat(44);
    stats.setRuns(9);
    stats.setHits(14);
    stats.setHomerun(0);
    stats.setRunsBattedIn(3);
    stats.setOnBasePercentage(0.4);
    stats.setSlugging(0.341);
    stats.setAverage(0.318);
    player.getStats().add(stats);

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

今まで触れていなかったが、db4oの場合生成したオブジェクトをObjectContainer#setメソッドでデータベースに登録することになる。(実はこのとき暗黙的なトランザクションが開始されており、セッション(※)のクローズ時にsetメソッドで登録されたデータがコミットされる)

これでオブジェクトグラフに打撃成績(stats)で表される階層が追加されたことになるが、この階層にアクセスするためのクエリをdb4oで可能なそれぞれのクエリで試してみよう。

  • QBE

QBEの場合、階層にアクセスする場合であってもプロトタイプを生成してクエリに組み込むのは同じだ。例えば打撃成績のうち、2007年の打率が.333の選手だけを絞り込む(QBEは範囲値を表現できない)クエリは以下のように書く

IPlayer player = new PlayerImpl();
IBattingStats stats = new BattingStatsImpl();
stats.setYear(2007);
stats.setAverage(0.333);
player.getStats().add(stats);
printResult(db.get(player), "QBE for Average = .333");

クエリ実行結果

QBE for Average = .333
result count = 1
  PlayerImpl[ name=R.Cano, team=NYY, position=2B, stats{BattingStatsImpl[ year=2007, games=10, atBat=42, runs=6, hits=14, homerun=0, runsBattedIn=4, onBasePercentage=0.391, slugging=0.381, average=0.333], }]
  • NQ

NQの場合は簡単だ。普通にメンバ変数にアクセスして条件を記述するだけ。同様に打率が.333以上の選手だけを絞り込むクエリをNQで書いてみよう。

List rosters = db.query(new Predicate(){
    @Override
    public boolean match(IPlayer candidate) {
        return ( candidate.getStats(2007).getAverage() >= 0.333 );
    }
});
printResult(rosters, "NQ for Average. >= .333");

結果は省略する。

  • SODA

最後はSODAだ。SODAの場合は階層を走査する必要があるので記述は最も冗長になる。条件はNQと同じとして書いてみよう。

Query query = db.query();
query.constrain(IPlayer.class);
Query queryStats = query.descend("stats");// IPlayer#stats
queryStats.constrain(IBattingStats.class);
Query averageQuery = queryStats.descend("average"); // IPlayer#stats#average
averageQuery.constrain(0.333).greater().equal(); // average >= .333

結果は省略する。このサンプルのようにdescendメソッドでメンバフィールド"stats"にアクセスしてそこから更にもう一つIBattingStatsのインスタンスの"average"フィールドに下りて制約を追加している。SODAの場合、NQで書いたようにわざわざコレクションから任意の年の成績を抽出していないがこれはコレクションはSODAにより内部で走査されるためである。SODAは階層が深くなる度に直感的ではなくどんなクエリを書いているかが判り難くなる。

同様にSODA Evaluationの例も書いておこう。

Query query = db.query();
query.constrain(IPlayer.class);
Query queryStats = query.descend("stats");
queryStats.constrain(IBattingStats.class);
queryStats.constrain( new Evaluation(){
    @Override
    public void evaluate(Candidate candidate) {
        IBattingStats stats =(IBattingStats)candidate.getObject();
        candidate.include(stats.getAverage() >= 0.333);
    }
});
printResult(query.execute(), "SODA for Average >= 0.333");

NQと変わらないが、その前のクエリでどこまでdescendしているかでevaluateメソッドに入ってくるオブジェクトが変わるので注意が必要だ。

このようにそれぞれのクエリでオブジェクトの階層を降りるクエリを書くことが出来る。表というフラットな抽象を使うRDB程ではないにしろ、どのクエリでも階層が深くなるにつれて記述が複雑になるのは避けられない。当たり前だがシンプルなアクセスをしたいのであればオブジェクトの設計もシンプルにすべきだろう。

db4oはデータベースのオープン〜クローズまでを"セッション"と呼び、このセッションの間が暗黙なトランザクションの単位となっている。