Spring Framework 入門記 JDBCその2 JdbcTemplate#query

まずは落ち穂拾いから.
前回の内容に間違いがありました.無念だ.
JdbcTemplateのお手軽問い合わせメソッドについてですが,これらは戻り値がObjectのものとそれ以外でメソッドのシグネチャが異なります.まずObject以外については,

    • queryForXxx(String sql)
    • queryForXxx(String sql, Object[] args)

の2種類があり,Xxxの部分はIntLongListの3種類があります.
一方Objectを返すメソッドとしては,

    • queryForObject(String sql, Class requiredType)
    • queryForObject(String sql, Object[] args, Class requiredType)

の2種類があります.
ちょっと混乱していました.心より恥じる.


気を取り直して,ここから今日の学習です.
今回もJdbcTemplateの学習を続けます.まだ見ていないことが結構あるようなので.
前回書いたように,JdbcTemplateは豊富なメソッドを持っています.publicな問い合わせメソッドだけでも16個もあります.これらをもう少し詳しく見ていくことにします.
前回は,queryForInt(String)のような使いやすそうなメソッドを見たわけですが,それではすまない場合も多いでしょう.そのような場合に備えて,JdbcTemplateの問い合わせメソッドは様々にカスタマイズできるようになっています.


まずは,ResultSetの扱いをカスタマイズする方法を見てみましょう.それには,

  • RowCallbackHandler

というinterfaceimplementsしたclassを用意して,JdbcTemplate

    • List query(String sql, RowCallbackHandler rch)

などに渡します.
このRowCallbackHandlerResultSetを扱うためのもので,結果セットの行ごとにJdbcTemplateからコールバックされる,次のメソッドを持っています.

    • void processRow(ResultSet rs)

引数でResultSetを渡されますが,そのnext()を呼び出すのはJdbcTemplateの役割です.
ところで,このメソッドはvoidなんですね.行を処理した結果はどうするの?
このあたり,ちょっとイマイチな感じなのですが,実はRowCallbackHandlerの実装クラスが

  • ResultReader

というinterfaceimplementsしていれば,そのメソッドである

    • List getResults()

を通じてListを返すことができます.ということは,自分でListを用意するのですね.てっきり,行ごとのインスタンスを返せば,それをJdbcTemplateListに加えてくれるのだと思っちゃいました.無念だ.
なお,RowCallbackHandlerの実装クラスがResultReaderimplementsしていなければ,query(String, RowCallbackHandler)などのメソッドはnullを返します.
ということで,事実上RowCallbackHandlerResultReaderは同時にimplementsする必要がありますね.


これで十分とは限りません.結果をList以外で取得したい場合もあるでしょう.そんな場合でも,JdbcTemplateを利用することができます.それには,

  • ResultSetExtractor

というinterfaceimplementsしたclassを用意して,JdbcTemplate

    • Object query(String sql, ResultSetExtractor rse)

などに渡します.
このResultSetExtractorは,文字通りResultSetを展開するもので,次のメソッドを持っています.

    • Object extractData(ResultSet rs)

このメソッドは,RowCallbackHandler#processRow(ResultSet)と異なり,問い合わせごとに一回だけ呼び出されます.ResultSet#next()を呼び出すのもこのメソッドの役割になります.


ということで,ResultSetをどう扱うかで,JdbcTemplateのメソッドを次のように使い分ければいいようです.

