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)
といったあたりだと思われます.
引数のsql
はSQL文字列,args
はSQL文字列に含まれるパラメータ(バインド変数)の値の配列,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)
ってところかな.
引数のsql
はSQL文字列,args
はSQL文字列に含まれるパラメータ(バインド変数)の値の配列,argTypes
はargs
のJDBC型(PreparedStatement#setObject(int, Object, int)
に渡される)の配列です.
この他,execute()
,call()
,batchUpdate()
などが用意されています.このあたりは必要に応じてということで.
JdbcTemplate
は,次のプロパティを持っています.
ignoreWarnings
nativeJdbcExtractor
そんなに使う機会があるとは思えませんが,前者はboolean
のプロパティで,false
に設定されるとStatement
等にSQLWarning
が設定された場合にそれをスローしてくれます.うーむ,SQLWarning
をチェックしたことってないなぁ.心より恥じる.
後者はNativeJdbcExtractor
というinterface
型のプロパティです.どういうものかというと,例えばCommons DBCPなどのDataSource
はConnection
のラッパーを提供することでclose()
が呼ばれたときにそのConnection
をプールに戻すとかするわけですが,そのラッパーではなく,本当のConnection
を取得する手段を提供するというものらしいです.
JdbcTemplate
のメソッドには,PreapredStement
やResultSet
の扱い方を別途指定できるものがあり,それらと組み合わせるとJDBCドライバ固有の機能を活用できたりするということみたいです.
そういえば昔(JDBC 1.0の頃)はフェッチサイズを設定する標準APIがなくて,Oracle JDBCドライバの固有機能を使ったことがあります.そういう場合に使えるということでしょう.凝ってるなぁ.ダイエット...
それからもう一つ重要なことが.「実践J2EEシステムデザイン(ISBN:4797322888)」を読んだ人ならご存知のとおり,Rod Johnson氏はSQLException
がcheckedな例外であることが許せないようです.ということで当然,JdbcTemplate
はSQLException
を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
かなぁ?
おっと,残業申請が必要な時間までに終わった.よかった.