Spring Framework 入門記 JDBCその5 SqlQuery

かなーり間が空いてしまって久々の入門記です.
前回までは,JdbcTemplateという随分と低水準のユーティリティクラスを必要以上に眺め続けてしまったのですが,今回からはその上位に構築されている,もう少しコンポーネントらしいものを見ていきます.
まずは問合せ用のコンポーネントである,

  • SqlQuery

です.これは抽象クラスなのですが,次のプロパティを持っています.

    • dataSource
    • sql

本当は他にもjdbcTemplateとかあるのですが,使いそうもないので無視しましょう.
上記のプロパティを設定すると,問合せを実行することが出来ます.そのためのメソッドがこれまた豊富にあるんですが,大雑把に言うと,結果をListで受け取るexecute()の仲間が12個,結果をObjectで受け取るfindObject()の仲間が10個,用意されています.なんていうか,そのぉ,ダイエットしろよ...
とりあえず,execute()の仲間を見ていきましょう.
一番基本的なのは,

    • List execute()

です.これは,問合せSQLにパラメータが含まれていない場合に使うことが出来ます.
それから,パラメータとして1〜2個のintを持つ問合せSQLを実行するために,

    • List execute(int)
    • List execute(int, int)

が用意されています.
さらに,パラメータとして1個のlongStringを持つ問合せSQLを実行するために,

    • List execute(long)
    • List execute(String)

も用意されています.
そして,任意のパラメータを持つ問合せSQLを実行するために,

    • List execute(Object[])

が用意されています.ふーっ.
あって困るものではないものの,最初と最後のだけでも十分な気もするんですが...
これで6個,約半分です.残りの半分はというと,追加のコンテキストとしてMapを渡せるもので,

    • List execute(Map)
    • List execute(int, Map)
    • List execute(int, int, Map)
    • List execute(long, Map)
    • List execute(String, Map)
    • List execute(Object[], Map)

が用意されています.このコンテキストがどういうものかというと... よく分かりません.心より恥じる.
特に明確な使い方が想定されているわけではなくて,もしSqlQueryの派生クラスで必要な情報があれば,このコンテキストを使って渡せるよ,ということだと思います.
findObject()の仲間もほとんど同様です.


さて,SqlQueryexecute()で返すListの要素,あるいはfindObject()が返すObjectがどのようなものかというと,それは実装されていません.抽象クラスなので.
ということで,

  • MappingSqlQuery
  • MappingSqlQueryWithParameters

という派生クラスが用意されています.
用意されているのですが,これもまた抽象クラスです.とほほ... こんなんばっかだなぁ.無念だ.
こいつらはそれぞれ,

    • Object mapRow(ResultSet rs, int rowNum)
    • Object mapRow(ResultSet rs, int rowNum, Object[] parameters, Map context)

という抽象メソッドを持っていて,派生クラスでResultSetを適当なオブジェクトにして返せということみたいです.
うーむ.そこが一番面倒なんだけどなぁ.そこをやってくれるクラスは用意されていないみたい...
さすがSpring,太っていても人に優しくはないのですね.S2だったら本人自らDependency Injectorのひがさんが用意してくれているのに...


ということで,ちょっとは使いやすいSqlQueryの派生クラスを作ってみることにしましょう.
ResultSetのカラムを対応するBeanのプロパティに設定するというのは頻繁に行われていることだと思います.そんなわけで,Apache Jakarta Commons DbUtilsではBeanProcessorというクラスが用意されています.ただしこれ,1.1からのものなので,現在リリース済みの1.0には含まれていません.Nightly BuildsもしくはCVSから持ってくる必要があります.
そのBeanProcessorを使って,プロパティで指定されたBeanクラスのインスタンスを返すようなSqlQueryの派生クラス,BeanSqlQueryです.

package study;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import javax.sql.DataSource;
import org.apache.commons.dbutils.BeanProcessor;
import org.springframework.jdbc.core.ResultReader;
import org.springframework.jdbc.object.SqlQuery;

