多忙な Java 開発者のための db4o ガイド: クエリー、更新、そして ID

db4o でのクエリー方法

データを探し出して、取得するための主要な機構として、RDBMS は SQL を使いますが、OODBMS では、いくつかの異なる機構の中から 1つを選んで使うことができます。Ted Neward によるシリーズ第 2 回目の今回は、そうした機構の例として、Query by Exampleや OODBMS 特有のカスタム機構などのオプションを紹介します。これから彼が説明するように、SQL に代わる方法のいくつかは、SQL そのものよりも簡単に使うことができます。

Ted Neward, Principal, Neward & Associates

Ted Neward は、Neward & Associates の代表として、Java や .NET、XML サービスなどのプラットフォームに関するコンサルティング、助言、指導、講演を行っています。彼はワシントン州シアトルの近郊に在住です。



2007年 3月 27日

このシリーズの第 1 回の記事では、Java™ オブジェクトのストレージのためのソリューションとして、RDBMS が不適切であることを説明しました。そこで説明したように、今日のようなオブジェクト指向の世界では、db4oのようなオブジェクト・データベースの方が、リレーショナル・データベースよりも多くのものをオブジェクト指向の開発者に提供します。

このシリーズについて

過去 10 年ほど、情報の保存と取得はほとんど RDBMS と同義語でしたが、最近それが変わり始めています。特に Java 開発者は、いわゆるオブジェクトとリレーショナルとのミスマッチに不満を募らせており、それを解決するためのソリューションにも我慢できなくなっています。そのため、また有効な代替手段が現れてきたことにより、オブジェクトのパーシスタンスと取得に対する新たな関心が生まれつつあります。このシリーズではdb4o の実用的な紹介をします。db4o はオープンソースのデータベースであり、今日のオブジェクト指向の言語やシステム、考え方を活用しています。db4oのホームページを訪れ、今すぐ db4o をダウンロードしてください。この記事で説明する例を追うためには、このダウンロードが必要です。

今回の記事、および今後の記事では、オブジェクト・データベースの説明を続けます。そして、例を使いながら、一般に好まれているオブジェクト指向プログラミング言語(ここでは Java プログラミング) で扱うエンティティーと同じ「形状」のエンティティーになるように最適化されたストレージ・システムの強力さを示します。そして特に、オブジェクトを取得し、変更し、そして復元してdb4o に戻すためのさまざまな機構を紹介します。これから学ぶように、いったん SQL の制約から解放されると、驚くほどさまざまなことが可能になるのです。

まだ db4o をダウンロードしていない人は、今すぐ db4o をダウンロードしてください。ここで示す例をコンパイルするためには db4o が必要です。

Query by Example

QBE (Query by Example) はデータベース問い合わせ言語です。QBE は、述部基準を使う言語 (SQL など) とは異なり、比較の元となる「テンプレート」を設計することによって、クエリーを作成することができます。前回の記事では、db4oの QBE エンジンを使ってデータを取得する例を説明しましたが、それをここで簡単に復習します。まず、非常に初歩的なデータベースを見ることから始めましょう。このデータベースは1 つの型から成り、その定義をリスト 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;
}

POJO のように、Person は非常に単純です。Person は、POJO 流のアクティビティー (つまり toString()equals()) をサポートするための 3 つのフィールドと、いくつかの基本的なメソッドから構成されています。(Joshua Bloch の『EffectiveJava』を読んだ鋭い人は、私が hashCode() の実装を含めていないことに気付くでしょう。これは明らかに Rule 8 に違反します。よくある技術書の表現を借りて、私は hashCode() を「読者への課題」とします。これは通常、著者がそこまで手間をかけたくない、あるいは手近な例としては必要ないと思っていることを意味します。そのどちらなのか、その判断も読者への課題としましょう。)

リスト 2 では、6 個のオブジェクトを作成し、それらをファイルに入れ、そして QBE を使って、first name が「Brian」というパターンに一致する 2 つのオブジェクトを呼び出しています。このスタイルのクエリーは、プロトタイプ・オブジェクト (get() を呼び出す際に渡されるオブジェクト) を使って、データベースのオブジェクトとこのプロトタイプ・オブジェクトが一致するかどうかを判断し、その判断基準に一致するオブジェクトのObjectSet (基本的にオブジェクトの集合) を返します。

