Spring Framework 入門記 JDBCその7 StoredProcedure前編
遅い時間になってしまいましたが,一応は平日の日課なので,自宅からの入門記です.
SqlQuery
,SqlUpdate
に続いて,今回は
StoredProcedure
です.StoredProcedure
というくらいですから,ストアドプロシジャ/ファンクションを実行するためのクラスです.で,内部ではJDBCのCallableStatement
を使っているようです.
恥ずかしながら私,CallableStatement
を使ったことがありません.心より恥じる.というのもですね,Oracleだとかなり昔(JDBC2.0になる前)からPreparedStatement
でストアドが呼び出せていたからなんですね.うーみゅ,だからといってさぼってちゃいかんなぁ.
そんなわけでStoredProcedure
ですが,例によって
-
dataSource
sql
types
といったプロパティを持っています.
ただし,sql
プロパティに設定するのはストアドの名前だけです.CallableStatement
を使う場合,
{?= call <procedure-name>[<arg1>,<arg2>, ...]}
みたいなエスケープ構文が必要っぽいですが,このような文字列を組み立てるのはStoredProcedure
がやってくれます.なので,sql
プロパティには上記の<procedure-name>
の部分だけを渡せばいいようです.
上記プロパティに加えて,
-
function
というboolean
のプロパティが用意されています.ストアドファンクションから戻り値を得る場合はこのプロパティにtrue
を設定する必要があります.デフォルトはfalse
です.
それから,RdbmsOperation
から継承したtypes
プロパティですが,ストアドのパラメータにOUT
なものがある場合は使えません.そんな場合は,
-
void declareParameter(SqlParameter param)
を使ってパラメータの情報を与える必要があるみたい.っていうか,setTypes(int[])
は中でdeclareParameter(SqlParameter)
を呼び出しているんですね.ということは,前回作ったJdbcType
やそのPropertyEditor
なんて必要なくて,SqlParameter
のPropertyEditor
を作るべきだったようです.心より恥じる.
それはともかくですね,ストアドのパラメータの数分だけ,declareParameter(SqlParameter)
を呼び出すわけですが,ストアドの引数がIN
かOUT
かによって渡すインスタンスの型を変える必要があるようです.
それからもしかすると,結果セットを返すOUT
パラメータの場合はSqlReturnResultSet
のインスタンスを渡す必要があるかもしれません.いや,SqlOutParameter
も結果セット扱えるみたいだが... よくわかりません.心より恥じる.
さらに,INOUT
の場合はどうるんだろう? それもよくわかりません... 心より恥じる.恥じりまくりだなぁ.
ということで
SqlParameter
ですが,これは,次のプロパティを持っています.
-
name
sqlType
typeName
SqlParameter
はイミュータブルなので,これらはコンストラクタで設定します.
次に
SqlOutParameter
ですが,これはSqlParameter
の派生クラスなので,上記のプロパティも持っています.それに加えて,コンストラクタで
RowCallbackHandler
RowMapper
のいずれかを設定することができます.でも,たいていは意識しなくていいみたい.たぶん... 無念だ.
という具合になんかとっても大変そうなのですが,要はsql
プロパティにストアドの名前を設定して,戻り値があればfunction
プロパティをtrue
にして,ストアドのパラメータに応じてdeclareParameter(SqlParameter)
を呼び出せば,準備完了です... 完了なのは準備だけですか.無念だ.
ということで,準備ができたらストアドの呼び出しです.それには,
-
Map execute(Map inParams)
Map execute(ParameterMapper inParamMapper)
のいずれかを使います.
execute(Map)
の場合,declareParameter(SqlParameter)
で設定したSqlParameter
のname
プロパティの値をキーとしてinParams
から取得した値がパラメータの値になります.うーむ,今の文,日本語としてどうよ? 後で呼んだらわけわかだよなぁ.
execute(ParameterMapper)
の場合は,ParameterMapper
のcreateMap(Connection)
が返すMap
が使われるだけで,execute(Map)
とあまり変わらないような? Map
作るのにConnection
が必要な場合って? 想像が付きません.引数にCallableStatement
が渡されて,好きに設定できるのなら分かるんですが.うーみゅ.
いかんなぁ,今日は書いていてイマイチ理解が進まない感じ.いいんだい,そんな場合は手を動かすのさ! ということで,お試しタぁイム.
「Reference Documentation」の「9.4.4. StoredProcedure」では,OracleのSYSDATE
を呼び出すようなStoredProcedure
の派生クラス(しかも内部クラスだ)を作っています.そう,またしてもStoredProcedure
は抽象クラスなんですねぇ.でも,その派生クラスでやっていることって,プロパティの設定とdeclareParameter(SqlParameter)
の呼び出しくらいなんですよね.それくらいだったら,箱から出してすぐ使えるようにしておいてくれよぉ〜.
ということで,前回同様すぐに使えるStoredProcedure
の派生クラスを作ってみましょう.ということでまずは,SqlParameter
およびSqlOutParameter
のPropertyEditor
です.ちょっと悩んだのですが,これらの文字列表現を
IN,name,type OUT,name,type
ということにして,前者ならSqlParameter
,後者ならSqlOutParameter
のインスタンスを返すことにします.
ということで,こんな感じ.
package study; import java.beans.PropertyEditorSupport; import java.lang.reflect.Field; import java.sql.Types; import org.springframework.jdbc.core.SqlOutParameter; import org.springframework.jdbc.core.SqlParameter; public class SqlParameterEditor extends PropertyEditorSupport { public void setAsText(String text) throws IllegalArgumentException { String[] tokens = text.split(","); if (tokens.length != 3) { throw new IllegalArgumentException("Could not parse SqlParameter : " + text); } String kind = tokens[0]; String name = tokens[1]; String typeName = tokens[2]; try { Field field = Types.class.getField(typeName); int type = field.getInt(null); if ("IN".equalsIgnoreCase(kind)) { setValue(new SqlParameter(name, type, typeName)); } else if ("OUT".equalsIgnoreCase(kind)) { setValue(new SqlOutParameter(name, type, typeName)); } else { throw new IllegalArgumentException("Illegal IN/OUT type : " + kind); } } catch (Exception e) { throw (IllegalArgumentException) new IllegalArgumentException("Illegal jdbc type name : " + typeName).initCause(e); } } public String getAsText() { SqlParameter sqlParameter = (SqlParameter) getValue(); if (sqlParameter instanceof SqlOutParameter) { return "OUT" + "," + sqlParameter.getTypeName() + "," + sqlParameter.getName(); } else { return "IN" + "," + sqlParameter.getTypeName() + "," + sqlParameter.getName(); } } }
このPropertyEditorをfactory.xmlに組み込みます.このあたりの話はまたしても「Property Editorだよ」を参照.
そして,StoredProcedure
の派生クラス.
package study; import javax.sql.DataSource; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.SqlParameter; import org.springframework.jdbc.object.StoredProcedure; public class MyStoredProcedure extends StoredProcedure { public MyStoredProcedure() { super(); } public MyStoredProcedure(DataSource ds, String name) { super(ds, name); } public MyStoredProcedure(JdbcTemplate jdbcTemplate, String name) { super(jdbcTemplate, name); } public void setSqlParameters(SqlParameter[] parameters) { for (int i = 0; i < parameters.length; ++i) { declareParameter(parameters[i]); } } }
うーみゅ,またしてもMy〜
にしてしまった.心より恥じる.←もはや句読点並大安売り.
そして定義ファイル.
<?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="storedProcedure" class="study.MyStoredProcedure"> <property name="dataSource"> <ref bean="dataSource"/> </property> <property name="sql"> <value>CURRENT_DATE</value> </property> <property name="function"> <value>true</value> </property> <property name="sqlParameters"> <list> <value>OUT,date,DATE</value> </list> </property> </bean> </beans>
HSQLDBなのでSYSDATE
ではなくCURRENT_DATE
を使ってみました.
で,実行用のクラス.
package study; import java.util.HashMap; import java.util.Map; 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; import org.springframework.jdbc.object.StoredProcedure; public class Main { public static void main(String[] args) { try { BeanFactoryLocator locator = ContextSingletonBeanFactoryLocator.getInstance(); BeanFactoryReference ref = locator.useBeanFactory("context"); ApplicationContext context = (ApplicationContext) ref.getFactory(); StoredProcedure storedProcedure = (StoredProcedure) context.getBean("storedProcedure"); Map result = storedProcedure.execute(new HashMap()); System.out.println(result); ref.release(); } catch (Throwable e) { e.printStackTrace(); } } }
これを実行!!
java.sql.SQLException: This function is not supported at org.hsqldb.Trace.getError(Unknown Source) at org.hsqldb.Trace.error(Unknown Source) at org.hsqldb.jdbcPreparedStatement.getNotSupported(Unknown Source) at org.hsqldb.jdbcPreparedStatement.registerOutParameter(Unknown Source) at org.springframework.jdbc.core.CallableStatementCreatorFactory$CallableStatementCreatorImpl.createCallableStatement(CallableStatementCreatorFactory.java:183) at org.springframework.jdbc.core.JdbcTemplate.execute(JdbcTemplate.java:520) at org.springframework.jdbc.core.JdbcTemplate.call(JdbcTemplate.java:549) at org.springframework.jdbc.object.StoredProcedure.execute(StoredProcedure.java:102) at study.Main.main(Main.java:19)
ぐはぁっ,何が起きたわけ?
うーみゅ,どうやらHSQLDBの1.7.1ではCallableStatement
はまともに実装されていないっぽい.
しょうがないので1.7.2RC5で再度実行!!!!
java.sql.SQLException: Unknown JDBC escape sequence: { at org.hsqldb.jdbc.jdbcConnection.nativeSQL(Unknown Source) at org.hsqldb.jdbc.jdbcPreparedStatement.(Unknown Source) at org.hsqldb.jdbc.jdbcCallableStatement. (Unknown Source) at org.hsqldb.jdbc.jdbcConnection.prepareCall(Unknown Source) at org.seasar.extension.dbcp.impl.ConnectionWrapperImpl.prepareCall(ConnectionWrapperImpl.java:115) at org.springframework.jdbc.core.CallableStatementCreatorFactory$CallableStatementCreatorImpl.createCallableStatement(CallableStatementCreatorFactory.java:150) at org.springframework.jdbc.core.JdbcTemplate.execute(JdbcTemplate.java:520) at org.springframework.jdbc.core.JdbcTemplate.call(JdbcTemplate.java:549) at org.springframework.jdbc.object.StoredProcedure.execute(StoredProcedure.java:102) at study.Main.main(Main.java:19)
ぐはぁっ,今度は何だっていうわけっ!?
うーみゅ,StoredProcedure
は
{? = call CURRENT_DATE()}
というSQL文字列を組み立ててprepareCall(String)
を呼び出すのですが,HSQLDBはそれを理解できないらしい... 使えないじゃん!
それとも自分が間違っているのかなぁ?
よくわからないので,明日職場のOracleで再度挑戦することにします.なので今日は前編,明日の後編に続きます.
心より恥じる.
2004/05/13 追記
Oracleで試してみたところ,SqlParameterEditor
に問題があることが分かりました.無念だ.
SqlParameter
のtypeName
って,オブジェクト型など構造化型の場合の型名を示すものなのですね.なので,普通のTIMESTAMP
などの場合はnull
にしておかなければいけないようで,見事にはまってしまいました.心より恥じる.
そこを修正してsql
プロパティを"SYSDATE
"にしたところ,すんなり動きました.
とはいえSYSDATE
だけではイマイチな感じもするわけで,大して変わらないけれど
<bean id="storedProcedure" class="study.MyStoredProcedure"> <property name="dataSource"> <ref bean="dataSource"/> </property> <property name="sql"> <value>DBMS_DEBUG.PROBE_VERSION</value> </property> <property name="function"> <value>false</value> </property> <property name="sqlParameters"> <list> <value>OUT,major,INTEGER</value> <value>OUT,minor,INTEGER</value> </list> </property> </bean>
として実行すると
{minor=4, major=2}
という感じになりました.
おしまい.