db4o その8(登録、更新、削除)

クエリについては一通り言及したので、今回からはデータベースの更新(登録、変更、削除)について言及しようと思う。
RDB(リレーショナルデータベース)では表のデータを更新するためには専用のDML(Data Manipulation Language)を使い、クエリでSQLを発行したのと同様に更新の指示をサーバに対して行う。
db4oの場合は専用のDMLなどは使わず生成したオブジェクト、更新したオブジェクトをデータベースに対してセット(set)するだけである。db4oはセットの指示があると対象のオブジェクトがデータベース上に存在しなければ登録し、既に存在すれば更新(削除)する。非常にシンプルだ。

更新処理の流れとしては

    1. データベースをオープンする
    2. オブジェクトを生成する、又はクエリによりオブジェクトを取得する
    3. データベースにオブジェクトの登録又は更新、削除を指示する
    4. データベースをクローズする

という順になる。非常に簡単且つ明解である。

  • 登録

何も難しいことは無い。新たなオブジェクトを生成してデータベースにセットすることで登録は完了する。登録が可能なクラスは全てのPOJO(.NETならPONO)であり、特定のインタフェースを実装したり、アノテーションを記述することも、Serializableでマーキングされている必要も無い。むろんJ2EEのようにJavaBeans仕様である必要すらない。

ObjectServer os = Db4o.openServer("roster.db", 0);
ObjectContainer db = os.openClient();
try {
    IPlayer player = new PlayerImpl();
    player.setName("D.Jeter");
    player.setTeam("NYY");
    player.setPosition("SS");
    db.set(player);
} finally {
    db.close();
}

前回書いたとおり、db4oトランザクションは暗黙の場合セッション(データベースオープン〜クローズ)の単位で実行される。この例の場合、コミットはデータベースのクローズ時 (db.close())に行われるので次回のセッションではこのオブジェクトは既に存在していることになる。

  • 更新

一度データベースに登録されたオブジェクトの状態やデータを変更した後にObjectContainer#setを呼び出すことで変更が実施される。登録と違うのは予め更新対象のオブジェクトをデータベースから取得しておく必要があることだ。(でないと更新ではなく登録になってしまう)

ObjectServer os = Db4o.openServer("roster.db", 0);
ObjectContainer db = os.openClient();
try {
    //D,Jeterの守備位置を"3B"に変更する
    IPlayer player = new PlayerImpl();
    player.setName("D.Jeter");
    ObjectSet results = db.get(player);
    for ( IPlayer player : players ) {
        player.setPosition("3B");
        db.set(player);
    }
} finally {
    db.close();
}

サンプルではQBEを使用して登録されているオブジェクトを取得して、その後オブジェクトの守備位置(position)を"3B"に変更している。同様に対象のオブジェクトの2007年の打撃成績から打率を3割4分に変更する場合は以下のように書く。

ObjectServer os = Db4o.openServer("roster.db", 0);
ObjectContainer db = os.openClient();
try {
    //D.Jeterを選手名に持つオブジェクト全ての守備位置を"3B"に変更する
    IPlayer player = new PlayerImpl();
    player.setName("D.Jeter");
    ObjectSet results = db.get(player);
    for ( IPlayer player : players ) {
        player.getStats(2007).setAverage(0.340);
        db.set(player);
    }
} finally {
    db.close();
}
  • 削除

更新と同様にクエリで取得した結果セットに対してdeleteメソッドを実行してオブジェクトをデータベースから削除することができる。

ObjectServer os = Db4o.openServer("roster.db", 0);
ObjectContainer db = os.openClient();
try {
    //D.Jeterを選手名に持つオブジェクト全てを削除する
    IPlayer player = new PlayerImpl();
    player.setName("D.Jeter");
    ObjectSet results = db.get(player);
    for ( IPlayer player : players ) {
        db.delete(player);
    }
} finally {
    db.close();
}

ここで注意しなければならないことが二つある。一つは削除されたオブジェクトはメモリ上から取り除かれている訳ではないこと、もう一つは削除されたPlayerオブジェクトはデータベースからは取り除かれるが...

Query q = db.query();
q.constrain(IBattingStats.class);
ObjectSet stats = q.execute();
printResult(q.execute(), "IBattingStats just alive.");

IBattingStats just alive
result count = 1
BattingStatsImpl[ year=2007, games=10, atBat=44, runs=9, hits=14, homerun=0, runsBattedIn=3, onBasePercentage=0.4, slugging=0.341, average=0.34]

このように元々IPlayerのメンバとしてセットされた登録されていたIBattingStattsオブジェクトのインスタンスは消えずにデータベースに残っている。これはバグではなく、デフォルトの正しい振る舞いであるため(deleteの対象は"D.Jeter"を名前に持つPlayerであってBattingStatsではない)、知っておかないと親となるオブジェクトと参照の無いゴミがどんどんデータベースに溜まることになる。
なお、RDBのFOREGIN KEY制約のCASCADEのように参照されているオブジェクト、この場合であればPlayerが保持しているBattingStatsまで削除したい場合は、データベースをオープンする前に対象の型に対してのDELETE時の振舞いを変更することができる。

Db4o.configure().objectClass(PlayerImpl.class).cascadeOnDelete(true); //(※1)

しかしこのように指定した場合はPlayerImplクラスを削除した場合に所持している他のオブジェクト全てが削除の対象となるため、意図しないオブジェクトの削除が起きないように設定は慎重に行うべきだろう。例えばある選手が他のチームに移籍したことを想定し、二つの球団に渡って登録された選手オブジェクトの打撃成績が共有されていたとしよう。

    PlayerImpl player = new PlayerImpl();
    player.setName("D.Mientkiewicz");
    player.setTeam("KC");
    player.setPosition("1B");

    //2006年の打撃成績
    IBattingStats stats = new BattingStatsImpl();
    stats.setYear(2006);
    stats.setGames(91);
    stats.setAtBat(314);
    stats.setRuns(37);
    stats.setHits(89);
    stats.setHomerun(4);
    stats.setRunsBattedIn(43);
    stats.setOnBasePercentage(0.359);
    stats.setSlugging(0.411);
    stats.setAverage(0.283);
    player.getStats().add(stats);
    db.set(player);

    PlayerImpl player2 = new PlayerImpl();
    player2.setName("D.Mientkiewicz");
    player2.setTeam("NYY");
    player2.setPosition("1B");
    player2.getStats().add(stats);
    db.set(player2);

cascadeOnDeleteを指定してあると、この例の場合最初に登録されたPlayerを削除すると残すべき打撃成績も削除されてしまう。
このように事故が発生する可能性もあるので、削除の必要があったとしても自動ではなく明示的に削除すべきだろう。

※1 objectClassメソッドにおいては最初、該当インタフェースであるIPlasyer.classを指定したのだが保持しているIBattingStatsはカスケード削除の対象にならなかった。インタフェースでは駄目なようだ。