リスト 2. Query by Example
import com.db4o.*;

import com.tedneward.model.*;

public class Hellodb4o
{
    public static void main(String[] args)
        throws Exception
    {
        ObjectContainer db = null;
        try
        {
            db = Db4o.openFile("persons.data");

            Person Brian = 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);
            
            db.set(Brian);
            db.set(jason);
            db.set(Brians);
            db.set(david);
            db.set(glenn);
            db.set(neal);

            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();
        }
    }
}

クエリーの規則

QBE はデータを検索するためのテンプレートとしてプロトタイプ・オブジェクトを使うため、その使い方に関して、いくつかの単純な規則があります。db4oが指定のターゲットに対して Person 型の全オブジェクトを検索し (これは実際の動作を単純化しすぎていますが、概念的には正確です)、データ・ストアの中の特定オブジェクトが基準に一致するかどうかを判断する場合、フィールド値は1 つずつ比較されます。もしプロトタイプ・オブジェクトのフィールドが「ヌル」であれば、この値はデータ・ストア中のどの値とも一致しますが、それ以外の場合には、値は完全に一致する必要があります。プリミティブ型の場合、プリミティブ型は実際には「ヌル」という値を持てないため、ワイルドカード値としてゼロが使われます。(これはQBE による方法の限界も示しています。つまり、ゼロは実質的には検索用の値には使えません。) もし複数のフィールド値が指定されている場合には、候補となるオブジェクトがクエリー基準を満たすには、データベースの中にある、そのオブジェクトのすべてのフィールド値が一致する必要があります。これは要するに、そのオブジェクトのフィールドが「AND」されてクエリー述部を構成する、ということです。

前の例では、クエリーは、firstName フィールドが「Brian」に等しいすべての Person 型を探し、lastName フィールドと age フィールドは実質的に無視されます。テーブルの中では、この呼び出しは大雑把に言えば、SQL クエリーの SELECT * FROM Person WHERE firstName = "Brian" に対応します。(ただし、OODBMS クエリーから SQL にマップしようとする際には注意してください。この例えは完璧ではなく、特定のクエリーの性質やパフォーマンスに関して誤解を生ずる恐れがあります。)

クエリーで返されるオブジェクトは ObjectSet です。これはオブジェクト用の単純なコンテナーであるという意味で、JDBC の ResultSet に似ています。クエリーの結果を 1 つずつ確認するのは、ObjectSet が実装する Iterator インターフェースを使えば簡単です。Person の特定のメソッドを使うためには、next() が返すオブジェクトに対するダウンキャストが必要です。


更新と ID

単純にデータを表示すること自体も興味深いことですが、大部分のオブジェクトは、表示する以外にも、変更してデータベースに戻す必要があります。これはOODBMS を扱う上で、おそらくもっとも面倒な部分です。オブジェクト・データベースでは、リレーショナル・データベースとは異なる ID の概念を使うからです。これを現実的に言えば、オブジェクト・データベースを扱う際には、「メモリー中のオブジェクトとストレージ中のオブジェクト」の関係に十分注意する必要があるということです。

リスト 3 に示す単純な例は、この、ID の概念の違いを示しています。

リスト 3. Brian が 3 人
import com.db4o.*;

import com.tedneward.model.*;

public class Hellodb4o
{
    public static void main(String[] args)
        throws Exception
    {
        ObjectContainer db = null;
        try
        {
            db = Db4o.openFile("persons.data");

            Person Brian = 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);
            
            db.set(Brian);
            db.set(jason);
            db.set(Brians);
            db.set(david);
            db.set(glenn);
            db.set(neal);

            db.commit();
            
            // Find all the Brians
            ObjectSet Brians = db.get(new Person("Brian", null, 0));
            while (Brians.hasNext())
                System.out.println(Brians.next());
                
            Person Brian2 = new Person("Brian", "Goetz", 39);db.set(Brian2);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();
        }
    }
}

