Spiderの実装 (2)

Spiderを実装するうえで頭を悩ませたのは、細かな違いを吸収し、統一された方法でデータベースにアクセスするためのDatabaseインターフェイスです。

インターフェイスのメソッドを実行するたびにSQLをコンパイルしていたのでは動作が重くなって仕方ありませんので、Connection#prepareStatement()でプリコンパイルしておかなければなりません。このタイミングとして、(1) Databaseの実装オブジェクトを構築するときにプリコンパイルする、(2) 各メソッドを実行するときにプリコンパイルする、という方法が考えられます。効率の面を考えれば、(2)ではメソッドを実行するたびにPreparedStatementのフィールドが初期化されているかを調べなければならないので、(1)の方が優れています。(1)は初期化に時間を要しますが、すべてのメソッドが使われるのならば、総計では変わりありません。nullチェックが入る分、(2)の方が(若干ですが)重くなります。

ただ、効率よりも考えるべきなのは、コードの見やすさです。(1)はコンストラクタ、または初期化メソッドにすべてのSQLを書き込まなければならず、そのSQLを使う部分とは離れてしまうことになります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public Database1 implements Database
{
    public Database1 ()
    {
        this.prepared = connection.prepareStatement("SELECT ...");
        // 初期化するSQLが増えていけば、ここがどんどん伸びていく
    }
    public int getData (long id)
    {
        this.prepared.setLong(1, id);
        ResultSet rs = this.prepared.executeQuery();
        // ...
    }
}

もっとも、エディタの上下分割機能を使えば解決するという問題でもあります。しかし、個人的には、そのような機能を使わずとも、SQLと、そのSQLを利用するコードは近くに配置したいという気持ちです。

一方、(2)の方法を採るのならば、SQLと、そのSQLを利用するコードは近くに配置することができるようになります。

1
2
3
4
5
6
7
8
9
10
11
12
13
public Database2 implements Database
{
    private PreparedQuery prepared;
    public int getData (long id)
    {
        if (this.prepared == null) {
            this.prepared = connection.prepareQuery("SELECT ...");
        }
        this.prepared.setLong(1, id);
        ResultSet rs = this.prepared.executeQuery();
        // ...
    }
}

SQLが遠く離れていると、何番目のパラメータに何のデータを入れればよいのか分かりにくくなってしまいますが、このようなコーディング方法であれば必ず近くに配置されるので分かりやすさが向上します。しかし、この方法では、メソッドが実際に呼ばれるまでSQLがコンパイルされません。そのため、SQLに文法エラーがあるとき、発見が遅れることになります(先にコンパイルしておけばオブジェクト構築時にエラーが検出される)。もっとも、すべてのメソッドについてテストするようにしておけば、これは問題になりません。開発スタイルとしても、ちゃんとテストは行うべきです。次に、毎回nullチェックが入るため、動作速度が(きわめてわずかですが)遅くなります。フィールドは最初に初期化された以降nullになることがないので、このチェックは最初の一回以外すべて意味を持ちません。個人的には、このような無駄が気になって仕方ありません。

以上から、(1)のように、あらかじめSQLをプリコンパイルするようにしつつ、(2)のように、SQLと、そのSQLを使うコードを付近に配置するという方針を考えることにします。

Spiderの実装 (1)

インターフェイスだけ定義してもプログラムは動作しませんので、実装をしなければなりません。

Spiderシステムは情報を蓄積して扱いやすいインターフェイスを提供するためのものですから、情報を蓄積するストレージが必要になります。フリースタイル(自前でディスクへの保存などの機能を実装する)でもいいのですが、効率の良いものを構築する自信はありません。また、かなりの手間がかかるので、完成がいつになるのか分かりません。そこで、リレーショナルデータベースをストレージシステムとして利用することにします。幸いなことに、JavaにはJDBCという仕組みが用意されているので、データベースの操作は比較的に楽です。また、著名なデータベースのJDBCドライバも公開されています。

どのデータベースを利用するか悩むところですが、まだシステムが成熟しておらず、テスト段階であることを考えれば、手軽に使うことのできるものが望ましいといえます。とはいえ、いつまでも簡易なデータベースに頼っているようでは、システムやデータの規模が大きくなってきたときに対応できなくなるおそれがあります。そのため、特定のデータベースに依存しない設計を目指すことにし、最初は扱いやすいデータベース(SQLiteなど)で開発し、徐々に本格的なデータベース(MySQLなど)へ移行していくことにします。具体的には、NetworkやConceptはデータベースを抽象化したオブジェクトを介してデータベースと情報をやり取りします。そして、そのデータベースごとに仲介オブジェクトの実体を実装していきます。

簡略的に記述すれば、次のようになります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package jp.blacksoft.spider_jdbc;
public interface Database
{
    /**
     * @param id
     *            調べるオブジェクトの概念ID
     * @return オブジェクトの作成日時, オブジェクトが存在しなければ{@code null}
     */
    public Date getConceptCreateDate (long id);
    // etc...
}
public class JdbcNetwork implements Network
{
    private final Database objDatabase;
    // etc...
}
public abstract class JdbcConcept implements Concept
{
    // ...
    @Override
    public Date getCreateDate ()
    {
        return this.getDatabase().getConceptCreateDate(this.numConceptID);
    }
    // ...
}

各データベース用の実装クラスでは、Databaseで定義されたインターフェイスに合わせた情報を返すように、各データベースに応じたSQLを記述します。