このシリーズの前回の記事では、db4o が構造化オブジェクト、つまり非プリミティブ・フィールドを含むオブジェクトをどう処理するかの説明から始めました。そこで説明したように、オブジェクト同士の関係が複雑になると、db4o のパーシスタンス・モデルにとって深刻な影響があります。前回の記事では、アクティベーションの深さ (activation depth) や、更新や削除のカスケーディング、参照整合性などに対応することの重要性を説明しました。db4o では、削除の際にはこれらの機能をサポートしていません。また前回の記事では、エクスプローラー・テストという開発者用のテスト方法を紹介し、さらに db4o の API を使った最初の演習を行いました。
この記事では、db4o での構造化オブジェクトの保管と操作の紹介を続けます。最初に、オブジェクトがオブジェクトのコレクションをフィールドとして保持する、多重度を持った関係 (multiplicity relationship) について調べます。(このシリーズでのコレクションは、ArrayList のような Collection クラスと、標準的な言語配列の両方を指します。) この記事で、db4o が多重度を問題なく処理できることを理解できるはずです。また、db4o のカスケード更新とアクティベーションの深さについてもさらによく理解できるはずです。
古い Person クラスは、このシリーズが進むにつれて明らかに複雑になっていきます。構造化オブジェクトに関する前回の説明では、Person に lispouse(配偶者) フィールドと、Person に関するいくつかのビジネス・ルールを追加したところで終わりました。前回の最後に触れたように、和やかな家庭生活を送っているうちに、その家庭で生活をともにする「小さな誰か (子供)」が 1 人あるいは何人かできるかもしれません。しかし、この家庭の構成に子供を追加し始める前に、この Person が実際に住む場所を持っていることを確認したいと思います。それを行う間、Person に、仕事先の明確な場所と、少なくとも夏のバケーション用に素敵な別荘を持てるオプションを与えたいと思います。この 3 つのすべてを、Address 型が処理してくれるはずです。
リスト 1. Person クラスに Address 型を追加する
package com.tedneward.model;
public class Address
{
public Address()
{
}
public Address(String street, String city, String state, String zip)
{
this.street = street; this.city = city;
this.state = state; this.zip = zip;
}
public String toString()
{
return "[Address: " +
"street=" + street + " " +
"city=" + city + " " +
"state=" + state + " " +
"zip=" + zip + "]";
}
public int hashCode()
{
return street.hashCode() & city.hashCode() &
state.hashCode() & zip.hashCode();
}
public boolean equals(Object obj)
{
if (obj == this)
return this;
if (obj instanceof Address)
{
Address rhs = (Address)obj;
return (this.street.equals(rhs.street) &&
this.city.equals(rhs.city) &&
this.state.equals(rhs.state) &&
this.zip.equals(rhs.zip));
}
else
return false;
}
public String getStreet() { return this.street; }
public void setStreet(String value) { this.street = value; }
public String getCity() { return this.city; }
public void setCity(String value) { this.city = value; }
public String getState() { return this.state; }
public void setState(String value) { this.state = value; }
public String getZip() { return this.zip; }
public void setZip(String value) { this.zip = value; }
private String street;
private String city;
private String state;
private String zip;
}
|
これを見るとわかるように、Address は単純なデータ・オブジェクトにすぎません。これを Person クラスに追加するということは、Person が、address と呼ばれるフィールドとして Person の配列を持つことを意味します。最初の住所は必ず家の住所であり、2 番目は仕事先の住所、そして 3 番目は (ヌルでなければ) 別荘です。もちろん、これらはすべて、さまざまなメソッドによって今後のカプセル化に対して保護されています。
それが終わったら、今度は子供をサポートするように Person クラスを強化する番です。そこで、Person に新しいフィールド、Person の ArrayList を与えます。これにも、適切なカプセル化が行えるようにするためのメソッドがをあります。
次に、ほとんどの子供には親がいるので、さらに 2 つのフィールドを母親と父親用に、適切なaccesor/mutator メソッドと共に追加します。この Person クラスに新しいメソッドを追加し、新しい Person (haveBaby という適切な名前です) を作成できるようにします。また、子供を持つための生物学的な要求をサポートするいくつかのビジネス・ルールを追加し、そしてこの新しい小さな Person を、mother フィールドと father フィールドに対して作成された children の ArrayList に追加します。これがすべて終わったら、この赤ちゃんを呼び出し側に返します。
リスト 2 は、この多重度を持った関係を処理するように定義された、新しい Person クラスを示しています
リスト 2. 多重度を持った関係として定義された家族
package com.tedneward.model;
import java.util.List;
import java.util.ArrayList;
import java.util.Iterator;
public class Person
{
public Person()
{ }
public Person(String firstName, String lastName, Gender gender, int age, Mood mood)
{
this.firstName = firstName;
this.lastName = lastName;
this.gender = gender;
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 Gender getGender() { return gender; }
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 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);
}
public Address getHomeAddress() { return addresses[0]; }
public void setHomeAddress(Address value) { addresses[0] = value; }
public Address getWorkAddress() { return addresses[1]; }
public void setWorkAddress(Address value) { addresses[1] = value; }
public Address getVacationAddress() { return addresses[2]; }
public void setVacationAddress(Address value) { addresses[2] = value; }
public Iterator<Person> getChildren() { return children.iterator(); }
public Person haveBaby(String name, Gender gender) {
// Business rule
if (this.gender.equals(Gender.MALE))
throw new UnsupportedOperationException("Biological impossibility!");
// Another highly objectionable business rule
if (getSpouse() == null)
throw new UnsupportedOperationException("Ethical impossibility!");
// Welcome to the world, little one!
Person child = new Person(name, this.lastName, gender, 0, Mood.CRANKY);
// Well, wouldn't YOU be cranky if you'd just been pushed out of
// a nice warm place?!?
// These are your parents...
child.father = this.getSpouse();
child.mother = this;
// ... and you're their new baby.
// (Everybody say "Awwww....")
children.add(child);
this.getSpouse().children.add(child);
return child;
}
public String toString()
{
return
"[Person: " +
"firstName = " + firstName + " " +
"lastName = " + lastName + " " +
"gender = " + gender + " " +
"age = " + age + " " +
"mood = " + mood + " " +
(spouse != null ? "spouse = " + spouse.getFirstName() + " " : "") +
"]";
}
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.gender.equals(other.gender) &&
this.age == other.age);
}
private String firstName;
private String lastName;
private Gender gender;
private int age;
private Mood mood;
private Person spouse;
private Address[] addresses = new Address[3];
private List<Person> children = new ArrayList<Person>();
private Person mother;
private Person father;
} |
これだけ大量のコードがあっても、リスト 2 は家族の関係のモデルとしては単純化されたモデルです。この階層構造のどこかの点で、こうした大量のヌル値を処理しなければなりません。しかしその問題は db4o でのオブジェクト操作の問題というよりも、むしろオブジェクトのモデリングの問題です。そのため、とりあえず無視しても安全です。
リスト 2 の Person クラスに関する重要な点として、親と子の間の関係を、階層構造で循環型の一連の参照を使ってリレーショナルの方法でモデリングすることは明らかに不適切だということに注意してください。私が指摘している複雑さは、インスタンス化されたオブジェクト・モデルを見ると一層よくわかります。そこで、Person クラスをインスタンス化するエクスプローラー・テストを作成することにします。リスト 3 ではJUnit の scaffold を除いていることに注意してください。JUnit 4 API については、(このシリーズの前回の記事を含めて) 他の資料で学べると思います。また、この記事のソース・コードを読むことも、詳しく学ぶために役立つはずです。
リスト 3. 幸福な家族のテスト
@Test public void testTheModel()
{
Person bruce = new Person("Bruce", "Tate",
Gender.MALE, 29, Mood.HAPPY);
Person maggie = new Person("Maggie", "Tate",
Gender.FEMALE, 29, Mood.HAPPY);
bruce.setSpouse(maggie);
Person kayla = maggie.haveBaby("Kayla", Gender.FEMALE);
Person julia = maggie.haveBaby("Julia", Gender.FEMALE);
assertTrue(julia.getFather() == bruce);
assertTrue(kayla.getFather() == bruce);
assertTrue(julia.getMother() == maggie);
assertTrue(kayla.getMother() == maggie);
int n = 0;
for (Iterator<Person> kids = bruce.getChildren(); kids.hasNext(); )
{
Person child = kids.next();
if (n == 0) assertTrue(child == kayla);
if (n == 1) assertTrue(child == julia);
n++;
}
}
|
ここまでは順調でした。子供に対して ArrayList を使うことから必然的に生ずる長子相続性を含め、すべてをチェックできました。しかし、db4o データベースにテスト・データを追加するために @Before 条件と @After 条件を追加すると、もっと興味深いことになります。
リスト 4. 子供をデータベースに送る
@Before public void prepareDatabase()
{
db = Db4o.openFile("persons.data");
Person bruce = new Person("Bruce", "Tate",
Gender.MALE, 29, Mood.HAPPY);
Person maggie = new Person("Maggie", "Tate",
Gender.FEMALE, 29, Mood.HAPPY);
bruce.setSpouse(maggie);
bruce.setHomeAddress(
new Address("5 Maple Drive", "Austin",
"TX", "12345"));
bruce.setWorkAddress(
new Address("5 Maple Drive", "Austin",
"TX", "12345"));
bruce.setVacationAddress(
new Address("10 Wanahokalugi Way", "Oahu",
"HA", "11223"));
Person kayla = maggie.haveBaby("Kayla", Gender.FEMALE);
kayla.setAge(8);
Person julia = maggie.haveBaby("Julia", Gender.FEMALE);
julia.setAge(6);
db.set(bruce);
db.commit();
}
|
これでも、家族全員を保管する作業が 1 つの Person オブジェクトを保管する以上に大変なわけではないことに注目してください。前回の記事から、保管されたオブジェクトは本質的に再帰的であること、そして bruce からアクセスできるオブジェクトは bruce 参照が db.set() 呼び出しに渡されると保管されることを思い出してください。しかし説明だけでは重みがありません。そこで単純なエクスプローラー・テストを実行して、実際に何が起こるのかを調べてみましょう。最初に、Person と共に保管されているさまざまな Address が、呼び出されたときに見つかるかどうかをテストします。次に、子供も存在して考慮されているかどうかをテストします。
リスト 5. 家庭と家族を検索する
@Test public void testTheStorageOfAddresses()
{
List<Person> maleTates =
db.query(new Predicate<Person>() {
public boolean match(Person candidate) {
return candidate.getLastName().equals("Tate") &&
candidate.getGender().equals(Gender.MALE);
}
});
Person bruce = maleTates.get(0);
Address homeAndWork =
new Address("5 Maple Drive", "Austin",
"TX", "12345");
Address vacation =
new Address("10 Wanahokalugi Way", "Oahu",
"HA", "11223");
assertTrue(bruce.getHomeAddress().equals(homeAndWork));
assertTrue(bruce.getWorkAddress().equals(homeAndWork));
assertTrue(bruce.getVacationAddress().equals(vacation));
}
@Test public void testTheStorageOfChildren()
{
List<Person> maleTates =
db.query(new Predicate<Person>() {
public boolean match(Person candidate) {
return candidate.getLastName().equals("Tate") &&
candidate.getGender().equals(Gender.MALE);
}
});
Person bruce = maleTates.get(0);
int n = 0;
for (Iterator<Person> children = bruce.getChildren();
children.hasNext();
)
{
Person child = children.next();
System.out.println(child);
if (n==0) assertTrue(child.getFirstName().equals("Kayla"));
if (n==1) assertTrue(child.getFirstName().equals("Julia"));
n++;
}
}
|
リスト 5 に示す Collection ベースの型 (ArrayList) が、Person 型の「dependent (扶養家族)」としてではなく、それ自体で完全なオブジェクトとして保管されることを不思議に思う人がいるかもしれません。これは、いったん理解してしまえば納得できますが、オブジェクト・データベースの ArrayList 型に対してクエリーを実行すると、おかしな結果を生ずる可能性があり、時には実際におかしな結果を生じます。ここまでの時点でデータベースには ArrayList が 1 つしかないので、それに対してクエリーを実行すると何が起こるかを調べるためにエクスプローラー・テストを実行しても、あまり意味がありません。これは皆さんの演習問題とします。
当然ながら、コレクションの中に保管された Person も、データベースの中ではファーストクラス・エンティティーとして扱われます。そのため、ある特定の基準を満たすすべての Person (例えば女性の Person すべて、など) を見つけると、ArrayList インスタンス内部から参照される Person も見つかります (リスト 6)。
リスト 6. Julia はどこにいるのでしょう
@Test public void findTheGirls()
{
List<Person> girls =
db.query(new Predicate<Person>() {
public boolean match(Person candidate) {
return candidate.getGender().equals(Gender.FEMALE);
}
});
boolean maggieFound = false;
boolean kaylaFound = false;
boolean juliaFound = false;
for (Person p : girls)
{
if (p.getFirstName().equals("Maggie"))
maggieFound = true;
if (p.getFirstName().equals("Kayla"))
kaylaFound = true;
if (p.getFirstName().equals("Julia"))
juliaFound = true;
}
assertTrue(maggieFound);
assertTrue(kaylaFound);
assertTrue(juliaFound);
}
|
オブジェクト・データベースが、(少なくとも参照を知っている限り) 参照を「正しく」保持することにも注意してください。例えば、ある Person (例えば母親) と別の Person (例えば娘) を別々のクエリーで取得しても、相変わらず両者の間には双方向の関係があるものとして認識されます (リスト 7)。
リスト 7. 関係を実際どおりに保つ
@Test public void findJuliaAndHerMommy()
{
Person maggie = (Person) db.get(
new Person("Maggie", "Tate", Gender.FEMALE, 0, null)).next();
Person julia = (Person) db.get(
new Person("Julia", "Tate", Gender.FEMALE, 0, null)).next();
assertTrue(julia.getMother() == maggie);
}
|
当然ですが、これはオブジェクト・データベースに期待される動作そのものです。また、上記で daughter オブジェクトを返すクエリーに対するアクティベーションの深さが十分低く設定されていれば、getMother() を呼び出すと、実際のオブジェクトではなく、ヌルが返されることにも注意してください。これは、Person の mother フィールドが、取得される元々のオブジェクトの、もう 1 つの「ホップ」だからです。(アクティベーションの深さについては「前回の記事」を参照してください。)
ここまでは、db4o が複数のオブジェクトの保管と取得をどう処理するのかを見てきました。では、更新と削除に関してオブジェクト・データベースは何をするのでしょう。構造化オブジェクトの場合と同じく、複数オブジェクトの更新あるいは削除の間に行われることの大部分は、更新の深さの管理、あるいはカスケード削除に関係しています。これまでの説明で、構造化オブジェクトとコレクションの間に多くの類似点があり、一方のエンティティー・タイプについて言えることが、ほとんどそのまま他方のエンティティー・タイプにも当てはまることに気付いた人もいるかもしれません。これは、ArrayList を、コレクションではなく「もう 1 つの構造化オブジェクト」と見なせば納得できます。
つまり、これまでに学んだことから、データベースの中の girl の 1 人を更新し、そしてそのオブジェクトを更新するためには、その親のどちらか一方を単純にデータベースに再保管するだけでよいはずです (図 8)。
リスト 8. Kayla、誕生日おめでとう
@Test public void kaylaHasABirthday()
{
Person maggie = (Person) db.get(
new Person("Maggie", "Tate", Gender.FEMALE, 0, null)).next();
Person kayla = (Person) db.get(
new Person("Kayla", "Tate", Gender.FEMALE, 0, null)).next();
kayla.setAge(kayla.getAge() + 1);
int kaylasNewAge = kayla.getAge();
db.set(maggie);
db.close();
db = Db4o.openFile("persons.data");
kayla = (Person) db.get(
new Person("Kayla", "Tate", Gender.FEMALE, 0, null)).next();
assert(kayla.getAge() == kaylasNewAge);
}
|
「前回の記事」から、データベースへの接続を明示的に切断し、既に作業メモリーにあるオブジェクトを再取得するフォルス・ポジティブを回避する必要があることを思い出してください。
多重度を持った関係でのオブジェクトの削除は、前回説明した構造化オブジェクトの削除とほとんど同じであり、両方の種類のオブジェクトに影響するカスケード削除のみに注意すればよいだけです。カスケード削除を行うと、そのオブジェクトは、そのオブジェクトが参照されていたすべての場所から完全に削除されます。データベースから Person を削除しようとしてカスケード削除を行うと、その Person の mother オブジェクトと father オブジェクトは、それらのオブジェクトの children コレクションの中に突然、有効なオブジェクト参照ではなくヌル参照を持つことになります。
多くの面で、配列とコレクションをオブジェクト・データベースに保管する作業は、通常の構造化オブジェクトを保管する場合の作業とほとんど変わりません。ただし、配列に対して直接クエリーを実行することはできませんが、コレクションはできるという単純な点に注意してください。これは実際上、モデリングしながらコレクションと配列を使うことができ、いずれか一方を使うようにパーシスタンス・エンジンが要求するまで待つ必要はないということを意味します。
学ぶために
- 「多忙な Java 開発者のための db4o ガイド: 単純なオブジェクトを越える」(Ted Neward 著、 developerWorks、2007年7月) を読んで構造化オブジェクトの使い方を学び、また 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) シリーズを読み、エクスプローラー・テストのような開発者用テスト手法について学んでください。
- このシリーズ「多忙な Java 開発者のための db4o ガイド」(Ted Neward 著、developerWorks) は、今日のオブジェクト指向言語やシステム、考え方を活用したオープンソースのデータベース、db4o を紹介しています。
-
db4o のホームページで db4o について学んでください。
-
ODBMS.org には、オブジェクト・データベース技術に関する無料資料が豊富に用意されています。
-
developerWorks の Java technology ゾーンには、Java プログラミングのあらゆる側面を網羅した記事が豊富に用意されています。
-
NNew to IBM Information Management をご覧ください。まだ OODBMS 用には販売されていないかもしれませんが、IBM の強力な RDBMS (relational database management system) サーバー・ファミリーに関する情報を入手することができます。
製品や技術を入手するために
- Java プログラミングと .NET 専用のオープンソースのデータベース、db4o をダウンロードしてください。
議論するために
-
developerWorks blogs から developerWorks のコミュニティーに加わってください。