この「多忙な Java 開発者のための db4o ガイド」シリーズの前回までは、db4o を使ってマッピング・ファイルに頼らずに Java オブジェクトを保管するためのさまざまな方法について説明してきました。オブジェクト・リレーショナル・マッピングを使わずに済むことは、ネイティブ・オブジェクト・データベースを使う上での (核心ではないかもしれませんが) 利点の 1 つです。しかし、これを示すために私が使用したモデルは、非現実的なほど単純なものでした。大部分のエンタープライズ・システムでは、非常に複雑なオブジェクト (構造化オブジェクトとしても知られています) を作成したり操作したりする必要があります。そこでこの記事では少し方向を変え、構造化オブジェクトの作成について説明します。
構造化オブジェクトというのは、要するに他のオブジェクトに対する参照を持つオブジェクトのことです。db4o では、構造化オブジェクトに対して通常のすべての CRUD 操作を行えますが、そのために少し複雑になることは避けられません。この記事では、そのように複雑になる主な原因のいくつか (無限再帰やカスケーディング動作、参照整合性など) を探り、次回の記事で、構造化オブジェクトの高度な処理方法について解説します。さらに、クラス・ライブラリーと db4o の API の両方をテストできる、あまり知られていないテスト手法、エクスプローラー・テストを紹介します。
リスト 1 は、これまで db4o を紹介する中で使用してきた単純な Person クラスを改めて要約したものです。
リスト 1. Person
package com.tedneward.model;
public class Person
{
public Person()
{ }
public Person(String firstName, String lastName, int age, Mood mood)
{
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
this.mood = mood;
}
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 Mood getMood() { return mood; }
public void setMood(Mood value) { mood = value; }
public String toString()
{
return
"[Person: " +
"firstName = " + firstName + " " +
"lastName = " + lastName + " " +
"age = " + age + " " +
"mood = " + mood +
"]";
}
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;
private Mood mood;
}
|
この単純な Person クラスは、db4o での基本的な保管やクエリー、取得などの操作を紹介するための例としては適切でしたが、実際のエンタープライズ・プログラミングの複雑さには対応できません。例えば、データベースの中の Person が家の住所を含むことは珍しくないでしょう。そのうちの何人かは、配偶者や、場合によっては子供がいるかもしれません。
ここで、データベースに「Spouse (配偶者)」用のフィールドを追加したいと思います。これはつまり、Spouse オブジェクトを参照するように Person を拡張するということです。何らかのビジネス・ルールに対応することを考えると、適切に変更された Gender 列挙型と、equals() メソッドもコンストラクターに追加する必要があります。リスト 2 の Person 型には、spouse フィールドと、適切な get/set メソッドのペアが、いくつかのビジネス・ルールが付加された形で存在しています。
リスト 2. この人は結婚できる人なのか
package com.tedneward.model;
public class Person {
// . . .
public Person getSpouse() { return spouse; }
public void setSpouse(Person value) {
// A few business rules
if (spouse != null)
throw new IllegalArgumentException("Already married!");
if (value.getSpouse() != null && value.getSpouse() != this)
throw new IllegalArgumentException("Already married!");
spouse = value;
// Highly sexist business rule
if (gender == Gender.FEMALE)
this.setLastName(value.getLastName());
// Make marriage reflexive, if it's not already set that way
if (value.getSpouse() != this)
value.setSpouse(this);
}
private Person spouse;
}
|
リスト 3 は、結婚可能な 2 人の Person を作成するコードを示しています。これは、ほとんど皆さんが想像する通りのものです。
リスト 3. 教会に行き、結婚します
import java.util.*;
import com.db4o.*;
import com.db4o.query.*;
import com.tedneward.model.*;
public class App
{
public static void main(String[] args)
throws Exception
{
ObjectContainer db = null;
try
{
db = Db4o.openFile("persons.data");
Person ben = new Person("Ben", "Galbraith",
Gender.MALE, 29, Mood.HAPPY);
Person jess = new Person("Jessica", "Smith",
Gender.FEMALE, 29, Mood.HAPPY);
ben.setSpouse(jess);
System.out.println(ben);
System.out.println(jess);
db.set(ben);
db.commit();
List<Person> maleGalbraiths =
db.query(new Predicate<Person>() {
public boolean match(Person candidate) {
return candidate.getLastName().equals("Galbraith") &&
candidate.getGender().equals(Gender.MALE);
}
});
for (Person p : maleGalbraiths)
{
System.out.println("Found " + p);
}
}
finally
{
if (db != null)
db.close();
}
}
}
|
ここでは、好ましくなさそうなビジネス・ルールとは別に、いくつか重要なことが行われています。第 1 に、ben オブジェクトがデータベースに保存されると、OODBMS は明らかに、単に 1 つのオブジェクトを保管する以上のことをします。逆に ben オブジェクトをデータベースから取得すると、関連付けられた配偶者は、保管されているだけではなく、自動的に取得されるのです。
これは、考えてみると少し恐ろしい意味合いを持っています。ここから、OODBMS が無限再帰シナリオをどう回避しているのかを理解することは不可能ではありませんが、それよりも、ある 1 つのオブジェクトが、それぞれ独自のオブジェクトを参照する何十、あるいは何百、何千のオブジェクト参照を持つシナリオを考えると、もっと恐ろしくなります。例えば、このモデルが子供と親、等々を表現するとしたらどうなるかを少し考えてみてください。データベースから 1 つの Person を取得しようとすると、歴史の起源以来のすべての人間を取得することになっても不思議はありません。これでは、膨大なオブジェクトをネットワーク経由で引き出すことになります。
幸い、初期の OODBMS を除くすべての OODBMS は、こうした懸念事項に対応しており、db4o もその例外ではありません。
db4o の、この面を探るのは面倒な作業ですが、ここでは私の友人が教えてくれた方法、エクスプローラー・テスト (exploration test) を紹介しましょう。(私の知る限り最初にこの言葉を作り出した人である、Stu Halloway 氏に感謝します。) 一言で言えば、エクスプローラー・テストというのは、対象のライブラリーをテストするだけではなく API も調べ、ライブラリーが想定通り動作することを確認するために作成された一連のユニット・テストなのです。この方法の有益な副次的効果として、ライブラリーの将来のバージョンをエクスプローラー・テスト・コードに入れてコンパイルし、テストすることができます。もしコードがコンパイルできない、あるいはエクスプローラー・テストのすべてにパスすることができない場合には、そのライブラリーは明らかに後方互換ではありません。それを、実動システムで使う前に知ることができるのです。
db4o の API をエクスプローラー・テストすることで、データベースを作成してそこに Person データを追加する「before」メソッドを設定することができ、またデータベースを破棄してテスト中にフォルス・ポジティブが起きる可能性をなくす「after」メソッドを設定することができます。これがないと、毎回忘れずに persons.data ファイルを手動で削除しなければなりません。私は正直なところ、それを API を調べるたびに忘れずに行えるかどうか、自分自身を信頼できません。
ここでの db4o のエクスプローラー・テストでは、JUnit 4 テスト・ライブラリーをコンソール・モードで使います。何もテストを作成していない状態では、StructuredObjectTest クラスはリスト 4 に示すようなものです。
リスト 4. db4o の API を予備テストする
import java.io.*;
import java.util.*;
import com.db4o.*;
import com.db4o.query.*;
import com.tedneward.model.*;
import org.junit.Before;
import org.junit.After;
import org.junit.Ignore;
import org.junit.Test;
import static org.junit.Assert.*;
public class StructuredObjectsTest
{
ObjectContainer db;
@Before public void prepareDatabase()
{
db = Db4o.openFile("persons.data");
Person ben = new Person("Ben", "Galbraith",
Gender.MALE, 29, Mood.HAPPY);
Person jess = new Person("Jessica", "Smith",
Gender.FEMALE, 29, Mood.HAPPY);
ben.setSpouse(jess);
db.set(ben);
db.commit();
}
@After public void deleteDatabase()
{
db.close();
new File("persons.data").delete();
}
@Test public void testSimpleRetrieval()
{
List<Person> maleGalbraiths =
db.query(new Predicate<Person>() {
public boolean match(Person candidate) {
return candidate.getLastName().equals("Galbraith") &&
candidate.getGender().equals(Gender.MALE);
}
});
// Should only have one in the returned set
assertEquals(maleGalbraiths.size(), 1);
// (Shouldn't display to the console in a unit test, but this is an
// exploration test, not a real unit test)
for (Person p : maleGalbraiths)
{
System.out.println("Found " + p);
}
}
}
|
当然ながら、このテスト・スイートに対して JUnit テスト・ランナーを実行すると、コンソールあるいは GUI どちらのテスト・ランナーを使ったかによって、想定した出力である、「 . 」あるいは緑のバーが作成されます。通常、コンソールにデータを書き込むことは好ましくないですが (つまりデータは目ではなくアサーションを使って検証すべきです)、エクスプローラー・テストの場合には、何を取得できるのかを適当なデータをアサートする前に調べるための適切な方法であることに注意してください。もし他のものがすべて失敗したら、いつでも System.out.println コールをコメントアウトすることができます。(db4o の API の他の面をテストするために必要と思えるものを、自由にテストに追加してください。)
ここから先では、コード・サンプルは (メソッドのシグニチャーに対する @Test 注釈からわかるように) リスト 4 に示すテスト・スイート内部のテスト・メソッドだという前提で話を進めます。
構造化オブジェクトを保管するために必要なことの大部分は、これまでに行ったことと同じです。つまりオブジェクトに対して db.set() をコールすると、後は OODBMS が行ってくれます。set() をコールする対象のオブジェクトは、あまり重要ではありません。その理由は、OODBMS は OID (object identifier) によってオブジェクトを追跡するため (「多忙な Java 開発者のための db4o ガイド: クエリー、更新、そして ID」を参照)、同じオブジェクトを 2 度保管する方法を知らないからです。
構造化オブジェクトを取得することを考えると身の毛がよだちます。もし、(QBE によって、あるいはネイティブ・クエリーによって) 取得されるオブジェクトがいくつかのオブジェクト参照を持っており、またそうしたオブジェクトが、それぞれいくつかのオブジェクト参照を持っているとしたらどうなるでしょう。これは一種の悪徳ネズミ講のようなものではないでしょうか。
大部分の開発者は無限再帰に対して、最初は「絶対そんな風にしているはずはないのですが」というような反応をしますが、無限再帰はまさに (ある意味で)、構造化オブジェクトを db4o が取得する方法そのものです。実際、この種の動作は大部分のプログラマーが望む動作そのものです。なぜなら、私達がオブジェクトを探す際には一般的に、作成したオブジェクトが「すぐそこにある」ことを期待するからです。同時に、当然ながら私達は、世界全体をネットワークから取得したいとは思いません (少なくとも一度に取得したいとは思いません)。
db4o は妥協として、取得するオブジェクトの数を、アクティベーションの深さ (activation depth) と呼ばれるメカニズムを使って制限します (アクティベーションの深さは、オブジェクト・グラフのどこまで下がってオブジェクトを取得するのか、そのレベル数を示します)。つまりアクティベーションの深さは、db4o がトラバースしてクエリーの一部として返す、(ルート・オブジェクトから識別可能な) 参照の数をカウントしたものです。前の場合で Ben を取得する際に Jessica も取得するためには (1 つの参照をトラバースすれば Jessica を取得できるので)、デフォルトのアクティベーションの深さ (5) で十分です。Ben から 5 つの参照ホップよりも遠いオブジェクトは取得されず、それらのオブジェクトの参照はヌルのままです。そうすると今度は、(ObjectContainer コンテナーに対して activate() メソッドを使うことで) これらのオブジェクトをデータベースから明示的にアクティベートするのは私の仕事になります。
デフォルトのアクティベーションの深さを変更したい場合には、(db.configure() から返される) Configuration クラスに対して db4o の activationDepth() メソッドを使ってデフォルトを他の値に変更することで、きめ細かな変更を行うことができます。あるいは、アクティベーションの深さをクラスごとに設定することもできます。リスト 5 では、ObjectClass を使って Person 型のアクティベーションの深さを設定しています。
リスト 5. ObjectClass を使ってアクティベーションの深さを設定する
// See ObjectClass for more info
Configuration config = Db4o.configure();
ObjectClass oc = config.objectClass("com.tedneward.model.Person");
oc.minimumActivationDepth(10);
|
更新も、懸念事項の 1 つです。もし、グラフの中の、ある 1 つのオブジェクトを更新し、それを明示的に設定しなかったらどうなるのでしょう。初めて set() をコールすると、保管されているオブジェクトを参照する従属オブジェクトが保管されるのと同じように、あるオブジェクトが ObjectContainer に渡されると、db4o はその参照をトラバースし、見つかったオブジェクトもデータベースの中に保管します (リスト 6)。
リスト 6. 参照されるオブジェクトを更新する
@Test public void testDependentUpdate()
{
List<Person> maleGalbraiths =
db.query(new Predicate<Person>() {
public boolean match(Person candidate) {
return candidate.getLastName().equals("Galbraith") &&
candidate.getGender().equals(Gender.MALE);
}
});
Person ben = maleGalbraiths.get(0);
// Happy birthday, Jessica!
ben.getSpouse().setAge(ben.getSpouse().getAge() + 1);
// We only have a reference to Ben, so store that and commit
db.set(ben);
db.commit();
// Find Jess, make sure she's 30
Person jess = (Person)db.get(
new Person("Jessica", "Galbraith", null, 0, null)).next();
assertTrue(jess.getAge() == 30);
}
|
jess オブジェクトは変更されましたが、ben オブジェクトは jess への参照を保持したままです。従ってメモリー内の jess Person に対して更新された変更は、データベースに対しても一貫して持続されます。
いや、実はそうなりません。私はまったく嘘をついていました。
実は、これはエクスプローラー・テストに失敗し、フォルス・ポジティブを生ずる 1 つの領域なのです。ドキュメンテーションからは明白ではありませんが、ObjectContainer はアクティベートされたオブジェクトのキャッシュを維持します。そのため、リスト 6 のテストがコンテナーから Jessica オブジェクトを取得する際には、実際にディスクに書き込まれたデータではなく、その変更を含むメモリー内オブジェクトを返します。これによって今度は、ある型に対するデフォルトの更新の深さ (update depth) が 1 であるという事実が隠されます。これはつまり、set() コールでは (Strings) を含む) プリミティブ値のみが保管されることを意味します。この動作の実際を見るためには、少しテストを変更する必要があります (リスト 7)。
リスト 7. フォルス・ポジティブをテストする
@Test(expected=AssertionError.class)
public void testDependentUpdate()
{
List<Person> maleGalbraiths =
db.query(new Predicate<Person>() {
public boolean match(Person candidate) {
return candidate.getLastName().equals("Galbraith") &&
candidate.getGender().equals(Gender.MALE);
}
});
Person ben = maleGalbraiths.get(0);
assertTrue(ben.getSpouse().getAge() == 29);
// Happy Birthday, Jessica!
ben.getSpouse().setAge(ben.getSpouse().getAge() + 1);
// We only have a reference to Ben, so store that and commit
db.set(ben);
db.commit();
// Close the ObjectContainer, then re-open it
db.close();
db = Db4o.openFile("persons.data");
// Find Jess, make sure she's 30
Person jess = (Person)db.get(
new Person("Jessica", "Galbraith", null, 0, null)).next();
assertTrue(jess.getAge() == 30);
}
|
これを行うと、AssertionFailure が取得されます。これでは私が先ほど、オブジェクトを更新すると、その更新が順次グラフの下の方にカスケーディングしていく、と言ったことが嘘ということになります。(@Test 注釈に対してスローされると想定されるクラス型を expected 値に設定することで、この失敗を JUnit に事前に想定させることができます。)
キャッシュされたオブジェクトを db4o が暗黙的に処理せず、単純に返すだけという点は、議論の余地のあるところです。大部分のプログラマーは、この動作は破壊的であり直感的でないと考えるか、あるいはこれこそ OODBMS が行うべき動作だと考えるかのいずれかです。ここで重要なのは、それぞれの立場のメリットには深入りせず、データベースのデフォルトの動作を理解し、その変更方法を知ることです。リスト 8 では ObjectClass.setCascadeOnUpdate() メソッドを使って、ある特定の型に対する db4o のデフォルトの更新動作を変更しています。ただし、ObjectContainer を開く前に、このメソッドを「真」に設定する必要があったことに注意してください。リスト 8 は、変更された、正しいカスケーディング・テストを示しています。
リスト 8. カスケーディング動作を「真」に設定する
@Test
public void testWorkingDependentUpdate()
{
// the cascadeOnUpdate() call must be done while the ObjectContainer
// isn't open, so close() it, setCascadeOnUpdate, then open() it again
db.close();
Db4o.configure().objectClass(Person.class).cascadeOnUpdate(true);
db = Db4o.openFile("persons.data");
List<Person> maleGalbraiths =
db.query(new Predicate<Person>() {
public boolean match(Person candidate) {
return candidate.getLastName().equals("Galbraith") &&
candidate.getGender().equals(Gender.MALE);
}
});
Person ben = maleGalbraiths.get(0);
assertTrue(ben.getSpouse().getAge() == 29);
// Happy Birthday, Jessica!
ben.getSpouse().setAge(ben.getSpouse().getAge() + 1);
// We only have a reference to Ben, so store that and commit
db.set(ben);
db.commit();
// Close the ObjectContainer, then re-open it
db.close();
db = Db4o.openFile("persons.data");
// Find Jess, make sure she's 30
Person jess = (Person)db.get(
new Person("Jessica", "Galbraith", null, 0, null)).next();
assertTrue(jess.getAge() == 30);
}
|
カスケーディング動作は、更新用に設定できるだけではなく、取得用に設定する (そして「無限の」アクティベーションの深さを作成する) ことも、削除用に設定することもできます。削除用の設定については、洗練された新しい Person オブジェクトに対する操作として、最後に説明します。
オブジェクトをデータベースから削除するための操作は、取得や更新の場合と同じです。あるオブジェクトが削除される際には、デフォルトでは、そのオブジェクトが参照するオブジェクトはどれも削除されません。これも、一般的には望ましい動作です (リスト 9)。
リスト 9. 構造化オブジェクトを削除する
@Test
public void simpleDeletion()
{
Person ben = (Person)db.get(new Person("Ben", "Galbraith", null, 0, null)).next();
db.delete(ben);
Person jess = (Person)db.get(new Person("Jessica", "Galbraith", null, 0, null)).next();
assertNotNull(jess);
}
|
しかし場合によると、削除するオブジェクトに参照されているオブジェクトも強制的に削除したいことがあります。アクティベーションと更新の場合と同じく、この動作は Configuration クラスを呼び出すことで設定することができます (リスト 10)。
リスト 10. Configuration.setCascadeOnDelete()
@Test
public void cascadingDeletion()
{
// the cascadeOnUpdate() call must be done while the ObjectContainer
// isn't open, so close() it, setCascadeOnUpdate, then open() it again
db.close();
Db4o.configure().objectClass(Person.class).cascadeOnDelete(true);
db = Db4o.openFile("persons.data");
Person ben =
(Person)db.get(new Person("Ben", "Galbraith", null, 0, null)).next();
db.delete(ben);
ObjectSet<Person> results =
db.get(new Person("Jessica", "Galbraith", null, 0, null));
assertFalse(results.hasNext());
}
|
ただしこれは、注意して行う必要があります。なぜなら、これはカスケードで削除されるオブジェクトを参照する他のすべてのオブジェクトが、今やヌルへの参照を保持することを意味するからです。つまり db4o オブジェクト・データベースには、参照されるオブジェクトが削除されるのを防止するための参照整合性の概念がありません。(参照整合性は db4o では一般的に要求される機能であり、開発チームは、今後のバージョンでこの機能を追加する方法を検討中だと聞いています。リレーショナル・データベースの場合でさえ、整合性を破ることが実際には望ましい場合もあることを考えると、鍵となるのは、db4o を使用する開発者にとって POLS (Principle Of Least Surprise: 驚き最小の法則) に反しない方法で、この機能を追加することになるでしょう。)
今回の記事は、このシリーズの分岐点です。前回までは、すべての例は非常に単純なオブジェクトに基づくものでした。それらの例はアプリケーションの観点からは非現実的ですが、保管されるオブジェクトではなく OODBMS の理解に焦点を絞るためには適切でした。参照によって保持される関連オブジェクトを db4o のような OODBMS がどう保管するのかを理解することは、簡単ではありません。しかし幸いなことに、いったん動作が完全に説明され、それを理解できれば、単にその動作を考慮に入れてコードの調整を始めればよいだけです。
この記事では、db4o のオブジェクト・モデルを考慮して複雑なコードを調整するための最初の例をいくつか見てきました。構造化オブジェクトに対して単純な CRUD 操作を行う方法を学び、そうする中で、必然的に発生する問題とその回避方法をいくつか見ました。
しかしここまでは、構造化オブジェクトの例と言っても、それらに対する直接の参照を操作したのみ、という意味で、ある程度単純なものでした。多くのカップルが学ぶように、結婚してしばらくすると、子供の話題が出るようになります。このシリーズの次回の記事では、ben オブジェクトとjess オブジェクトとでうまくいっている状況に何人かの子供を組み入れると何が起こるのかを調べながら、db4o で構造化オブジェクトを作成して操作するための方法をさらに詳しく探ります。
学ぶために
-
このシリーズ「多忙な Java 開発者のための db4o ガイド」 (Ted Neward 著、developerWorks) は、今日のオブジェクト指向言語やシステム、考え方を活用したオープンソースのデータベース、db4o を紹介しています。
-
「多忙な Java 開発者のための db4o ガイド: クエリー、更新、そして ID」 (Ted Neward 著、2007年3月、developerWorks) は、データを発見し、取得するために db4o が持つ様々な機能を、OID を含めて解説しています。
-
「Have a Little Respect for SQL Databases」 (Jack D. Herrington 著、DevX.com、2003年10月) は、参照整合性を含め、リレーショナル・データベースが得意なことを解説しています。
-
「コード品質を追求する」
(Andrew Glover 著、developerWorks) を読み、エクスプローラー・テストのような開発者用テスト手法について学んでください。
-
db4o のホームページで db4o について学んでください。
-
ODBMS.orgには、オブジェクト・データベース技術に関する無料資料が豊富に用意されています。
-
developerWorks の Java technology ゾーンには、Java プログラミングのあらゆる側面を網羅した記事が豊富に用意されています。
-
New to IBM Information Managementをご覧ください。まだ OODBMS 用には販売されていないかもしれませんが、IBM の強力な RDBMS (relational database management system) サーバー・ファミリーに関する情報を入手することができます。
製品や技術を入手するために
-
Java プログラミングと .NET 専用のオープンソースのデータベース、 db4o をダウンロードしてください。
議論するために
-
developerWorks
blogsからdeveloperWorks のコミュニティーに加わってください。