Spring Framework 入門記 JDBCその1 JdbcTemplate

今日からは,「Chapter 9. Data Access using JDBC」に突入です.
SpringのJDBCサポートは,4つのパッケージから成り立っているそうです.

core
JDBC呼び出しのテンプレートなどを提供する.
datasource
DataSourceの実装クラスを提供する.
object
なんだかよく分からない... 心より恥じる.
suppourt
SQL例外から階層化されたunchecked例外への変換などを提供する.

ということで今回は,core パッケージのテンプレートというヤツを学習します.
ここでいうテンプレートとは,デザパタのテンプレートメソッドパターンのことです.JDBC APIを呼び出すというと,お決まりの作法があったりしますよね.こんな感じとか.

Statement st = conn.createStatement(sql);
try {
    st.execute();
}
finally {
    st.close();
}

Springでは,こういう決まりきった作法をテンプレートメソッドとしたクラスを用意してくれていて,それが

  • JdbcTemplate

というクラスです.
このクラス,メソッドがやたらとたくさんあるので,とても一つ一つ丁寧に見ていこうという気にはなれません.軽〜く眺める程度にしましょう.
まず問合せ系のメソッドですが,query〜()というメソッドが16個(publicのみ)も用意されてるのですが,よく使うのはこのうちの半分くらいで,

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

といったあたりだと思われます.
引数のsqlSQL文字列,argsSQL文字列に含まれるパラメータ(バインド変数)の値の配列,requiredTypeは期待する戻り値の型です.最後のrequiredTypeですが,指定した型と実際に取得した結果の型が合わない場合には例外がスローされます.別に適切に変換してくれるというわけではありません.無念だ.
メソッド名のXxxの部分はメソッドの戻り値の型を示し,次のものが用意されています.

  • Object
  • Int
  • Long
  • List

このうち,Listを返すもの以外は,問合せ結果が一行でないと例外が吹っ飛んできます.また,Listは実際にはArrayListです.
JdbcTemplateには他にも,PreparedStatementの扱いをカスタマイズできるような問合せメソッドなどが用意されていますが,ここでは省略します.


次に更新系のメソッドですが,こちらは5個(publicのみ)用意されています.そのうちよく使いそうなのは,

  • update(String sql)
  • update(String sql, Object[] args)
  • update(String sql, Object[] args, int[] argTypes)