public class BeanSqlQuery extends SqlQuery {
    private Class beanClass;
    public BeanSqlQuery() {
    }
    public BeanSqlQuery(DataSource ds, String sql) {
        super(ds, sql);
    }
    public Class getBeanClass() {
        return beanClass;
    }
    public void setBeanClass(Class beanClass) {
        this.beanClass = beanClass;
    }
    protected ResultReader newResultReader(int rowsExpected, Object[] parameters, Map context) {
        return new ResultReaderImpl(rowsExpected);
    }
    protected class ResultReaderImpl implements ResultReader {
        private List results;
        private BeanProcessor beanProcessor = new BeanProcessor();
        public ResultReaderImpl(int rowsExpected) {
            this.results = (rowsExpected > 0) ? (List) new ArrayList(rowsExpected) : (List) new LinkedList();
        }
        public void processRow(ResultSet rs) throws SQLException {
            this.results.add(beanProcessor.toBean(rs, beanClass));
        }
        public List getResults() {
            return this.results;
        }
    }
}

これは,beanClassプロパティで指定されたクラスのインスタンスを作成し,それが持つプロパティと一致するカラムの値を設定します.実際にその作業を行うのは,ResultReaderimplementsした内部クラスです.このつくりはMappingSqlQueryのパクリです.てへっ.
ということで,Beanを用意しなくてはなりません.
かなーり手抜きして,プロパティ1つだけのクラスを用意.

package study;

public class Model {
    private String name;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String toString() {
        return name;
    }
}

このModelMVCModelではなくて,雑誌モデルのModelです.心より恥じる.
ということで,おなじみのFooBeanSqlQueryおよびModelを使うように修正.ついでにテーブルもカラム1つだけにしちゃいました.

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

public class Foo {
    private DataSource dataSource;
    private BeanSqlQuery beanSqlQuery;
    public DataSource getDataSource() {
        return dataSource;
    }
    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }
    public BeanSqlQuery getBeanSqlQuery() {
        return beanSqlQuery;
    }
    public void setBeanSqlQuery(BeanSqlQuery beanSqlQuery) {
        this.beanSqlQuery = beanSqlQuery;
    }
    /**
     * @@org.springframework.transaction.interceptor.DefaultTransactionAttribute()
     */
    public void createTable() {
        JdbcTemplate jt = new JdbcTemplate(dataSource);
        jt.execute("create table model (name varchar, primary key(name))");
    }
    /**
     * @@org.springframework.transaction.interceptor.DefaultTransactionAttribute()
     */
    public void insert() {
        JdbcTemplate jt = new JdbcTemplate(dataSource);
        jt.update("insert into model (name) values('Yuri Ebihara')");
        jt.update("insert into model (name) values('Yu Yamada')");
        jt.update("insert into model (name) values('Moe Oshikiri')");
    }
    /**
     * @@org.springframework.transaction.interceptor.DefaultTransactionAttribute(readOnly=true)
     */
    public List getModels() {
        return beanSqlQuery.execute();
    }
}

例によってAttributes Compilerでコンパイルします.
これを使う定義ファイルを用意して,

<?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="beanSqlQuery" class="study.BeanSqlQuery">
        <property name="dataSource">
            <ref bean="dataSource"/>
        </property>
        <property name="sql">
            <value>select name from model</value>
        </property>
        <property name="beanClass">
            <value>study.Model</value>
        </property>
    </bean>

    <bean id="foo" class="study.Foo" autowire="byName">
    </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();
            System.out.println(foo.getModels());

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

これを実行!

 BEGIN study.Foo#createTable()
 - Looking up default SQLErrorCodes for DataSource
 - Database product name found in cache {31578843}. Name is HSQL Database Engine
 END study.Foo#createTable() : null
 - Initiating transaction commit
 BEGIN study.Foo#insert()
 - Looking up default SQLErrorCodes for DataSource
 - Database product name found in cache {31578843}. Name is HSQL Database Engine
 END study.Foo#insert() : null
 - Initiating transaction commit
 BEGIN study.Foo#getModels()
 END study.Foo#getModels() : [Moe Oshikiri, Yu Yamada, Yuri Ebihara]
 - Initiating transaction commit
 [Moe Oshikiri, Yu Yamada, Yuri Ebihara]

うんうん,いい感じ.


ということで,SqlQueryを使えるようになりました.次は更新用のSqlUpdateなのですが,明日は「arton&いがぴょん合コン」,その後週末は「ゴールデンウィークの誓い」消化のため,次回の入門記は週明けになってしまうと思います.心より恥じる.