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を使うコードを付近に配置するという方針を考えることにします。