ってところかな.
引数のsqlSQL文字列,argsSQL文字列に含まれるパラメータ(バインド変数)の値の配列,argTypesargsJDBC型(PreparedStatement#setObject(int, Object, int)に渡される)の配列です.
この他,execute()call()batchUpdate()などが用意されています.このあたりは必要に応じてということで.


JdbcTemplateは,次のプロパティを持っています.

  • ignoreWarnings
  • nativeJdbcExtractor

そんなに使う機会があるとは思えませんが,前者はbooleanのプロパティで,falseに設定されるとStatement等にSQLWarningが設定された場合にそれをスローしてくれます.うーむ,SQLWarningをチェックしたことってないなぁ.心より恥じる.
後者はNativeJdbcExtractorというinterface型のプロパティです.どういうものかというと,例えばCommons DBCPなどのDataSourceConnectionのラッパーを提供することでclose()が呼ばれたときにそのConnectionをプールに戻すとかするわけですが,そのラッパーではなく,本当のConnectionを取得する手段を提供するというものらしいです.
JdbcTemplateのメソッドには,PreapredStementResultSetの扱い方を別途指定できるものがあり,それらと組み合わせるとJDBCドライバ固有の機能を活用できたりするということみたいです.
そういえば昔(JDBC 1.0の頃)はフェッチサイズを設定する標準APIがなくて,Oracle JDBCドライバの固有機能を使ったことがあります.そういう場合に使えるということでしょう.凝ってるなぁ.ダイエット...


それからもう一つ重要なことが.「実践J2EEシステムデザイン(ISBN:4797322888)」を読んだ人ならご存知のとおり,Rod Johnson氏はSQLExceptionがcheckedな例外であることが許せないようです.ということで当然,JdbcTemplateSQLExceptionをuncheckedな例外に変換してくれます.


普通に使うのに必要なのはこんなところかなぁ? さっき書いたようにいろいろカスタマイズするなら豊富なinterfaceを見るべきなのですが,あまり使いそうな気がしないし... いずれ気が向いたらということで.心より恥じる.


ということで実験コーナーなのですが,JDBC編である以上,データベースを使いまくるわけです.ということは,トランザクションも使いまくるわけです.そこで,そのあたりの設定を個別に用意することにしましょう.
まず,S2JTAとついでにTraceInterceptorの定義ファイルをs2.xmlとして準備.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans
    PUBLIC "-//SPRING//DTD BEAN//EN"
    "http://www.springframework.org/dtd/spring-beans.dtd"
>
<beans>
    <bean id="userTransaction"
        class="org.seasar.extension.jta.TransactionManagerImpl"
    />

    <bean id="xaDataSource"
        class="org.seasar.extension.dbcp.impl.XADataSourceImpl">
        <property name="driverClassName">
            <value>org.hsqldb.jdbcDriver</value>
        </property>
        <property name="URL">
            <value>jdbc:hsqldb:.</value>
        </property>
        <property name="user">
            <value>sa</value>
        </property>
        <property name="password">
            <value></value>
        </property>
    </bean>

    <bean id="connectionPool" destroy-method="close" 
        class="org.seasar.extension.dbcp.impl.ConnectionPoolImpl">
        <property name="transactionManager">
            <ref bean="userTransaction"/>
        </property>
        <property name="XADataSource">
            <ref bean="xaDataSource"/>
        </property>
    </bean>

    <bean id="dataSource"
        class="org.seasar.extension.dbcp.impl.DataSourceImpl">
        <constructor-arg>
            <ref bean="connectionPool"/>
        </constructor-arg>
    </bean>

    <bean id="trace"
        class="org.seasar.framework.aop.interceptors.TraceInterceptor"
    />
</beans>

これで,いつでもS2JTAを使うことができます.本当はデータソースのプロパティは可変にしておくべきなのでしょうが,ちょっと面倒なので...
次に,Springのトランザクションマネージャの定義ファイルをtransaction.xmlとして用意.メタデータによる宣言的トランザクションを使うことにしました.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans
    PUBLIC "-//SPRING//DTD BEAN//EN"
    "http://www.springframework.org/dtd/spring-beans.dtd"
>
<beans>
    <bean id="transactionManager"
        class="org.springframework.transaction.jta.JtaTransactionManager">
        <property name="userTransaction">
            <ref bean="userTransaction"/>
        </property>
    </bean>
    
    <bean id="attributes"
        class="org.springframework.metadata.commons.CommonsAttributes"
    />

    <bean id="attributeSource"
        class="org.springframework.transaction.interceptor.AttributesTransactionAttributeSource">
        <constructor-arg>
            <ref bean="attributes"/>
        </constructor-arg>
    </bean>

    <bean id="transactionInterceptor" 
        class="org.springframework.transaction.interceptor.TransactionInterceptor">
        <property name="transactionManager">
            <ref bean="transactionManager"/>
        </property>
        <property name="transactionAttributeSource">
            <ref bean="attributeSource"/>
        </property>
    </bean>
</beans>

これらを取り込むために,久しぶりにbeanRefContext.xmlを修正します(それ何だっけ? と思った人は「邪悪なSingleton」を見ましょう).

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
    <bean id="context" class="org.springframework.context.support.ClassPathXmlApplicationContext">
        <constructor-arg>
            <list>
                <value>factory.xml</value>
                <value>s2.xml</value>
                <value>transaction.xml</value>
                <value>beans.xml</value>
            </list>
        </constructor-arg>
    </bean>
</beans>

これでトランザクションを使うのが少しは楽になるはず.でもメタデータを使うことがいいのかは疑問の余地あり.


さてここからが本題.
JdbcTemplateを使ってみましょう.例によって出来のいい題材が浮かばないので,トランザクション編で使った例をJdbcTemplateを使うように修正することにします.せっかくなので,レコード数を取得するメソッドを追加しました.ついでにトランザクションのコミットとかロールバックはもう関心がないので,そのあたりはなくしちゃいます.
こんな感じ.

package study;
import javax.sql.DataSource;
import org.springframework.jdbc.core.JdbcTemplate;

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 String queryValue(String key) {
        JdbcTemplate jt = new JdbcTemplate(dataSource);
        return (String) jt.queryForObject("select value from pair where key=?", new Object { key }, String.class);
    }
    /**
     * @@org.springframework.transaction.interceptor.DefaultTransactionAttribute()
     */
    public int queryCount() {
        JdbcTemplate jt = new JdbcTemplate(dataSource);
        return jt.queryForInt("select count(*) from pair");
    }
    /**
     * @@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 });
    }
    /**
     * @@org.springframework.transaction.interceptor.DefaultTransactionAttribute(readOnly=true)
     */
    public void update(String key, String value) {
        JdbcTemplate jt = new JdbcTemplate(dataSource);
        jt.update("update pair set value=? where key=?", new Object[] { value, key });
    }
}