List queryForList(String sql)
問い合わせ結果をMapListで欲しい場合に使う.
List query(String sql, RowCallbackHandler)
問い合わせ結果を任意の型のListで欲しい場合に使う.
Object query(String sql, ResultSetExtractor
問い合わせ結果を任意の型で欲しい場合に使う.

ということになります.


さて,カスタマイズしたくなるのはResultSetの扱いだけとは限りません.もしかすると,PreparedStatementの扱い方もカスタマイズしたくなるかもしれません.
例えばObjectの配列を作りたくない場合.そんな場合は,

  • PreparedStatementSetter

というinterfaceimplementsしたclassを用意して,JdbcTemplate

    • Object query(String sql, PreparedStatementSetter pss, RowCallbackHandler)

などに渡します.
このinterfaceは次のメソッドを持っています.

    • void setValues(PreparedStatement ps)

ここで,好きなようにPreparedStatementにパラメータを設定できます.
それでも十分ではない場合もあるかもしれません.例えばPreparedStatementをキャッシュしたりとか.そんな場合は,

  • PreparedStatementCreator

というinterfaceimplementsしたclassを用意して,JdbcTemplate

    • Object query(PreparedStatementCreator psc, RowCallbackHandler)

などに渡します.
このinterfaceは次のメソッドを持っています.

    • PreparedStatement createPreparedStatement(Connection conn)

ここで,好きなようにPreparedStatementを用意して返せばいいわけですね.


ということで,PreparedStatementをどう扱うかで,JdbcTemplateのメソッドを次のように使い分ければいいようです.

List query(String sql, Object[] args, RowCallbackHandler rch)
パラメータをObjectの配列で渡せる場合に使う.
List query(String sql, PreparedStatementSetter pss, RowCallbackHandler rch)
パラメータをObjectの配列で渡したくない場合に使う.
List query(PreparedStatementCreator psc, RowCallbackHandler rch)
PreparedStatementの取得方法をカスタマイズしたい場合に使う.

なお,これらのメソッドの第3引数は,ResultSetExtractorを受け取るものも用意されています.
ということで,最強(?)の問い合わせメソッドは

    • Object query(PreparedStatementCreator psc, ResultSetExtractor rse)

です.自分でPreparedStatementを用意して,自分でResultSetを処理します.
うーむ,それでもJdbcTemplateを使うメリットって?
おぉ,そうだ! 例外がSQLExceptionじゃなくなるんだ! 後は... そうだ! ResultSet#close()を自分で呼び出さなくてもいいんだ!


さて,こんな感じでいろいろなメソッドがあるわけですから,一通り使うべきだという気持ちもないわけではないのですが,あまりに細かいのばかりなので,メソッド一つだけ使うことで済ませたいと思います.心より恥じる.
前回のサンプルで使っているテーブルは,keyvalueという文字列型のカラムを持っています.ということで,このテーブルの問い合わせ結果をPropertiesにして返すということをやってみましょう.
まずはいつものFoo.例によってメタデータによる宣言的トランザクションを使います.

package study;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Properties;
import javax.sql.DataSource;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.ResultSetExtractor;

public class Foo {
    private DataSource dataSource;
    public DataSource getDataSource() {
        return dataSource;
    }
    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }
    /**
     * @@org.springframework.transaction.interceptor.DefaultTransactionAttribute()
     */
    public void createTable() {
        JdbcTemplate jt = new JdbcTemplate(dataSource);
        jt.execute("create table pair (key varchar, value varchar, primary key(key))");
    }
    /**
     * @@org.springframework.transaction.interceptor.DefaultTransactionAttribute(readOnly=true)
     */
    public Properties query() {
        JdbcTemplate jt = new JdbcTemplate(dataSource);
        return (Properties) jt.query("select * from pair", new ResultSetExtractor() {
            public Object extractData(ResultSet rs) throws SQLException {
                Properties props = new Properties();
                while (rs.next()) {
                    props.put(rs.getString("key"), rs.getString("value"));
                }
                return props;
            }
        }
    );
    }
    /**
     * @@org.springframework.transaction.interceptor.DefaultTransactionAttribute(readOnly=true)
     */
    public void insert(String key, String value) {
        JdbcTemplate jt = new JdbcTemplate(dataSource);
        jt.update("insert into pair (key, value) values(?, ?)", new Object[] { key, value });
    }
}

これをAttribute Compilerで処理します.私はいつもこれを忘れて痛い目に遭います.無念だ.
定義ファイルは前回と同じです.
後は実行用クラス.

package study;
import org.springframework.beans.factory.access.BeanFactoryLocator;
import org.springframework.beans.factory.access.BeanFactoryReference;
import org.springframework.context.ApplicationContext;
import org.springframework.context.access.ContextSingletonBeanFactoryLocator;

public class Main {
    public static void main(String[] args) {
        try {
            BeanFactoryLocator locator = ContextSingletonBeanFactoryLocator.getInstance();
            BeanFactoryReference ref = locator.useBeanFactory("context");
            ApplicationContext context = (ApplicationContext) ref.getFactory();

            Foo foo = (Foo) context.getBean("foo");
            foo.createTable();
            foo.insert("Yuri", "Ebihara");
            foo.insert("Akiko", "Yada");
            System.out.println(foo.query());

            ref.release();
        }
        catch (Throwable e) {
            e.printStackTrace();
        }
    }
}

これを実行!!

 BEGIN study.Foo#createTable()
 - Loading XML bean definitions from class path resource [org/springframework/jdbc/support/sql-error-codes.xml]
 - Creating shared instance of singleton bean 'DB2'
 - Creating shared instance of singleton bean 'HSQL'
 - Creating shared instance of singleton bean 'MS-SQL'
 - Creating shared instance of singleton bean 'MySQL'
 - Creating shared instance of singleton bean 'Oracle'
 - Creating shared instance of singleton bean 'Informix'
 - Creating shared instance of singleton bean 'PostgreSQL'
 - SQLErrorCodes loaded: [HSQL Database Engine, Oracle, Microsoft SQL Server, Informix Dynamic Server, PostgreSQL, MySQL, DB2]
 - Looking up default SQLErrorCodes for DataSource
 - Database Product Name is HSQL Database Engine
 - Driver Version is 1.7.1
 END study.Foo#createTable() : null
 - Initiating transaction commit
 BEGIN study.Foo#insert(Yuri, Ebihara)
 - Looking up default SQLErrorCodes for DataSource
 - Database product name found in cache {534353}. Name is HSQL Database Engine
 END study.Foo#insert(Yuri, Ebihara) : null
 - Initiating transaction commit
 BEGIN study.Foo#insert(Akiko, Yada)
 - Looking up default SQLErrorCodes for DataSource
 - Database product name found in cache {534353}. Name is HSQL Database Engine
 END study.Foo#insert(Akiko, Yada) : null
 - Initiating transaction commit
 BEGIN study.Foo#query()
 - Looking up default SQLErrorCodes for DataSource
 - Database product name found in cache {534353}. Name is HSQL Database Engine
 END study.Foo#query() : {Yuri=Ebihara, Akiko=Yada}
 - Initiating transaction commit
 {Yuri=Ebihara, Akiko=Yada}

こんな感じということで.
次はJdbcTemplate#execute()シリーズかなぁ.