code3 のクエリーを実行すると、データベースは 3 人の Brian をレポートし、そのうちの 2 人は Brian Goetz です。(似たようなことは、カレント・ディレクトリーに Personリスト 3 のクエリーを実行すると、データベースは 3 人の Brian をレポートし、そのうちの 2 人は Brian Goetz です。(似たようなことは、カレント・ディレクトリーに Persons.dataファイルが既に存在している場合にも起こります。つまり、作成されたすべての Person は Persons.data ファイルに保存され、そこに保存されたすべての Brian がクエリーによって返されます。)

Extension インターフェース

db4o 開発チームは時々、一部の API があまり頻繁に使われていないこと、あるいはコアである ObjectContainer API の一部とすべきかどうか迷うような API に対して「experiments (実験)」と表していることがあります。このような場合、ext() メソッドが返す ExtObjectContainer インスタンスにメソッドが提供されます。このクラスの中にあるメソッドはリリースごとに異なり、新たなものが導入されたり、削除されたり、あるいはコアのObjectContainer クラス自体の中に移動されたりします。このリストには、メモリー内のオブジェクトが db4o のコンテナー・インスタンスに関連付けられているかをテストするためのメソッドや、このコンテナーが認識しているすべてのクラスのリストを取得するメソッド、あるいは並列処理のためのセマフォーを設定、解放するためのメソッドなどが含まれていることが知られています。いつものことながら、ExtObjectContainer クラスの完全な詳細については、db4o のドキュメンテーションを参照してください。

明らかに、ここでは主キーに関する従来のルールが適用されません。では、オブジェクト・データベースは、一意性の概念をどう扱うのでしょう。

OID を活用する

あるオブジェクトがオブジェクト・データベースに保存されると、Object identifier、つまり OID (発音は avoid の最後の音節と似ています)と呼ばれる固有のキーが作成され、このキーがそのオブジェクトを一意に識別します。OID は、C# や Java プログラミングの this ポインター/参照と同じように、明示的に要求されない限り表現されることはありません。db4oでは、指定されたオブジェクトの OID を確認するには、db.ext().getID() を呼び出します。(逆に db.ext().getByID() メソッドを使って OID からオブジェクトを取得することもできます。このメソッドの呼び出しには、ここで説明しきれないほど複雑な意味がありますが、このメソッドが1 つの選択肢であることは変わりません。)

このことが実際に意味しているのは、あるオブジェクトがシステム内に存在していたかどうかを判断することは、開発者の仕事であるということです。通常、その判断は、そのオブジェクトをコンテナーに挿入する前にコンテナーに対してクエリーを実行することで行います(リスト 4)。

リスト 4. 挿入前にクエリーを実行する
// ... as before
        ObjectContainer db = null;
        try
        {
            db = Db4o.openFile("persons.data");
            
            ...

            // We want to add Brian Goetz to the database; is he already there?
            if (db.get(new Person("Brian", "Goetz", 0).hasNext() == false)
            {
                // Nope, no Brian Goetz here, go ahead and add him
                db.set(new Person("Brian", "Goetz", 39));
                db.commit();
            }
        }

この特定のケースでは、システム内での Person の一意性は、first name と last name の組み合わせによって決まるとしましょう。従ってデータベースの中で Brian を検索する際には、Person インスタンスに対する、これらの属性を探せばよいわけです。(もしかすると Brian は 2、3 年前、39 歳になる前に追加されたかもしれません。)

データベースの中のオブジェクトを変更する場合には、単純にそのオブジェクトをコンテナーから取得し、何らかの方法で変更し、そして再度保存するだけです(リスト 5)。

リスト 5. オブジェクトを更新する

リスティングを見るにはここをクリック

リスト 5. オブジェクトを更新する

// ... as before
        ObjectContainer db = null;
        try
        {
            db = Db4o.openFile("persons.data");
            
            ...

            // Happy Birthday, David Geary!
            if ((ObjectSet set = db.get(new Person("David", "Geary", 0))).hasNext())
            {
                Person davidG = (Person)set.next();davidG.setAge(davidG.getAge() + 1);db.set(davidG);db.commit();
            }
            else
                throw new MissingPersonsException(
                    "David Geary doesn't seem to be in the database");
        }

db4o コンテナーは、ここでは ID の問題は発生しません。対象のオブジェクトがデータベースの中にあるものとして既に特定されているためです。つまりオブジェクトのOID は db4o の管理インフラの中に既に保存されています。従ってdb4o は、set がコールされる時には、新しいオブジェクトを挿入するのではなく、既存のオブジェクトを更新するのだということを知っています。


検索ユーティリティー・メソッド

アプリケーションに固有である主キーの概念は、元々 QBE にはない概念ですが、この概念を適用する価値は十分にあります。ここで必要なものは、IDベースの検索を単純化するためのユーティリティー・メソッドです。このセクションでは、リフレクション API を使うことよって、正しい値を正しいフィールドに入れるソリューションを示し、そのソリューションをさまざまな好みや感覚に合わせて調整する方法を示します。

まず、基本的な前提から始めましょう。私は db4o データベースを持っており、その中には、ある特定の値の一連のフィールドに基づいてクエリーを実行したい型(Person) があります。このメソッドの中で、Class に対して リフレクション API を使い、この型の新しいインスタンスを作成します (デフォルトのコンストラクターを呼び出します)。次に、こうしたフィールドを持つString の配列に対して繰り返しを行い、Class の中の各 Field オブジェクトを取得します。その後、このフィールドのそれぞれの値に対応するオブジェクトの配列に対して繰り返しを行い、そして Field.set() を呼び出し、その値をテンプレート・オブジェクトの中に入れます。

これが終わったら、db4o データベースに対して get() を呼び出し、返された ObjectSet に何らかのオブジェクトが含まれているかどうかをチェックします。これによってメソッドの基本的なアウトラインができます (リスト 6)。

リスト 6. QBE による ID 検索を行うためのユーティリティー・メソッド
import java.lang.reflect.*;
import com.db4o.*;

public class Util
{
    public static boolean identitySearch(ObjectContainer db, Class type,
        String[] fields, Object[] values)
            throws InstantiationException, IllegalAccessException,
                NoSuchFieldException
    {
            // Create an instance of our type
            Object template = type.newInstance();
            
            // Populate its fields with the passed-in template values
            for (int i=0; i<fields.length; i++)
            {
                Field f = type.getDeclaredField(fields[i]);
                if (f == null)
                    throw new IllegalArgumentException("Field " + fields[i] + 
                        " not found on type " + type);
                if (Modifier.isStatic(f.getModifiers()))
                    throw new IllegalArgumentException("Field " + fields[i] + 
                        " is a static field and cannot be used in a QBE query");
                f.setAccessible(true);
                f.set(template, values[i]);
            }
            
            // Do the query
            ObjectSet set = db.get(template);
            if (set.hasNext())
                return true;
            else
                return false;
    }
}

これを見れば明らかなように、このメソッドを、好みに合わせてさまざまに調整することができます。例えば、すべての例外型をキャッチし、ランタイム例外として再スローする、あるいはtrue/false の代わりに ObjectSet そのものを返す、さらには、ObjectSet の内容を含むオブジェクトの配列を返すことさえできます (こうすると、返される配列の長さのチェックが容易になります)。しかしリスト 7 を見れば明らかなように、この使い方は既に示したQBE の基本的なバージョンと比べ、あまり単純にはなっていません。

リスト 7. ユーティリティー・メソッドの実際
// Is Brian already in the database?
if (Util.identitySearch(
    db, Person.class, {"firstName", "lastName"}, {"Brian", "Goetz"}) == false)
{
    db.set(new Person("Brian", "Goetz", 39));
    db.commit();
}

実は、このユーティリティー・メソッドの使いやすさは、保存されたクラスそのものに適用すると明らかになります (リスト 8)。

リスト 8. Person の中でユーティリティー・メソッドを使う
public class Person
{
    // ... as before
    
    public static boolean exists(ObjectContainer db, Person instance)
    {
        return (Util.identitySearch(db, Person.class,
            {"firstName", "lastName"},
            {instance.getFirstName(), instance.getLastName()});
    }
}

あるいは、見つかったインスタンスを返すようにこのメソッドを再度少し変更し、Person インスタンスに OID を適切に関連付ける、といったこともできます。ここで重要なことは、db4o のインフラ上にコンビニエンス・メソッドを作成し、使いやすくできるということです。

このスタイルのクエリーを、ディスクに保存された基礎となるオブジェクトに対してもっと効率的に行う方法としては、db4o の SODA クエリーAPI を使う方法があることに注意してください。ただしこの方法は、少しこの記事の範囲を超えているため、今後の記事で説明することにします。


高度なクエリー

ここまでは、個々のオブジェクト、あるいは特定の基準を満たすオブジェクトに対してクエリーを実行する方法を見てきました。これらの方法によってクエリーの発行はかなり容易になりますが、同時に選択肢が少し制限されます。例えば、lastname が G で始まるすべての Person、あるいは、21 歳を超える年齢のすべての Person を取得する必要があるとしたら、どうすればよいのでしょう。QBE による方法は、こうしたタイプのクエリーではまったくうまくいきません。これは、QBEは比較ではなく、等価比較を行うためです。

昔から、やや複雑という程度の比較でさえ、OODBMS にとっては弱点であり、リレーショナル・モデルや SQL にとっては強みでした。SQL で比較を行うクエリーを発行するのは簡単ですが、同じことをOODBMS で行おうとすると、あまり魅力のない、次のような方法しかありません。

  • すべてのオブジェクトをフェッチし、独自に相対比較を行う。
  • 述部を含めるように QBE API を拡張する。
  • オブジェクト・モデルに対するクエリーに変換される、クエリー言語を作成する。

比較に対する弱み

当然ですが、最初の選択肢を使えるのは最も簡単なデータベースの場合のみです。この方法では、実際に使用できるデータベースのサイズの上限が明らかに制限されるからです。百万個のオブジェクトをフェッチすることは、最も強力なハードウェアでも簡単ではありません。特にネットワーク接続を介する場合にはなおさらです。(ただしOODBMS を非難してそう言うのではありません。ネットワーク接続を介して百万行をフェッチするのは RDBMS サーバーでは可能なことかもしれませんが、それでもやはりネットワークはクラッシュします。)

2 番目の選択肢では QBE による方法の利点が損なわれ、リスト 9 のように奇怪なものになってしまいます。

リスト 9. 述部を持つ QBE
Query q = new Query();
q.setClass(Person.class);
q.setPredicate(new Predicate(
    new And(
        new Equals(new Field("firstName"), "David"),
        new GreaterThan(new Field("age"), 21)
    )));
q.Execute();

この方法では、多少複雑な程度のクエリーであっても、すぐに実行不能になることがかなり容易にわかります。特に、SQL のようなクエリー言語の単純さと比較した場合には、それが明らかです。

3 番目の選択肢では、クエリー言語を作成し、その言語を使ってデータベースのオブジェクト・モデルに対してクエリーを実行します。これまで OODBMS関係の人達は、標準のクエリー言語、Object Query Language、つまり OQL を作成しました。これはリスト 10 のようなものです。

リスト 10. OQL のスニペット
SELECT p FROM Person
WHERE p.firstName = "David" AND p.age > 21

OQL は、表面上は非常に SQL に似ているように見えます。そのため、SQL と同じくらい強力で使いやすいように思えてしまいます。OQL の欠点は、返すものが異なることです。SQLと非常に似ている言語であれば、SQL と同じように列セット (タプル) を返すように思えますが、オブジェクト・データベースはそのような動作はしません。つまりオブジェクト・データベースはオブジェクトを返すのであり、任意のセットを返すわけではありません。特に、強い型付けの言語(C# や Java プログラミング) では、SQL でのセット・ベースの概念とは異なり、返されるオブジェクトの型を事前に知っている必要があるのです。


db4o でのネイティブ・クエリー

db4o は、複雑なクエリー API を開発者に強制する代わりに、あるいは新しい「ナントカQL」を導入する代わりに、ネイティブ・クエリーと呼ばれる機構を提供しています。ネイティブ・クエリーは強力であると同時に、使い方も至って簡単で、これはリスト11 を見るとわかるでしょう。(db4o のためのクエリー API が、SODA クエリーの形で利用できます (SODA クエリーは主に、粒度の細かなクエリー・コントロールに使われます)。しかし、この後すぐにわかるように、SODAが必要になるのは、通常はクエリーを手動で最適化する場合のみです。)

リスト 11. db4o のネイティブ・クエリー
// ... as before
        ObjectContainer db = null;
        try
        {
            db = Db4o.openFile("persons.data");
            
            ...

            // Who wants to get a beer?
            List<Person> drinkers = db.query(new Predicate<Person>() {
                public boolean match(Person candidate) {
                    return person.getAge() > 21;
                }
            }
            for (Person drinker : drinkers)
                System.out.println("Here's your beer, " + person.getFirstName());
        }

このクエリーの「ネイティブ」な部分を見ると、プログラミング言語そのもの (この場合は Java 言語) で書かれており、何か別のものに変換が必要な言語で書かれているのではないことがわかります。(Genericsを使わない Predicate API が Java 5 よりも前のバージョンで使用できますが、あまり使いやすくはありません。)

これを少し考えてみると、この方法をどのようにして正確に実装するのか不思議に思い始める人がいることでしょう。ソース・プリプロセッサーを使って、内部にクエリーを持つソース・ファイルをデータベース・エンジンが理解できる何か(SQL/J や、その他の有名な組み込みプリプロセッサーのようなもの) に変換する必要があるのか、あるいは、データベースがすべての Person オブジェクトをクライアントに返送し、そこで完全なセットに対して述部を実行する (つまり、先ほど拒否した方法そのもの) かのどちらかです。

実は、db4o は、このどちらも行っていません。代わりに db4o の陰で、ネイティブ・クエリーに対して、興味深い、そして革新的な方法がとられています。大まかに言えば、db4oシステムはデータベースに述部を送り、そこで実行時に match() メソッドのバイトコードに対してバイトコード分析を行うのです。もしそのバイトコードが十分理解できるほど容易なものであれば、db4o は効率を高めるために、そのクエリーをSODA クエリーに変換します。この場合には、match() メソッドに渡すためにすべてのオブジェクトをインスタンス化する必要はありません。こうすることで、プログラマーは使い慣れた言語でクエリーを作成すればよく、一方クエリーそのものは、データベースが効率的に理解でき、実行できるものに変換されます。(いわば「JQL」、つまりJava Query Language のようなものです。しかし、db4o 開発者に向かって JQL という名前を使わないでください。私が非難されてしまうことになります。)

必ず BLOAT を含めてください

db4o の Java ディストリビューションには、JDK 1.1、JDK 1.2、Java 5 の各リリースに対するコア db4o 実装を含め、いくつかのjar ファイルが含まれています。このディストリビューションの中には、BLOAT という名前の jar ファイルも含まれています。BLOATはPurdue University で開発された Java バイトコード・オプティマイザーであり、その名前 (膨張という意味) にもかかわらず、ネイティブ・クエリーが動作するためには、db4o-5.0-nqopt.jarと共にランタイムのクラスパスに存在している必要があります。これらのライブラリーを含めなくてもエラーはまったく生成されませんが、どのネイティブ・クエリーも最適化されずに終わります。(開発者達はこれに気付きますが、このセクションで説明するリスナーを使って受け身の形で気付くにすぎません。)

db4o に語らせましょう

ネイティブ・クエリーの方法も、完全ではありません。例えば、バイトコード・アナライザーが役に立たないほど複雑なネイティブ・クエリーを作成してしまい、ワーストケースの実行モデルが必要となることもあり得ます。このワーストケースのシナリオでは、db4oは、データベースの中にあるクエリー対象の型のオブジェクトをすべてインスタンス化し、それらを 1 つずつ match() を実行することで渡す必要があります。当然予想されるように、これによってクエリーのパフォーマンスは大幅に低下します。しかしこれは、必要な場所にリスナーをインストールすることで回避することができます。

直感のみに頼っていては、最適化がうまくいかないことを必ずしも予想できません。それは最適化が失敗する理由と、コードを調べてわかることとが、まったく異なることがあるからです。例えば、コンソール出力文(Java ならば System.out.println、C# なら System.Console.WriteLine) を含めると、.NET バージョンの db4o ではオプティマイザーが失敗しますが、Java バージョンでは問題なく最適化されます。こうした種類のバリエーションを実際に予想することはできません(ただし経験から学ぶことはできます)。そのため、エクストリーム・プログラミングで言われるように、「システムに語らせる (Let The SystemTell You)」ことが賢明なのです。

単純にリスナー (Db4oQueryExecutionListener) を ObjectContainer 自体に対して登録し、ネイティブ・クエリーが最適化できない場合には通知させるようにします (リスト 12)。

リスト 12. DiagnosticListener
// ... as before
        ObjectContainer db = null;
        try
        {
            db = Db4o.openFile("persons.data");
            
            db.ext().configure().diagnostic().addListener(new DiagnosticListener() {
                public void onDiagnostic(Diagnostic d) {
                    if (d instanceof NativeQueryNotOptimized)
                    {
                        // could display information here, but for simplicity
                        // let's just fail loudly
                        throw new RuntimeException("Native query failed optimization!");
                    }
                }
            });
        }

当然ですが、こうしても問題ないのは開発中のみです。実行時には、この失敗を log4j エラー・ストリームにログとして記録するか、あるいは同程度にユーザーの目に付かない何かにログとして記録した方が適切です。


まとめ

「多忙な Java 開発者のための db4o ガイド」シリーズ第 2 回目の今回は、db4o がどのようにオブジェクトを保存し、取得するかを説明するために、またdb4o のネイティブ・クエリー機能を紹介するために、出発点として OODBMS での ID の概念を使いました。

QBE はより簡単に使える API であるため、単純なクエリーに向いた仕組みです。しかし QBE が必要とすることは、ドメイン・オブジェクトが、そのいずれかのフィールドあるいはすべてのフィールドに、ヌルに設定可能なデータを含められることであり、これは一部のドメイン・ルールに違反する可能性があります。例えば、Person オブジェクトの場合は、first name と last name の両方に対してこれを適用できた方が望ましいと言えます。しかし lastname のみに対する QBE クエリーに Person を使うと、first name に対してヌルが許容されてしまいます。これは実質的に、ドメインの制約か、あるいはクエリーの機能のいずれかを選ばなければならないということですが、どちらも完全に受け入れられるものではありません。

ネイティブ・クエリーは複雑なクエリーを実行するための強力な方法であり、そのために新しいクエリー言語を学んだり、述部をモデリングするための複雑なオブジェクト構造を作成したりする必要がありません。そして、db4oのネイティブ・クエリー機能では要求を満たすことができない状況では、SODA API (元々は任意のオブジェクト・システムのためのスタンドアロンのクエリー・システムとして登場したものであり、現在もSourceForge に置かれています) を使うことで、非常に詳細なレベルまでクエリーを調整することができますが、その代わり単純さに欠けるという代償が伴います。

データベースのクエリー方法がこのように多様なため、複雑でわかりにくく、RDBMS の動作とはまったく異なると思われるかもしれません。しかし実際には、そんなことはありません。大部分の大規模なデータベースは、SQL文字列をバイトコード・フォーマットに変換します。こうした SQL は分析され、最適化された後、ディスクに保存されたデータに対して実行され、再度文字列として組み立てられ、そして返されます。db4oのネイティブ・クエリーの方法は、バイトコードへのコンパイルを Java (あるいは C#) コンパイラーの手に戻します。そのためタイプ・セーフが保持され、不正なクエリー構文を早期に見つけることができます。(ところで、残念なことにJDBC による SQL へのアクセスはタイプ・セーフではありません。これは JDBC がコール・レベルのインターフェースであり、実行時にチェックできるストリングに制限されるためです。これは、JDBC以外の、どの CLI でも同じです。ODBC や、.NET の ADO.NET も同じ制約があります。) 最適化は相変わらずデータベース内部で行われますが、テキストが返される代わりに実際のオブジェクトが送り返され、そのまま使うことができます。これは、SQL/Hibernateあるいは他の ORM の方法とは非常に対照的です。Esther Dyson はこれを、次のような有名な言い方で表現しています。

テーブルを使ってオブジェクトを保存するのは、車を運転して家に帰り、そして車を分解してガレージに入れるようなものです。翌朝、また組み立てることはできますが、これが車を停めておく方法として最も効率が良いのか、やがて自問したくなるものです。

まったくそのとおりです。また次回お会いしましょう。

参考文献

学ぶために

製品や技術を入手するために

  • Java のためのオープンソースのオブジェクト・データベース、db4o をダウンロードしてください。

議論するために

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Java technology, Open source
ArticleID=250268
ArticleTitle=多忙な Java 開発者のための db4o ガイド: クエリー、更新、そして ID
publish-date=03272007