XMLMapperBuilder を使ってみる。

mybatis の一般的なSQL Mapper の指定は、Mapper インターフェースクラスのパッケージと
同じ階層に、SQLMap XML を配置するか、Configuration のXMLで、

   <mappers>
      <mapper resource="sql/sqlmap.xml"/>
   </mappers>

で書くであろう。
Configuration のXMLを書かずに、さらに、SQLMap XML の PATH をMapper インターフェースクラス
のパッケージと一致させない、XMLファイル名もインターフェースクラス名と全く異なる。
というスタンダードではない設計の時、どうするか?
先日の、mybatis config のXMLを書かない - Oboe吹きプログラマの黙示録
では、対応できていない。

Mapperを記述したXML
 org.apache.ibatis.builder.xml.XMLMapperBuilder
インスタンスを生成して parse() をメソッドを実行すれば、
インターフェース Mapper のパッケージ階層に置かず、まったく異なるXMLファイル名にしても
Configuration に、Mapper を配置してくれる。

public XMLMapperBuilder(InputStream inputStream, Configuration configuration, String resource, Map<String, XNode> sqlFragments)

↑↑↑
が、XMLMapperBuilder のコンストラク

new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments()).parse();

 inputStream に、対象の SQLMap XMLファイルの入力ストリームを指定
 configuration は、Datasource から、Environment を作って、Configuration 生成したもの
 resource は、あたかも、SQLMap XMLファイルリソースの指定のように見えてもここが罠で、
 SQLMap XMLで記述する namespace である。つまり マッパーインターフェースクラス名になる。

を実行して、SqlSessionFactoryBuilder の build メソッドで、configuration を指定して SqlSessionFactory を生成する。

SQLMap XMLを書かないものと、 XMLをルールどおりのパッケージ階層に置かない場合、
両方に対応するものを書くと。。。

key=namesapce 、value=マッパーXML のパス(リソースローダーで読み込むための相対パス
のマップと、マッパークラスの可変引数で、以下のようなメソッドが用意できる。

public static SqlSession getSqlSession(Map<String, String> mappers, Class<?>... mapperClasses){
   UnpooledDataSource dataSource = GenericBuilder.of(UnpooledDataSource::new)
      .with(UnpooledDataSource::setDriver, "com.mysql.cj.jdbc.Driver")
      .with(UnpooledDataSource::setUrl, "jdbc:mysql://localhost:3306/testDB?serverTimezone=JST")
      .with(UnpooledDataSource::setUsername, "root")
      .with(UnpooledDataSource::setPassword, "pass")
      .build();
   Environment environment = new Environment("deployment", new JdbcTransactionFactory(), dataSource);
   Configuration config = new Configuration(environment);
   // snake Case → camel Case
   config.setMapUnderscoreToCamelCase(true);
   for(Class<?> cls: mapperClasses){
   	config.addMapper(cls);
   }
   mappers.entrySet().stream().forEach(e->{
      try(InputStream input = ClassLoader.getSystemResourceAsStream(e.getValue())){
          new XMLMapperBuilder(input, config, e.getKey(), config.getSqlFragments()).parse();
      }catch(Exception ex){
          throw new IllegalArgumentException(ex.getMessage(), ex);
      }finally{
      }
   });
   SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(config);
   return factory.openSession();
}

使用例。。。
SampleMapper.class のパッケージ階層に置かない SampleMapper の SQL Map XMLである
"sql/sample.xml" を指定、
SQLMap XML を持たない FooMapper.class を指定
というケースで、、、

Map<String, String> mappers = new HashMap<>();
mappers.put(SampleMapper.class.getName(), "sql/sample.xml");

try(SqlSession session = getSqlSession(mappers, FooMapper.class)){
     // TODO
}catch(Exception ex){
    // TODO
}finally{
}