このように,throwsを書かなくても済むようになりました.いいねぇ.
それから,こいつの定義ファイル.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans
    PUBLIC "-//SPRING//DTD BEAN//EN"
    "http://www.springframework.org/dtd/spring-beans.dtd"
>
<beans>
    <bean id="autoProxyCreator" 
        class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
        <property name="beanNames">
            <value>foo</value>
        </property>
        <property name="interceptorNames">
            <value>transactionInterceptor,trace</value>
        </property>
    </bean>

    <bean id="foo" class="study.Foo">
        <property name="dataSource">
            <ref bean="dataSource"/>
        </property>
    </bean>
</beans>

で,これを実行するクラス.

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");
            System.out.println(foo.queryValue("Yuri"));
            foo.insert("Akiko", "Yada");
            System.out.println(foo.queryValue("Akiko"));
            System.out.println(foo.queryCount());

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

せっかく作ったFoo#update()を読んでいなかったりします.心より恥じる.
ともかく,実行.

 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 {18885993}. Name is HSQL Database Engine
 END study.Foo#insert(Yuri, Ebihara) : null
 - Initiating transaction commit
 BEGIN study.Foo#queryValue(Yuri)
 - Looking up default SQLErrorCodes for DataSource
 - Database product name found in cache {18885993}. Name is HSQL Database Engine
 END study.Foo#queryValue(Yuri) : Ebihara
 - Initiating transaction commit
 Ebihara
 BEGIN study.Foo#insert(Akiko, Yada)
 - Looking up default SQLErrorCodes for DataSource
 - Database product name found in cache {18885993}. Name is HSQL Database Engine
 END study.Foo#insert(Akiko, Yada) : null
 - Initiating transaction commit
 BEGIN study.Foo#queryValue(Akiko)
 - Looking up default SQLErrorCodes for DataSource
 - Database product name found in cache {18885993}. Name is HSQL Database Engine
 END study.Foo#queryValue(Akiko) : Yada
 - Initiating transaction commit
 Yada
 BEGIN study.Foo#queryCount()
 - Looking up default SQLErrorCodes for DataSource
 - Database product name found in cache {18885993}. Name is HSQL Database Engine
 END study.Foo#queryCount() : 2
 - Initiating transaction commit
 2

なんか,始めのほうでへんちくりんなメッセージが出てますが,SQLException#getSQLState()からuncheckedな例外に変換するための情報を読み込んだっていうメッセージですね.HSQLDBも入ってる.ちゃんと選択されてる.
それにしてもメッセージがくどいぞ>Spring JDBC
SpringのDebugInterceptorからS2のTraceInterceptorにしたので,そこは少し見やすくなりました.


ところで最近,S2JDBCもリリースされましたが,あちらはコンポーネントなんですよね.でもJdbcTemplateコンポーネントではなくて,単なるユーティリティ.このあたりにもコンセプトの違いが表れている?
JdbcTemplateを使ってS2JDBC風のコンポーネントを用意することも簡単だと思うので,この先学習していってもそれらしいものが出てこなかったらパクって作ってみようかな.


ということで今日はここまで.次は冒頭でよく分からないと書いたobjectかなぁ?
おっと,残業申請が必要な時間までに終わった.よかった.