db4o その17(スキーマエボリューションと自動リファクタリング)

以前にOODBの弱点とdb4oというエントリでOODBの弱点について多少言及したが、実は私が最初にOODBのことを知った時はもっと別な点を懸念していた。それはOODBは他のアーキテクチャのデータベース(構造型、ネットワーク型又はリレーショナル型)に比べてデータ構造の変更に関しては弱いのでは無いか、ということだ。

データベースは格納するデータのメタデータ(構造、制約、操作等)を記述する必要がある。いわゆるこれがデータベーススキーマだが、このスキーマのお陰でデータベースのデータ構造はそれを操作するプログラミング言語とは一応分離されていることになる。
OODBの場合はプログラミング言語から生成したオブジェクトがそのままデータベースに格納されるというのが一番の特徴であり、データベーススキーマが不要になること、オブジェクト層とデータ層のマッピングが不要になるという二つの大きな長所を生み出しているのだが、同時にデータベースに格納されるデータが特定のプラットホーム又はプログラミング言語に依存してしまうという短所にもなっている。

この問題を少しでも軽減するために巷のOODBはいろいろな策を講じているが、一つはOODBではあるが他の形式のデータベース同様にスキーマを記述して使うこと(※1)であり、もう一つはSchema Evolution(スキーマ・エボリューション)という技術を実装することである。

スキーマ・エボリューションとは、データベースをオンライン状態にしたままそのメタデータ、つまりスキーマを変更することを可能とし、尚且つ既に格納されている旧スキーマに対応したデータを自動的に移行する技術のことを指す。(※2)

db4oはこの問題をどう解決しているのだろうか。実はdb4oは他のDBMSと同様のスキーマは持っていない(そういった意味では純粋なOODBであるといえる)。
db4oは自身に格納されているオブジェクトがロードされる際に型の変更を検知し、変更を自動的に吸収する機能を備えている。これは前述したスキーマ・エボリューションと同様の機能だが、db4oでは敢えて"自動リファクタリング"と呼んでいる。

今まで使用してきた"roster.db"に格納されているIPlayerインタフェースに選手の年齢(Age)を表すフィールドを追加したとしよう。(当然だが、対応してPlayerImplクラスも修正することとする)

interface IPlayer {
    String getName();
    void setName(String name);

    String getTeam();
    void setTeam(String team);

    String getPosition();
    void setPosition(String position);

    int getAge();
    void setAge(int age);

    List getStats();
    void set(List stats);
    IBattingStats getStats(int year);
}

インタフェース追加前に"roster.db"に格納されているオブジェクトは年齢フィールドを持たないのでアクセスできないはずだが、以下のコードを実行しても全く問題なく年齢フィールドにアクセスできる。

public static ObjectSet sodaForPlayer(ObjectContainer db, String playerName) {
    Query query = db.query();
    query.constrain(IPlayer.class);
    query.descend("name").constrain(playerName);
    return query.execute();
}
:
:
ObjectContainer oldDb = Db4o.openFile("roster.db");
String playerName = "D.Jeter"; 
ObjectSet resultSet = sodaForPlayer(olddb, playerName);
IPlayer jeter = (IPlayer)playerName.next();
System.out.println(playerName + " Age? = " + jeter.getAge());
:
:

実行結果

D.Jeter Age? = 0

型の変更前には無かったフィールドの"Age"へのアクセスだが、基本的にはその型のデフォルト値が取得される(この場合はintなので0)。 例えばAgeがString型のフィールドだと定義されている場合、実行結果は

D.Jeter Age? = null

となる。
今度は古いデータを取得した後に、追加されたフィールドに値をセットしてみよう。

ObjectContainer oldDb = Db4o.openFile("roster.db");
String playerName = "D.Jeter"; 
ObjectSet resultSet = sodaForPlayer(olddb, playerName);
IPlayer jeter = (IPlayer)playerName.next();
System.out.println(playerName +" Age? = " + jeter.getAge());
jeter.setAge(32);
System.out.println(playerName +" Age? = " + jeter.getAge());

実行結果

D.Jeter Age? = 0
D.Jeter Age? = 32

このように新たに追加されたフィールドに値をセットした場合は即座にオブジェクトに反映される。ただしこの場合はまだメモリ上のオブジェクトが書き換わったに過ぎないため、データベースに反映するためには従来通りデータベースに更新処理を実施する必要がある。(例外処理は省略する)

ObjectContainer oldDb = Db4o.openFile("roster.db");
String playerName = "D.Jeter"; 
ObjectSet resultSet = sodaForPlayer(olddb, playerName);
IPlayer jeter = (IPlayer)playerName.next();
System.out.println(playerName +" Age? = " + jeter.getAge());
jeter.setAge(32);
System.out.println(playerName +" Age? = " + jeter.getAge());
oldDb.set(jeter);
oldDb.commit();

これで選手名"D.Jeter"のオブジェクトは年齢(Age)フィールドが追加された状態でデータベースに再格納された訳だが、この時点では他の選手オブジェクトにはまだ年齢フィールドは追加されていないことに注意が必要である。
以下、年齢フィールドが追加されたデータがObjectManagerの一覧ビューで見た様子である。(一覧ビューにおいてageフィールドはカラムの末尾に追加されるが、見やすいようにカラムを移動している)

次は他の選手の詳細データ(一覧ビューの左端のアイコンをクリックすることで詳細ビューに移行できる)を見てみよう。

このデータは変更前のA.Rodriguezのオブジェクトだが、ageフィールドは追加されていないため、まだnullのままである。このようにD.Jeter以外は年齢(age)フィールドが空であることが判るだろう。

このようにdb4oは単に型にフィールドを追加するだけであれば"自動リファクタリング"の対象であり、データベースとアプリケーションの双方は何の変更も必要が無いのだ。

※1 ODMGにおけるC++スキーマ等が有名である
※2 スキーマ・エボリューションはOODBに特有な機能ではなく、スキーマを扱う現代の商用DBMSの殆どが装備している