1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

PostgreSQL で OUT パラメータに CURSOR を設定した関数を MyBatis から実行するとエラーが発生した

Last updated at Posted at 2019-02-01

タイトルの通りだが、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 になっていて型が不一致だぞ、ということらしい。
そのため、CallableStatementregisterOutParameter で指定している型がおかしいっぽい。

MyBatis はCallableStatementHandlerregisterOutputParametersメソッドで、ParameterMapping から取得した JdbcTypeTYPE_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 の中身に少し詳しくなった気がするぞ。

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?