このシリーズの前回の記事では、RDBMS に対するクエリーと、db4o のようなオブジェクト・データベースに対するクエリーとの違いについて説明しました。そこで説明したように、db4o では通常のリレーショナル・データベースよりも、はるかに多くの方法でクエリーを行うことができます。そのため、さまざまなアプリケーション・シナリオに対応するために広範な選択肢を利用することができます。
今回は、その同じテーマ、つまり db4o に見られる数多くのオプションの説明の続きとして、db4o がどのようにリファクタリングを処理するかを説明します。db4o はバージョン 6.1 の時点で、3 つの異なる種類のリファクタリング (つまりフィールドの追加、フィールドの削除、そしてクラスへの新しいインターフェース追加) を自動的に認識して処理します。ここでは、これら 3 つすべてについては説明せず、フィールドの追加とクラス名の変更に焦点を絞りますが、db4o によるリファクタリングで最もエキサイティングなことを紹介します。それは、db4o に導入された、データベースの変更管理の後方互換性と前方互換性です。
これから説明するように、db4o は自らの判断で更新を行うことができ、コードからディスクへの一貫性を保証できるため、システムのパーシスタンス部分をリファクタリングする際の負担を大きく軽減することができます。こうした柔軟性から、db4o はテスト駆動の開発プロセスに含めるべき適切な候補と言うことができます。
前回の記事では、db4o に対するクエリーを、ネイティブ・クエリーと QBE スタイルのクエリーの両方を使って行う方法について説明しました。その説明の中で、サンプル・コードを実行する際に、それ以前の実行で得られた結果を含む既存のデータベース・ファイルを削除するように助言しました。この助言は、OODBMS での ID の概念とリレーショナルの理論に見られる ID の概念は同じではないという事実から生ずる、「おかしな」結果を避けるためです。
この回避策は私の例には適切でしたが、現実の世界を考えると、この回避策から興味深い疑問が提起されます。OODBMS が保管しているオブジェクトを定義するコードが変更されると、OODBMS には何が起きるのでしょう。RDBMS では、「ストレージ」と「オブジェクト」の間の境界は非常に明確なはずです。つまり RDBMS は、データベースを操作する前のある時点で実行される DDL 文で定義されるリレーショナル・スキーマに従います。そうすると、手動で作成された JDBC 処理コードを使って Java コードがクエリー結果を Java オブジェクトにマップするか、あるいは Hibernate のようなライブラリーや新しい JPA (Java Persistence API) によってマッピングが「自動的に」行われます。いずれにせよ、マッピングは明示的であり、リファクタリングが行われるたびにマッピングを変更する必要があります。
理論的には、理論と現実に差はありません。しかしそれは、理論上そうであるにすぎません。リレーショナル・データベースとオブジェクト/リレーショナルのマッピングのリファクタリングは単純なはずです。しかし実際には、RDBMS のリファクタリングが明確なのは、そのリファクタリングが純粋にJava コード・レベルの問題である場合のみです。この場合には、単純にマッピングを変更しさえすれば、リファクタリングを完了することができます。しかし、もしその変更が、リレーショナル・ストレージ・データ自体に対する変更の場合には、突然まったく新しい、複雑な世界に入り込むことになり、その話題だけで 1 冊の本が書けてしまうほどです。(私の同僚の 1 人はそうした本のことを、「データベース・テーブルとトリガー、そしてビューのための 500 ページ」と評したことがあります。) 現実の RDBMS は保持する必要があるデータを含んでいることが非常に多いため、単にスキーマを削除して、DDL 文からスキーマを再構築することは、選択肢になり得ません。
これで、RDBMS のオブジェクトを定義する Java コードが変更された場合に RDBMS に何が起きるのかがわかりました。(あるいは少なくとも、RDBMS マネージャーに何が起こるのかはわかります。これは大きな頭痛の種です。) では、コードが変更された場合に db4o データベースに何が起きるのかを見てみましょう。
このシリーズの、これまでの 2 回の記事を読んだ方であれば、私が使用してきた非常に初歩的なデータベースがおなじみのはずです。このデータベースは現在、Person 型という 1 つの型で構成されています。Person 型の定義をリスト 1 に示します。
リスト 1. Person
package com.tedneward.model;
public class Person
{
public Person()
{ }
public Person(String firstName, String lastName, int age)
{
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}
public String getFirstName() { return firstName; }
public void setFirstName(String value) { firstName = value; }
public String getLastName() { return lastName; }
public void setLastName(String value) { lastName = value; }
public int getAge() { return age; }
public void setAge(int value) { age = value; }
public String toString()
{
return
"[Person: " +
"firstName = " + firstName + " " +
"lastName = " + lastName + " " +
"age = " + age +
"]";
}
public boolean equals(Object rhs)
{
if (rhs == this)
return true;
if (!(rhs instanceof Person))
return false;
Person other = (Person)rhs;
return (this.firstName.equals(other.firstName) &&
this.lastName.equals(other.lastName) &&
this.age == other.age);
}
private String firstName;
private String lastName;
private int age;
}
|
次に、このデータベースにデータを追加します (リスト 2)。
リスト 2. 「t0」時点のデータベース
import java.io.*;
import java.lang.reflect.*;
import com.db4o.*;
import com.tedneward.model.*;
// Version 1
public class BuildV1
{
public static void main(String[] args)
throws Exception
{
new File(".", "persons.data").delete();
ObjectContainer db = null;
try
{
db = Db4o.openFile("persons.data");
Person brianG = new Person("Brian", "Goetz", 39);
Person jason = new Person("Jason", "Hunter", 35);
Person brianS = new Person("Brian", "Sletten", 38);
Person david = new Person("David", "Geary", 55);
Person glenn = new Person("Glenn", "Vanderberg", 40);
Person neal = new Person("Neal", "Ford", 39);
Person clinton = new Person("Clinton", "Begin", 19);
db.set(brianG);
db.set(jason);
db.set(brianS);
db.set(david);
db.set(glenn);
db.set(neal);
db.set(clinton);
db.commit();
// Find all the Brians
ObjectSet brians = db.get(new Person("Brian", null, 0));
while (brians.hasNext())
System.out.println(brians.next());
}
finally
{
if (db != null)
db.close();
}
}
}
|
リスト 2 のコード・スニペットの最初で「persons.data」というファイルを明示的に削除していることに注意してください。こうすることによって、確実にクリーンな状態で開始することができます。今後のバージョンの Build アプリケーションでは、リファクタリングのプロセスがわかるように persons.data ファイルには手を加えないつもりです。また、Person 型は変更されることにも注意してください (これが私のリファクタリングの焦点になります)。そのため、それぞれの例では、どのバージョンが保管され、取得されるのかをよく理解してください。(この記事のソース・コードの中にある各バージョンの Person に対するコメントと、ソース・コード・ツリーの中にある Person.java.svn ファイルを見ると、例を理解しやすくなるはずです。)
これまで、この古い会社ではすべてが順調でした。この会社のデータベースは Person で一杯です。必要な場合には、いつでも誰でも Person を照会し、保管し、使用することができ、それで基本的には誰もがハッピーです。しかし会社の幹部は、上級管理に関する最新のベストセラー、『人には感情もある』という本を読みました。そのため彼らは、Person の mood (気分) も含むようにデータベースを変更する必要がある、という決定を下しました。
従来のオブジェクト/リレーショナルのシナリオでは、これは 2 つの大きな作業を意味します。つまりコードをリファクタリングすること (これについては下記に説明します) と、Person の気分を反映する新しいデータを含むようにデータベース・スキーマをリファクタリングすることです。ところで、Scott Ambler が、RDBMS のリファクタリングのための素晴らしいリソースを作成しています (「参考文献」を参照)。しかし、リレーショナル・データベースのリファクタリングが (特に既存の実動データを保存しなければならない場合には) Java コードのリファクタリングよりもずっと複雑である、という事実は変わりません。
しかし OODBMS では、ずっと簡単です。これは、リファクタリングが完全にコードの中で (この場合は Java コードの中で) 行われるためです。OODBMS では、コードがスキーマであることを忘れないことが重要です。そのため OODBMS は、いわば「真実の唯一の源」を提供します。これはオブジェクト・リレーショナルの世界での、(いわゆる) 真実がデータベース・スキーマとオブジェクト・モデルという 2 つの異なる場所にエンコードされることと対照的です。(競合が起きた場合にどちらが「勝つ」かは、Java 開発者の間で大きな議論があり、苦悩の種でもあります。)
最初のステップは、追跡するすべての mood を定義する、新しい型を作成することです。これは Java 5 の列挙型を使えば簡単です (リスト 3)。
リスト 3. ご機嫌いかがですか
package com.tedneward.model;
public enum Mood
{
HAPPY, CONTENT, BLAH, CRANKY, DEPRESSED, PSYCHOTIC, WRITING_AN_ARTICLE
}
|
第 2 に、mood を追跡するためのフィールドと適当なプロパティー・メソッドを追加することで、Person コードを変更する必要があります (リスト 4)。
リスト 4. いや、あなたのご機嫌はいかがですか
package com.tedneward.model;
// Person v2
public class Person
{
// ... as before, with appropriate modifications to public constructor and
// toString() method
public Mood getMood() { return mood; }
public void setMood(Mood value) { mood = value; }
private Mood mood;
}
|
他のことをする前に、現在データベースの中にあるすべての Brian を検索するクエリーに対して、db4o がどのように応答するかを見てみましょう。つまり、既存の Person ベースのクエリーを、Mood インスタンスが保存されていない状態のデータベースに対して実行すると、db4o はどのように反応するのでしょう (リスト 5)。
リスト 5. 皆さんのご機嫌はいかがですか
import com.db4o.*;
import com.tedneward.model.*;
// Version 2
public class ReadV2
{
public static void main(String[] args)
throws Exception
{
// Note the absence of the File.delete() call
ObjectContainer db = null;
try
{
db = Db4o.openFile("persons.data");
// Find all the Brians
ObjectSet brians = db.get(new Person("Brian", null, 0, null));
while (brians.hasNext())
System.out.println(brians.next());
}
finally
{
if (db != null)
db.close();
}
}
}
|
その結果は、至って従順で少し驚きます (リスト 6)。
リスト 6. db4o はうまく処理します
[Person: firstName = Brian lastName = Sletten age = 38 mood = null]
[Person: firstName = Brian lastName = Goetz age = 39 mood = null]
|
db4o は、Person の 2 つの定義 (1 つはディスク上、1 つはコードの中) が同じではないという事実のために停止してしまうことがないばかりか、さらに 1 歩進んでディスク上のデータを調べ、そこにある Person インスタンスには mood フィールドがないことを判断し、そして自らの判断でデフォルト値である null で置き換えています。(ところで、同じ状況では Java Object Serialization API も正にこれを行います。)
ここで最も重要なことは、ディスク上に見えるものと型定義の中にあるものとの不一致を db4o が自らの判断で処理したという点です。実は db4o がリファクタリングを行う場合には、一貫してこのように動作することがわかります。つまり db4o は、可能な限り、バージョンの不一致を静かに処理するのです。db4o は、追加されたフィールドを含むためにディスク上の要素を拡張するか、あるいは、もしそのフィールドが、与えられた JVM 内の対象クラス定義の中に存在しなければ、そうしたフィールドを無視します。
この概念、つまりディスク上の足りないフィールド、あるいは余分なフィールドを db4o が必要に応じて調節するという概念は、大いに利用する価値があります。そこで、ディスク上のデータを、mood を含むように更新したらどうなるかを見てみましょう (リスト 7)。
リスト 7. 私達は元気です
import com.db4o.*;
import com.tedneward.model.*;
// Version 2
public class BuildV2
{
public static void main(String[] args)
throws Exception
{
ObjectContainer db = null;
try
{
db = Db4o.openFile("persons.data");
// Find all the Persons, and give them moods
ObjectSet people = db.get(Person.class);
while (people.hasNext())
{
Person person = (Person)people.next();
System.out.print("Setting " + person.getFirstName() + "'s mood ");
int moodVal = (int)(Math.random() * Mood.values().length);
person.setMood(Mood.values()[moodVal]);
System.out.println("to " + person.getMood());
db.set(person);
}
db.commit();
}
finally
{
if (db != null)
db.close();
}
}
}
|
リスト 7 では、データベースの中にあるすべての Person を発見し、それらに対してランダムに Mood を割り当てています。現実のアプリケーションでは、ランダムに選んだデータではなく基準となるデータ・セットを使いたいものですが、例としてはこれで十分です。このコードを実行すると、リスト 8 に示す結果が得られます。
リスト 8. 皆さんの今日のご機嫌はいかがですか
Setting Brian's mood to BLAH
Setting David's mood to WRITING_AN_ARTICLE
Setting Brian's mood to CONTENT
Setting Jason's mood to PSYCHOTIC
Setting Glenn's mood to BLAH
Setting Neal's mood to HAPPY
Setting Clinton's mood to DEPRESSED
|
この出力は、再度 ReadV2 を実行することで検証することができます。もっと良い方法として、オリジナルのクエリー・バージョン、ReadV1 を実行することができます (ReadV1 は ReadV2 と非常に似ていますが、V1 バージョンの Person に対してコンパイルされた点が異なります。) ReadV1 を実行すると、次の結果が得られます。
リスト 9. 古いバージョンの「皆さんの今日のご機嫌はいかがですか」
[Person: firstName = Brian lastName = Sletten age = 38]
[Person: firstName = Brian lastName = Goetz age = 39]
|
リスト 9 の出力で驚くべきことは、この出力が、Person クラスに Moodエクステンションを追加する前に db4o が出力したもの (リスト 6) と違いがないことです。これはつまり、db4o が後方互換でもあり、前方互換でもあることを意味しています。
ここで、既存のクラスのフィールドの型を変更したいとしましょう。例えば Person の年齢を整数 (integer) 型から短整数 (short) 型に変更してみます。(所詮、人が 32,000 年以上も生きることはありません。このような変更を提案して、もしこの変更が実際に問題になるとしても、コードをリファクタリングして整数フィールドに戻すことができるので心配いりません。) db4o は、2 つの型の性質が似ている (例えば整数型と短整数型、あるいは浮動小数点型と倍精度型など) と考え、自らの判断で変更を進めます。この場合も、程度の差はあれ Java Object Serialization API を真似ています。こうした操作の欠点は、db4o が誤って値が切り捨てられてしまう可能性があることです。唯一、この問題が起こるのは、「値が制限される変換」の場合、つまり値が、新しい型で許容される値の範囲を超えてしまう場合、例えば long 型を int 型に変更しようとする場合などです。これは買手責任 (Caveat emptor) です。開発中、あるいはプロトタイプ化作業中には、必ず十分なユニット・テストを行う必要があります。
実は、後方互換性に関して db4o が行うテクニックについては、もう少し説明する価値があります。db4o は基本的に、新しい型のフィールドを見つけると、同じ名前で新しい型を持つ新しいフィールドを (あたかもそのクラスに追加された、他の任意の新しいフィールドであるかのように) ディスク上に作成します。これはつまり、古い型のフィールドに古い値が相変わらず存在しているということを意味します。そのため、この場合も、オリジナルの値に対するフィールドをリファクタリングすることで、いつでも古い値を「コール・バック」することができます。これは、その時点での皆さんの視点次第で、機能と見なすこともでき、あるいはバグと見なすこともできます。
クラスに対するメソッドの変更は db4o に無関係なことに注意してください。db4o は、保管されるオブジェクト・データの一部としてメソッドあるいはメソッド実装を保管することはありません。コンストラクターをリファクタリングする場合も同じです。db4o にとって重要なのは、フィールドと、クラスの名前そのもの (これを次に説明します) のみです。
場合によると、リファクタリングによってもう少し抜本的なことが行われることがあります。例えばクラスの名前を完全に変更する (つまり、クラス名、あるいはそのクラスを含むパッケージのいずれかを変更する) ような場合です。このような変更は、db4o にとっては劇的な変更です。なぜなら、db4o は classname をキーとしてオブジェクトを保管するからです。例えば db4o が Person のインスタンスを探す際には、com.tedneward.model.Person という名前でタグ付けされたブロックの特定領域を探します。そのため、名前が変更されると、実際上 db4o は非常に困難な状況に陥ります。db4o は、com.tedneward.model.Person が今や com.tedneward.persons.model.Individual であることを魔法のように推論することはできません。幸いなことに、こうした変更に対応する手段を db4o に伝える方法がいくつかあります。
db4o がそうした劇的な変更に陥らないようにするための 1 つの方法は、独自のリファクタリング・ツールを作成することです。そのためには db4o の Refactoring API を使って既存のデータ・ファイルを開き、ディスク上の名前を変更します。これは、非常に単純な一連のコールを使って行うことができます (リスト 10)。
リスト 10. Person から Individual にリファクタリングする
import com.db4o.*;
import com.db4o.config.*;
// ...
Db4o.configure().objectClass("com.tedneward.model.Person")
.rename("com.tedneward.persons.model.Individual");
|
リスト 10 のコードが db4o の Configuration API を使って設定オブジェクトを取得していることに注意してください (そして設定オブジェクトは、db4o の大部分のオプションに対する一種の「メタ・コントロール」として使われます)。実行時に特定の設定を行うためには、コマンド・ライン・フラグや設定ファイルではなく、この API を使います。(ただし、Configuration API をコールするために独自のコマンド・ライン・フラグや設定ファイルを作成してはいけないわけではありません。) そして Configuration オブジェクトは、Person クラスに対する ObjectClassインスタンスを取得するために使われます。あるいはもっと正確に言うと、ディスク上に保管されている Person インスタンスを表現する ObjectClass インスタンスを取得するために使われます。ObjectClass は、他にもいくつかのオプションを含んでいます。このシリーズの今後の記事では、それらをいくつか説明する予定です。
場合によると、何らかの理由 (技術的理由にせよ政治的理由にせよ) で再コンパイルできない初期のアプリケーションをサポートするために、ディスク上のデータをそのままにしておく必要がある場合があります。こうした場合、V2 アプリケーションは、何らかの方法でメモリー内に V1 インスタンスを取り込み、それを V2 インスタンスに変換する必要があります。幸い、db4o のエイリアス機能を利用すると、ディスクとの間でオブジェクトの保管と取得を行う一方で、シャッフルを行うステップを作成することができます。こうすることで、保管されている型とメモリー内で使われる型とを異なるものにすることができます。
db4o は、3 種類の異なるエイリアスをサポートしています。そのうちの 1 つは、.NET での db4o と Java での db4o の間でデータ・ファイルを共有する場合以外はあまり便利ではありません。リスト 11 で使用しているエイリアスは TypeAlias です。これは実質的に db4o に対して、メモリー内の「A」型 (ランタイム名) と、ディスク上の「B」型 (保管名) とを交換するように命令します。これは、2 行の操作で実現することができます。
リスト 11. TypeAlias によるシャッフル
import com.db4o.config.*;
// ...
TypeAlias fromPersonToIndividual =
new TypeAlias("com.tedneward.model.Person", "com.tedneward.persons.model.Individual");
Db4o.configure().addAlias(fromPersonToIndividual);
|
これを実行すると、db4o は、データベースから Individual オブジェクトを照会するすべての呼び出しを、保管された Person インスタンス全体を見るためのリクエストとして認識するようになります。これはつまり、Individual クラスは、Person に保管されているフィールドと似た名前と型のフィールドを持っている必要があるということです (db4o はこれらを適切にマップします)。そして Individual インスタンスは Person 名の下に保管されます。
この記事で紹介したリファクタリングの例は、どれも非常に単純です。これは、オブジェクト・データベースのスキーマが、異なる言語でのスタンドアロンの DDL 定義ではなく、クラス定義そのものであるという事実によるものです。db4o でのリファクタリングはコード操作であり、これは多くの場合、設定を呼び出すことによって、あるいは最悪の場合でも、(既存のインスタンスを古い型から新しい型に更新する) 変換ユーティリティーを作成して実行することによって、容易に行うことができます。こうした種類の変換は、実動のほとんどすべての RDBMS のリファクタリングに必要です。
db4o の持つ強力なリファクタリング機能は、開発の際に便利です。開発の際には設計中のリッチなドメイン・オブジェクトが何度も変更され、リファクタリングを (1 時間毎ではないにせよ) 毎日行う必要があるからです。ユニット・テストやテスト駆動開発に db4o を使うことで、データベースをいじり回す時間を大幅に節約することができ、特にリファクタリングが単純にフィールドの追加や削除をする場合、または名前や型の変更である場合には効果的です。
今回はこれで終わりますが、次のことを忘れないでください。もし皆さんがオブジェクトを作成しようとする場合、そしてパーシスタンスが本当に「単なる実装の問題」であるなら、なぜ必要もないのに完璧に適切なオブジェクトを同じような形にし、均一な枠の中に押し込もうとするのでしょうか。
| 内容 | ファイル名 | サイズ | ダウンロード形式 |
|---|---|---|---|
| Sample code | j-db4o3-source.zip | 28KB | HTTP |
学ぶために
-
「多忙な Java 開発者のための db4o ガイド: 紹介と概要」(Ted Neward 著、developerWorks、2007年3月) は db4o を紹介し、なぜ db4o が今日のリレーショナル・データベースを置き換える重要なものになったのかを説明しています。
-
「多忙な Java 開発者のための db4o ガイド: クエリー、更新、そして ID」(Ted Neward 著、developerWorks、2007年3月) は、データを発見し、取得するための db4o のさまざまな機構を解説しています。
-
『Refactoring Databases: Evolutionary Database Design』(Scott Ambler と Pramod J. Sadalage の共著、Addison-Wesley 社 の Signature シリーズの 1 冊、2006年) は、データベースのリファクタリングを 500 ページに渡って解説した大著です。
-
「Book review -- Refactoring Databases: Evolutionary Database Design」(Eric Naiburg 著、developerWorks、2006年9月) は Rational Edge で好意的な評価を受けています。
-
db4o のホームページで db4o について学んでください。
-
New to IBM Information Managementをご覧ください。まだ OODBMS 用には販売されていないかもしれませんが、IBM の強力な RDBMS (relational database management system) サーバー・ファミリーに関する情報を入手することができます。
-
ODBMS.orgには、オブジェクト・データベース技術に関する無料資料が豊富に用意されています。
-
developerWorks の Java technology ゾーンには、Java プログラミングのあらゆる側面を網羅した記事が豊富に用意されています。
製品や技術を入手するために
-
ネイティブ Java プログラミングと .NET のためのオープンソースのデータベース、db4o をダウンロードしてください。
議論するために
-
developerWorks
blogs から developerWorks のコミュニティーに加わってください。