タイトルの通りだが、PostgreSQL で、OUT パラメータに CURSOR を設定した関数を定義し、MyBatis から実行するとエラーが発生してしまった。
原因となんとか回避する方法を調べてみた。
環境
- Java 11
- PostgreSQL 10
- PostgreSQL JDBC Driver 42.2.5
- MyBatis 3.5.0
PostgreSQL の関数
テーブル定義、関数定義はこんな感じ。
CREATE TABLE test (
id serial,
hoge character(10),
fuga character(10)
);
INSERT INTO test (hoge, fuga) VALUES('hogehoge', 'fugafuga');
INSERT INTO test (hoge, fuga) VALUES('hogege', 'fugaga');
CREATE FUNCTION testfunc (rc_out OUT refcursor) RETURNS refcursor AS $$
BEGIN
OPEN rc_out FOR SELECT * FROM test;
END;
$$ LANGUAGE plpgsql;
普通に Java から実行してみる
CallableStatement
を利用して実行する。クローズ処理は省略。
public class App {
public static void main(String[] args) throws Exception {
HikariDataSource ds = new HikariDataSource();
ds.setDriverClassName("org.postgresql.Driver");
ds.setJdbcUrl("jdbc:postgresql://localhost:5432/postgres");
ds.setUsername("postgres");
ds.setPassword("postgres");
Connection conn = ds.getConnection();
conn.setAutoCommit(false);
CallableStatement cstmt = conn.prepareCall("{call testfunc(?)}");
cstmt.registerOutParameter(1, Types.REF_CURSOR);
cstmt.execute();
ResultSet rs = (ResultSet)cstmt.getObject(1);
while(rs.next()){
System.out.println(rs.getInt(1));
System.out.println(rs.getString(2));
System.out.println(rs.getString(3));
}
}
}
実行結果は以下のような感じ。ちゃんと実行できている。
1
hogehoge
fugafuga
2
hogege
fugaga
MyBatis で実行してみる
実行結果をマッピングするための Test
クラスと、そのラッパーを作る。
@Data
public class Test {
private int id;
private String hoge;
private String fuga;
}
@Data
public class TestWrapper {
private List<Test> test;
}
続いて Mapper をつくる。@Result
は省略しても自動マッピングで上手くいく。
public interface TestMapper {
@Select(value = "{call testfunc(#{test, mode=OUT, jdbcType=CURSOR, resultMap=testMap})}")
@Options(statementType = StatementType.CALLABLE)
@ResultType(Test.class)
@Results(id = "testMap", value = {
@Result(id = true, column = "id", property = "id"),
@Result(column = "hoge", property = "hoge"),
@Result(column = "fuga", property = "fuga")
})
void out(TestWrapper wrapper);
}
以下のようにして実行してみる。
public class App {
public static void main(String[] args) throws Exception {
HikariDataSource ds = new HikariDataSource();
ds.setDriverClassName("org.postgresql.Driver");
ds.setJdbcUrl("jdbc:postgresql://localhost:5432/postgres");
ds.setUsername("postgres");
ds.setPassword("postgres");
Environment env = new Environment("postgres", new JdbcTransactionFactory(), ds);
Configuration config = new Configuration(env);
config.addMapper(TestMapper.class);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(config);
try(SqlSession session = sqlSessionFactory.openSession()){
TestWrapper wrapper = new TestWrapper();
session.getMapper(TestMapper.class).out(wrapper);
System.out.println(wrapper);
}
}
}
実行すると、こんなエラーが発生する。
Exception in thread "main" org.apache.ibatis.exceptions.PersistenceException:
### Error querying database. Cause: org.postgresql.util.PSQLException: CallableStatement 関数が実行され、出力パラメータ 1 は java.sql.Types=2012 型 でした。しかし、java.sql.Types=-10 型 が登録されました。
### The error may exist in com/example/TestMapper.java (best guess)
### The error may involve com.example.TestMapper.out-Inline
### The error occurred while setting parameters
### SQL: {call testfunc(?)}
### Cause: org.postgresql.util.PSQLException: CallableStatement 関数が実行され、出力パラメータ 1 は java.sql.Types=2012 型 でした。しかし、java.sql.Types=-10 型 が登録されました。
エラーの原因を探る
SQL 側は 2012 の型(java.sql.Types.REF_CURSOR
)で返しているけど、受ける側の型が -10 になっていて型が不一致だぞ、ということらしい。
そのため、CallableStatement
に registerOutParameter
で指定している型がおかしいっぽい。
MyBatis はCallableStatementHandler
の registerOutputParameters
メソッドで、ParameterMapping
から取得した JdbcType
の TYPE_CODE
を型として設定している。
CallableStatementHandler.java
そこで、JdbcType.CURSOR
を見てみると、TYPE_CODE
は -10 になっていることがわかった。
JdbcType.java
ということで、原因はここ。
コメントに Oracle と書いてあるので、Oracle は 2012 ではなく -10 をなのだろうか??
Oracle 用で PostgreSQL には対応してないっていうことなのかな?
なんとかして動かしてみる
registerOutParameter
で 2012 を設定できれば動きそうなので、Interceptor で無理やり設定してみる。
http://www.mybatis.org/mybatis-3/ja/configuration.html#plugins
@Intercepts({ @Signature(type = StatementHandler.class, method = "parameterize", args = { Statement.class }) })
public class CursorInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object ret = invocation.proceed();
if(!(invocation.getArgs()[0] instanceof CallableStatement)){
return ret;
}
CallableStatement cstmt = (CallableStatement) invocation.getArgs()[0];
List<ParameterMapping> parameterMappings = ((StatementHandler) invocation.getTarget()).getBoundSql()
.getParameterMappings();
int parameterIndex = 1;
for (ParameterMapping parameterMapping : parameterMappings) {
if ((parameterMapping.getMode() == ParameterMode.INOUT || parameterMapping.getMode() == ParameterMode.OUT)
&& parameterMapping.getJdbcType() == JdbcType.CURSOR) {
cstmt.registerOutParameter(parameterIndex, Types.REF_CURSOR);
}
parameterIndex++;
}
return ret;
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
}
}
JdbcType.CURSOR
が指定された OUT, INOUT パラメータに対して、Types.REF_CURSOR
として登録している。
invocation.proceed()
を呼び出すことで CallableStatementHandler
の処理が実行されるので、事前に実行しておかないと、せっかく設定した値が上書きされてしまう。
再チャレンジしてみる
Interceptor を登録するには Configuration
クラスの addInterceptor
メソッドを利用すればいい。
public class App {
public static void main(String[] args) throws Exception {
HikariDataSource ds = new HikariDataSource();
ds.setDriverClassName("org.postgresql.Driver");
ds.setJdbcUrl("jdbc:postgresql://localhost:5432/postgres");
ds.setUsername("postgres");
ds.setPassword("postgres");
Environment env = new Environment("postgres", new JdbcTransactionFactory(), ds);
Configuration config = new Configuration(env);
config.addMapper(TestMapper.class);
config.addInterceptor(new CursorInterceptor()); // ★ここ★
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(config);
try(SqlSession session = sqlSessionFactory.openSession()){
TestWrapper wrapper = new TestWrapper();
session.getMapper(TestMapper.class).out(wrapper);
System.out.println(wrapper);
}
}
}
実行結果は以下のような感じ。
TestWrapper(test=[Test(id=1, hoge=hogehoge, fuga=fugafuga), Test(id=2, hoge=hogege, fuga=fugaga)])
まとめ
これはバグなのか仕様なのか判断できなかった。
とはいえ、いろいろ調査することでMyBatis の中身に少し詳しくなった気がするぞ。