技術ネタはQiitaに移りました。壁もどこぞに。

Spring Boot と MyBatis を組み合わせて使用する

クエリレベルでのチューニングの必要があるようなプロジェクトで、Hibernate を適用できないといった場合にたびたび採用されるであろう MyBatis。今回はこれを Spring Boot と組み合わせてみる。

Spring Boot を使用すると、コンフィグレーションのほとんどがアノテーションベースになるため、MyBatis もそうかと思えば、そうではない。MyBatis は MyBatis で、これまでどおり XML ベースのコンフィグレーションファイルが必要になるので、ここは事前に認識しておく。

Spring と MyBatis の連携には、mybatis-spring という、インテグレーションモジュールがでているので、まずは POM にこれらを追加する。

<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.2.8</version>
</dependency>
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis-spring</artifactId>
    <version>1.2.2</version>
</dependency>
<dependency>
    <groupId>commons-dbcp</groupId>
    <artifactId>commons-dbcp</artifactId>
    <version>1.4</version>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-dbcp2</artifactId>
    <version>2.1</version>
</dependency>

Spring から MyBatis を使用する場合には、SqlSessionTemplate クラスを使用するので、これを構成する。

@SpringBootApplication
public class ApplicationContext {
    @Autowired
    @Bean
    public DataSourceInitializer dataSourceInitializer(
            @Qualifier("dataSource") DataSource dataSource) {
        DataSourceInitializer dataSourceInitializer = new DataSourceInitializer();
        dataSourceInitializer.setDataSource(dataSource);
        ResourceDatabasePopulator databasePopulator = new ResourceDatabasePopulator();
        databasePopulator.addScript(new ClassPathResource("sql"));
        dataSourceInitializer.setDatabasePopulator(databasePopulator);
        dataSourceInitializer.setEnabled(false);

        return dataSourceInitializer;
    }

    @Autowired
    @Bean
    public DataSourceTransactionManager transactionManager(
            @Qualifier("dataSource") DataSource dataSource) {
        DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
        transactionManager.setDataSource(dataSource);

        return transactionManager;
    }

    @Autowired
    @Bean
    public SqlSessionTemplate sqlSessionTemplate(
            @Qualifier("dataSource") DataSource dataSource) throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(dataSource);

        ResourcePatternResolver resolver = ResourcePatternUtils.getResourcePatternResolver(
                new DefaultResourceLoader());

        // MyBatis のコンフィグレーションファイル
        bean.setConfigLocation(resolver.getResource("classpath:config.xml"));
        // MyBatis で使用する SQL ファイル群
        bean.setMapperLocations(resolver.getResources("classpath:sql/*.xml"));

        return new SqlSessionTemplate(bean.getObject());
    }

    @Primary
    @Autowired
    @Bean
    public DriverManagerDataSource dataSource() {
        // 今回の例は ORACLE、適宜変更する
        DriverManagerDataSource dataSource = new DriverManagerDataSource();
        dataSource.setDriverClassName("oracle.jdbc.driver.OracleDriver");
        dataSource.setUrl("jdbc:oracle:thin:@//localhost:1521/xe");
        dataSource.setUsername("system");
        dataSource.setPassword("manager");

        return dataSource;
    }
}

続いて、MyBatis 側でのコンフィグレーションファイルの内容。この設定ファイルがない場合、アンダースコア区切り (スネークケース) のカラム名が、キャメルケースのフィールドにマッピングされないため、ほとんど必須。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <settings>
        <!-- アンダースコア区切り (スネークケース) のカラム名をキャメルケースにマップする設定 -->
        <setting name="mapUnderscoreToCamelCase" value="true"/>
        <!-- SQL 内で AS によって設定された列名をマップする設定 -->
        <setting name="useColumnLabel" value="true"/>
    </settings>
    <!-- カラムタイプによりデフォルトとは異なる O/R 変換処理が必要になる場合に設定 -->
    <!--
    <typeHandlers>
        <typeHandler handler="package.name.LocalDateTypeHandler"/>
    </typeHandlers>
    -->
</configuration>

最後に Repository (永続化) レイヤでの SqlSessionTemplate の使い方と SQL ファイルのマッピングを例にあげておく。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- 任意の名前空間を定義する -->
<mapper namespace="SalesRepository">
    <!-- 名前空間 + ID でプロジェクト全体で一意になるように名前を設定 -->
    <!-- ParameterType は SqlSessionTemplate で渡すパラメタの型 -->
    <!-- ResultType は SqlSessionTemplate で返ってくるデータの型 -->
    <select id="findSummaryByYearMonthRange"
            parameterType="jp.hotpepper.beauty.frozen.model.entity.SearchCondition"
            resultType="jp.hotpepper.beauty.frozen.model.entity.SalesSummary">
        SELECT
          STORE.STORE_ID        AS STORE_ID,
          STORE.NAME            AS STORE_NAME,
          COUNT(SALES.SALES_ID) AS SALES_COUNT,
          SUM(SALES.SALES)      AS SALES_TOTAL,
          AVG(CUSTOMER.AGE)     AS AGE_AVERAGE
        FROM
          SALES
        LEFT OUTER JOIN
          STORE
          ON SALES.STORE_ID = STORE.STORE_ID
        LEFT OUTER JOIN
          CUSTOMER
          ON SALES.CUSTOMER_ID = CUSTOMER.CUSTOMER_ID
        WHERE
          <!-- パラメタ型のフィールド名を #{ } で囲んで設定 -->
          <!-- パラメタ型が String など構造を持たない場合は任意の名前でよい -->
          SALES.CREATED BETWEEN #{from} AND #{to}
        GROUP BY
          STORE.STORE_ID,
          STORE.NAME
        ORDER BY
          STORE_ID
    </select>
</mapper>
@Repository
public class MybatisSalesRepository implements SalesRepository {
    @Autowired
    private SqlSessionTemplate template;

    @Override
    public List<SalesSummary> findSummaryByYearMonth(SearchCondition condition) {
        List<SalesSummary> items = this.<SalesSummary>getTemplate().selectList(
                // SQL を定義した XML での、名前空間 + ID を指定
                // ID だけでもプロジェクト全体で一意になる場合は、ID のみの指定も可能
                "SalesRepository.findSummaryByYearMonthRange",
                condition);

        return items;
    }

    protected SqlSessionTemplate getTemplate() {
        return template;
    }
}

全体としてはこのような形で、Spring Boot で MyBatis を使用できるようになる。MyBatis のコンフィグレーションファイルを、アプリケーションコンテキストで別途読み込まないといけないところに気付くまでにずいぶん時間がかかってしまった。