6.2. データベースアクセス(MyBatis3編)


6.2.1. Overview

本節では、MyBatis3を使用してデータベースにアクセスする方法について説明する。

本ガイドラインでは、MyBatis3のMapperインタフェースをRepositoryインタフェースとして使用することを前提としている。
Repositoryインタフェースについては、「Repositoryの実装」を参照されたい。
Overviewでは、MyBatis3とMyBatis-Springを使用してデータベースアクセスする際のアーキテクチャについて説明を行う。
実際の使用方法については、「How to use」を参照されたい。
Scope of description

Picture - Scope of description


6.2.1.1. MyBatis3について

MyBatis3は、O/R Mapperの一つだが、データベースで管理されているレコードとオブジェクトをマッピングするという考え方ではなく、SQLとオブジェクトをマッピングするという考え方で開発されたO/R Mapperである。
そのため、正規化されていないデータベースへアクセスする場合や、発行するSQLをO/R Mapperに任せずに、アプリケーション側で完全に制御したい場合に有効なO/R Mapperである。
本ガイドラインでは、MyBatis3から追加されたMapperインタフェースを使用して、EntityのCRUD操作を行う。
Mapperインタフェースの詳細については、「Mapperインタフェースの仕組みについて」を参照されたい。
本ガイドラインでは、MyBatis3の全ての機能の使用方法について説明を行うわけではないため、「MyBatis 3 REFERENCE DOCUMENTATION」も合わせて参照して頂きたい。

6.2.1.1.1. MyBatis3のコンポーネント構成について

MyBatis3の主要なコンポーネント(設定ファイル)について説明する。
MyBatis3では、設定ファイルの定義に基づき、以下のコンポーネントが互いに連携する事によって、SQLの実行及びO/Rマッピングを実現している。

項番

コンポーネント/設定ファイル

説明

MyBatis設定ファイル

MyBatis3の動作設定を記載するXMLファイル。

データベースの接続先、マッピングファイルのパス、MyBatisの動作設定などを記載するファイルである。 Springと連携して使用する場合は、データベースの接続先やマッピングファイルのパスの設定を本設定ファイルに指定する必要がないため、 MyBatis3のデフォルトの動作を変更又は拡張する際に、設定を行う事になる。

org.apache.ibatis.session.SqlSessionFactoryBuilder

MyBatis設定ファイルを読込み、SqlSessionFactoryを生成するためのコンポーネント。

Springと連携して使用する場合は、アプリケーションのクラスから本コンポーネントを直接扱うことはない。

org.apache.ibatis.session.SqlSessionFactory

SqlSessionを生成するためのコンポーネント。

Springと連携して使用する場合は、アプリケーションのクラスから本コンポーネントを直接扱うことはない。

org.apache.ibatis.session.SqlSession

SQLの発行やトランザクション制御のAPIを提供するコンポーネント。

MyBatis3を使ってデータベースにアクセスする際に、もっとも重要な役割を果たすコンポーネントである。

Springと連携して使用する場合は、アプリケーションのクラスから本コンポーネントを直接扱うことは、基本的にはない。

Mapperインタフェース

マッピングファイルに定義したSQLをタイプセーフに呼び出すためのインタフェース。

Mapperインターフェースに対する実装クラスは、MyBatis3が自動で生成するため、開発者はインターフェースのみ作成すればよい。

マッピングファイル

SQLとO/Rマッピングの設定を記載するXMLファイル。


以下に、MyBatis3の主要コンポーネントが、どのような流れでデータベースにアクセスしているのかを説明する。
データベースにアクセスするための処理は、大きく2つにわける事ができる。
  • アプリケーションの起動時に行う処理。下記(1)~(3)の処理が、これに該当する。

  • クライアントからのリクエスト毎に行う処理。下記(4)~(10)の処理が、これに該当する。

    Relationship of MyBatis3 components

    Picture - Relationship of MyBatis3 components

アプリケーションの起動時に行う処理は、以下の流れで実行する。
Springと連携時の流れについては、「MyBatis-Springのコンポーネント構成について」を参照されたい。

項番

説明

アプリケーションは、SqlSessionFactoryBuilderに対して SqlSessionFactoryの構築を依頼する。

SqlSessionFactoryBuilderは、 SqlSessionFactoryを生成するためにMyBatis設定ファイルを読込む。

SqlSessionFactoryBuilderは、MyBatis設定ファイルの定義に基づき SqlSessionFactoryを生成する。


クライアントからのリクエスト毎に行う処理は、以下の流れで実行する。
Springと連携時の流れについては、「MyBatis-Springのコンポーネント構成について」を参照されたい。

項番

説明

クライアントは、アプリケーションに対して処理を依頼する。

アプリケーションは、SqlSessionFactoryBuilderによって構築された SqlSessionFactoryから SqlSessionを取得する。

SqlSessionFactoryは、SqlSessionを生成しアプリケーションに返却する。

アプリケーションは、SqlSessionからMapperインタフェースの実装オブジェクトを取得する。

アプリケーションは、Mapperインタフェースのメソッドを呼び出す。

Mapperインタフェースの仕組みについては、「Mapperインタフェースの仕組みについて」を参照されたい。

Mapperインタフェースの実装オブジェクトは、SqlSessionのメソッドを呼び出して、SQLの実行を依頼する。

SqlSessionは、マッピングファイルから実行するSQLを取得し、SQLを実行する。

Tip

トランザクション制御について

上記フローには記載していないが、トランザクションのコミット及びロールバックは、アプリケーションのコードからSqlSessionのAPIを直接呼び出して行う。

ただし、Springと連携する場合は、Springのトランザクション管理機能がコミット及びロールバックを行うため、アプリケーションのクラスからSqlSessionのトランザクションを制御するためのAPIを直接呼び出すことはない。


6.2.1.2. MyBatis3とSpringの連携について

MyBatis3とSpringを連携させるライブラリとして、MyBatisからMyBatis-Spring というライブラリが提供されている。
このライブラリを使用することで、MyBatis3のコンポーネントをSpringのDIコンテナ上で管理する事ができる。

MyBatis-Springを使用すると、

  • MyBatis3のSQLの実行をSpringが管理しているトランザクション内で行う事ができるため、MyBatis3のAPIに依存したトランザクション制御を行う必要がない。

  • MyBatis3の例外は、Springが用意している汎用的な例外(org.springframework.dao.DataAccessException)へ変換されるため、MyBatis3のAPIに依存しない例外処理を実装する事ができる。

  • MyBatis3を使用するための初期化処理は、すべてMyBatis-SpringのAPIが行ってくれるため、基本的にはMyBatis3のAPIを直接使用する必要がない。

  • スレッドセーフなMapperオブジェクトの生成が行えるため、シングルトンのServiceクラスにMapperオブジェクトを注入する事ができる。

等のメリットがある。 本ガイドラインでは、MyBatis-Springを使用することを前提とする。

本ガイドラインでは、MyBatis-Springの全ての機能の使用方法について説明を行うわけではないため、 「Mybatis-Spring REFERENCE DOCUMENTATION 」も合わせて参照して頂きたい。


6.2.1.2.1. MyBatis-Springのコンポーネント構成について

MyBatis-Springの主要なコンポーネントについて説明する。
MyBatis-Springでは、以下のコンポーネントが連携する事によって、MyBatis3とSpringの連携を実現している。

項番

コンポーネント/設定ファイル

説明

org.mybatis.spring.SqlSessionFactoryBean

SqlSessionFactoryを構築し、SpringのDIコンテナ上にオブジェクトを格納するためのコンポーネント。

MyBatis3標準では、MyBatis設定ファイルに定義されている情報を基にSqlSessionFactoryを構築するが、SqlSessionFactoryBeanを使用すると、MyBatis設定ファイルがなくてもSqlSessionFactoryを構築することができる。
もちろん、併用することも可能である。

org.mybatis.spring.mapper.MapperFactoryBean

シングルトンのMapperオブジェクトを構築し、SpringのDIコンテナ上にオブジェクトを格納するためのコンポーネント。

MyBatis3標準の仕組みで生成されるMapperオブジェクトはスレッドセーフではないため、スレッド毎にインスタンスを割り当てる必要があった。
MyBatis-Springのコンポーネントで作成されたMapperオブジェクトは、スレッドセーフなMapperオブジェクトを生成する事ができるため、ServiceなどのシングルトンのコンポーネントにDIすることが可能となる。

org.mybatis.spring.SqlSessionTemplate

SqlSessionインターフェースを実装したシングルトン版のSqlSessionコンポーネント。

MyBatis3標準の仕組みで生成されるSqlSessionオブジェクトはスレッドセーフではないため、スレッド毎にインスタンスを割り当てる必要があった。
MyBatis-Springのコンポーネントで作成されたSqlSessionオブジェクトは、スレッドセーフなSqlSessionオブジェクトが生成されるため、ServiceなどのシングルトンのコンポーネントにDIすることが可能になる。
ただし、本ガイドラインでは、SqlSessionを直接扱う事は想定していない。

以下に、MyBatis-Springの主要コンポーネントが、どのような流れでデータベースにアクセスしているのかを説明する。 データベースにアクセスするための処理は、大きく2つにわける事ができる。

  • アプリケーションの起動時に行う処理。下記(1)~(4)の処理が、これに該当する。

  • クライアントからのリクエスト毎に行う処理。下記(5)~(11)の処理が、これに該当する。

    Relationship of MyBatis-Spring components

    Picture - Relationship of MyBatis-Spring components

アプリケーションの起動時に行う処理は、以下の流れで実行される。

項番

説明

SqlSessionFactoryBeanは、SqlSessionFactoryBuilderに対して SqlSessionFactoryの構築を依頼する。

SqlSessionFactoryBuilderは、 SqlSessionFactoryを生成するためにMyBatis設定ファイルを読込む。

SqlSessionFactoryBuilderは、MyBatis設定ファイルの定義に基づきSqlSessionFactoryを生成する。

生成されたSqlSessionFactoryは、SpringのDIコンテナによって管理される。

MapperFactoryBeanは、スレッドセーフなSqlSession(SqlSessionTemplate)と、スレッドセーフなMapperオブジェクト(MapperインタフェースのProxyオブジェクト)を生成する。

生成されたMapperオブジェクトは、SpringのDIコンテナによって管理され、ServiceクラスなどにDIされる。
Mapperオブジェクトは、スレッドセーフなSqlSession(SqlSessionTemplate)を利用することで、スレッドセーフな実装を提供している。

クライアントからのリクエスト毎に行う処理は、以下の流れで実行される。

項番

説明

クライアントは、アプリケーションに対して処理を依頼する。

アプリケーション(Service)は、 DIコンテナによって注入されたMapperオブジェクト(Mapperインターフェースを実装したProxyオブジェクト)のメソッドを呼び出す。

Mapperインタフェースの仕組みについては、 「Mapperインタフェースの仕組みについて」を参照されたい。

Mapperオブジェクトは、呼び出されたメソッドに対応するSqlSession(SqlSessionTemplate)のメソッドを呼び出す。

SqlSession(SqlSessionTemplate)は、Proxy化されたスレッドセーフなSqlSessionのメソッドを呼び出す。

Proxy化されたスレッドセーフなSqlSessionは、トランザクションに割り当てられているMyBatis3標準のSqlSessionを使用する。

トランザクションに割り当てられているSqlSessionが存在しない場合は、MyBatis3標準のSqlSessionを取得するために、SqlSessionFactoryのメソッドを呼び出す。

SqlSessionFactoryは、MyBatis3標準のSqlSessionを返却する。

返却されたMyBatis3標準のSqlSessionはトランザクションに割り当てられるため、同一トランザクション内であれば、新たに生成されることはなく、同じSqlSessionが使用される仕組みになっている。

MyBatis3標準のSqlSessionは、マッピングファイルから実行するSQLを取得し、SQLを実行する。

Tip

トランザクション制御について

上記フローには記載していないが、トランザクションのコミット及びロールバックは、Springのトランザクション管理機能が行う。

Springのトランザクション管理機能を使用したトランザクション管理方法については、「トランザクション管理について」を参照されたい。


6.2.2. How to use

ここからは、実際にMyBatis3を使用して、データベースにアクセスするための設定及び実装方法について、説明する。

以降の説明は、大きく以下に分類する事ができる。

項番

分類

説明

アプリケーション全体の設定

MyBatis3をアプリケーションで使用するための設定方法や、MyBatis3の動作を変更するための設定方法について記載している。

ここに記載している内容は、プロジェクト立ち上げ時にアプリケーションアーキテクトが設定を行う時に必要となる。
そのため、基本的にはアプリケーション開発者が個々に意識する必要はない部分である。

以下のセクションが、この分類に該当する。

MyBatis3用のブランクプロジェクト からプロジェクトを生成した場合は、上記で説明している設定の多くが既に設定済みの状態となっているため、アプリケーションアーキテクトは、プロジェクト特性を判断し、必要に応じて設定の追加及び変更を行うことになる。

データアクセス処理の実装方法

MyBatis3を使った基本的なデータアクセス処理の実装方法について記載している。

ここに記載している内容は、アプリケーション開発者が実装時に必要となる。

以下のセクションが、この分類に該当する。


6.2.2.1. pom.xmlの設定

インフラストラクチャ層にMyBatis3を使用する場合は、pom.xmlにterasoluna-gfw-mybatis3-dependenciesへの依存関係を追加する。
マルチプロジェクト構成の場合は、domainプロジェクトのpom.xml(projectName-domain/pom.xml)に追加する。

MyBatis3用のブランクプロジェクト からプロジェクトを生成した場合は、terasoluna-gfw-mybatis3-dependenciesへの依存関係は、設定済みの状態である。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
        http://maven.apache.org/maven-v4_0_0.xsd">

    <modelVersion>4.0.0</modelVersion>
    <artifactId>projectName-domain</artifactId>
    <packaging>jar</packaging>

    <parent>
        <groupId>com.example</groupId>
        <artifactId>mybatis3-example-app</artifactId>
        <version>1.0.0-SNAPSHOT</version>
        <relativePath>../pom.xml</relativePath>
    </parent>

    <dependencies>

        <!-- omitted -->

        <!-- (1) -->
        <dependency>
            <groupId>org.terasoluna.gfw</groupId>
            <artifactId>terasoluna-gfw-mybatis3-dependencies</artifactId>
            <type>pom</type>
        </dependency>

        <!-- omitted -->

    </dependencies>

    <!-- omitted -->

</project>

項番

説明

terasoluna-gfw-mybatis3をdependenciesに追加する。 terasoluna-gfw-mybatis3には、MyBatis3及びMyBatis-Springへの依存関係が定義されている。

Note

上記設定例は、依存ライブラリのバージョンを親プロジェクトである terasoluna-gfw-parent で管理する前提であるため、pom.xmlでのバージョンの指定は不要である。


6.2.2.2. MyBatis3とSpringを連携するための設定

6.2.2.2.1. データソースの設定

MyBatis3とSpringを連携する場合、データソースはSpringのDIコンテナで管理しているデータソースを使用する必要がある。

MyBatis3用のブランクプロジェクト からプロジェクトを生成した場合は、Apache Commons DBCPのデータソースが設定済みの状態であるため、プロジェクトの要件に合わせて設定を変更すること。

データソースの設定方法については、共通編の「データソースの設定」を参照されたい。


6.2.2.2.2. トランザクション管理の設定

MyBatis3とSpringを連携する場合、 トランザクション管理はSpringのDIコンテナで管理しているPlatformTransactionManagerを使用する必要がある。

ローカルトランザクションを使用する場合は、JDBCのAPIを呼び出してトランザクション制御を行うDataSourceTransactionManagerを使用する。

MyBatis3用のブランクプロジェクト からプロジェクトを生成した場合は、DataSourceTransactionManagerが設定済みの状態である。

設定例は以下の通り。

  • projectName-env/src/main/xxx/yyy/zzz/config/app/ProjectNameEnvConfig.java

    @Bean("transactionManager")
    public TransactionManager transactionManager(
            @Qualifier("dataSource") DataSource dataSource) {
        DataSourceTransactionManager bean = new DataSourceTransactionManager(); // (1)
        bean.setDataSource(dataSource); // (2)
        bean.setRollbackOnCommitFailure(true); // (3)
        return bean;
    }
    

    項番

    説明

    PlatformTransactionManagerとして、org.springframework.jdbc.datasource.DataSourceTransactionManagerを指定する。

    dataSourceプロパティに、設定済みのデータソースのbeanを指定する。

    トランザクション内でSQLを実行する際は、ここで指定したデータソースからコネクションが取得される。

    コミット時にエラーが発生した場合にロールバック処理が呼び出される様にする。

    この設定を追加することで、「未確定状態の操作を持つコネクションがコネクションプールに戻ることで発生する意図しないコミット(コネクション再利用時のコミット、コネクションクローズ時の暗黙コミットなど)」が発生するリスクを下げることができる。ただし、ロールバック処理時にエラーが発生する可能性もあるため、意図しないコミットが発生するリスクがなくなるわけではない点に留意されたい。


6.2.2.2.3. MyBatis-Springの設定

MyBatis3とSpringを連携する場合、MyBatis-Springのコンポーネントを使用して、

  • MyBatis3とSpringを連携するために必要となる処理がカスタマイズされたSqlSessionFactoryの生成

  • スレッドセーフなMapperオブジェクト(MapperインタフェースのProxyオブジェクト)の生成

を行う必要がある。

MyBatis3用のブランクプロジェクト からプロジェクトを生成した場合は、MyBatis3とSpringを連携するための設定は、設定済みの状態である。

設定例は以下の通り。

  • projectName-domain/src/main/xxx/yyy/zzz/config/app/ProjectNameInfraConfig.java

    @Configuration
    @MapperScan(basePackages = "com.example.domain.repository", sqlSessionFactoryRef = "sqlSessionFactory") // (4)
    @Import({ ProjectNameEnvConfig.class })
    public class ProjectNameInfraConfig {
    
        // omitted
    
        @Bean("sqlSessionFactory")
        public SqlSessionFactoryBean sqlSessionFactory(
                @Qualifier("dataSource") DataSource dataSource) throws IOException {
            SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); // (1)
            bean.setDataSource(dataSource); // (2)
            bean.setConfiguration(MybatisConfig.configuration()); // (3)
            return bean;
        }
    

    項番

    説明

    SqlSessionFactoryを生成するためのコンポーネントとして、SqlSessionFactoryBeanをbean定義する。

    dataSourceプロパティに、設定済みのデータソースのbeanを指定する。

    MyBatis3の処理の中でSQLを発行する際は、ここで指定したデータソースからコネクションが取得される。

    configurationプロパティに、MyBatisのConfigrationを指定する。上記設定例では後述するMyBatis設定ファイルから作成されるorg.apache.ibatis.session.Configurationを指定している。

    Mapperインタフェースをスキャンするために@MapperScanアノテーションを定義し、base-package属性には、Mapperインタフェースが格納されている基底パッケージを指定する。

    指定されたパッケージ配下に格納されている Mapperインタフェースがスキャンされ、スレッドセーフなMapperオブジェクト(MapperインタフェースのProxyオブジェクト)が自動的に生成される。

    【指定するパッケージは、各プロジェクトで決められたパッケージにすること】


6.2.2.3. MyBatis3の設定

MyBatis3では、MyBatis3の動作をカスタマイズするための仕組みが用意されている。
MyBatis3の動作をカスタマイズする場合は、MyBatis設定ファイルに設定値を追加する事で実現可能である。
ここでは、アプリケーションの特性に依存しない設定項目についてのみ、説明を行う。
その他の設定項目に関しては、「MyBatis 3 REFERENCE DOCUMENTATION(Configuration XML) 」を参照し、アプリケーションの特性にあった設定を行うこと。
基本的にはデフォルト値のままでも問題ないが、アプリケーションの特性を考慮し、必要に応じて設定を変更すること。

6.2.2.3.1. fetchSizeの設定

大量のデータを返すようなクエリを記述する場合は、JDBCドライバに対して適切なfetchSizeを指定する必要がある。
fetchSizeは、JDBCドライバとデータベース間の1回の通信で取得するデータの件数を設定するパラメータである。

fetchSizeを指定しないとJDBCドライバのデフォルト値が利用されるため、使用するJDBCドライバによっては以下の問題を引き起こす可能性がある。

  • デフォルト値が小さいJDBCドライバの場合は「性能の劣化」

  • デフォルト値が大きい又は制限がないJDBCドライバの場合は「メモリの枯渇」

これらの問題が発生しないように制御するために、MyBatis3は以下の2つの方法でfetchSizeを指定することができる。

  • 全てのクエリに対して適用する「デフォルトのfetchSize」の指定

  • 特定のクエリに対して適用する「クエリ単位のfetchSize」の指定

Note

「デフォルトのfetchSize」について

「デフォルトのfetchSize」は、MyBatis 3.3.0以降のバージョンで利用することができる。


以下に、「デフォルトのfetchSize」を指定する方法を示す。

  • projectName-domain/src/main/xxx/yyy/zzz/config/app/mybatis/MybatisConfig.java

    public static Configuration configuration() throws IOException {
        Configuration configuration = new Configuration();
    
        // omitted
    
        setSettings(configuration);
    
        return configuration;
    }
    
    private static void setSettings(Configuration configuration) {
    
        // omitted
    
        configuration.setDefaultFetchSize(100); // (1)
    }
    

    項番

    説明

    defaultFetchSizeに、1回の通信で取得するデータの件数を指定する。

Note

「クエリ単位のfetchSize」の指定方法

fetchSizeをクエリ単位に指定する必要がある場合は、検索用のSQLを記述するためのXML要素(<select>要素)のfetchSize属性に値を指定すればよい。

なお、大量のデータを返すようなクエリを記述する場合は、「ResultHandlerの実装」の利用も検討すること。


6.2.2.3.2. SQL実行モードの設定

MyBatis3では、SQLを実行するモードとして以下の3種類を用意している。

どのモードを使用するかは、各モードの特性と制約、及び性能要件を考慮して決定して頂きたい。
実行モードの設定方法などについては、「SQL実行モードの利用」を参照されたい。

項番

モード

説明

SIMPLE

SQL実行毎に新しいjava.sql.PreparedStatementを作成する。

MyBatisのデフォルトの動作であり、MyBatis3用のブランクプロジェクトSIMPLEモードとなっている。

REUSE

PreparedStatementをキャッシュし再利用する。

同一トランザクション内で同じSQLを複数回実行する場合は、REUSEモードで実行すると、SIMPLEモードと比較して性能向上が期待できる。

これは、SQLを解析してPreparedStatementを生成する処理の実行回数を減らす事ができるためである。

BATCH

更新系のSQLをバッチ実行する。(java.sql.Statement#executeBatch()を使ってSQLを実行する)。

同一トランザクション内で更新系のSQLを連続して大量に実行する場合は、BATCHモードで実行すると、SIMPLEモードやREUSEモードと比較して性能向上が期待できる。

これは、

  • SQLを解析してPreparedStatementを生成する処理の実行回数

  • サーバと通信する回数

を減らす事ができるためである。

ただし、BATCHモードを使用する場合は、MyBatisの動きがSIMPLEモードやSIMPLEモードと異なる部分がある。
具体的な違いと注意点については、「バッチモードのRepository利用時の注意点」を参照されたい。

6.2.2.3.3. NULL値とJDBC型のマッピング設定

使用しているデータベース(JDBCドライバ)によっては、カラム値をnullに設定する際に、エラーが発生する場合がある。
この事象は、JDBCドライバがnull値の設定と認識できるJDBC型を指定する事で、解決する事ができる。
null値を設定した際に、以下の様なスタックトレースを伴うエラーが発生した場合は、null値とJDBC型のマッピングが必要となる。
MyBatis3のデフォルトでは、OTHERと呼ばれる汎用的なJDBC型が指定されるが、OTHERだとエラーとなるJDBCドライバもある。
java.sql.SQLException: Invalid column type: 1111
    at oracle.jdbc.driver.OracleStatement.getInternalType(OracleStatement.java:3916) ~[ojdbc6-11.2.0.2.0.jar:11.2.0.2.0]
    at oracle.jdbc.driver.OraclePreparedStatement.setNullCritical(OraclePreparedStatement.java:4541) ~[ojdbc6-11.2.0.2.0.jar:11.2.0.2.0]
    at oracle.jdbc.driver.OraclePreparedStatement.setNull(OraclePreparedStatement.java:4523) ~[ojdbc6-11.2.0.2.0.jar:11.2.0.2.0]
    ...

Note

Oracle使用時の動作について

Oracle JDBC ドライバはJDBC型のOTHERをサポートしていないため、デフォルト設定のままだとエラーが発生することが確認されている。

OracleではJDBC型のNULL型を指定すれば、null値を正常にマッピングすることが可能となる。


以下に、MyBatis3のデフォルトの動作を変更する方法を示す。

  • projectName-domain/src/main/xxx/yyy/zzz/config/app/mybatis/MybatisConfig.java

    public static Configuration configuration() throws IOException {
        Configuration configuration = new Configuration();
    
        // omitted
    
        setSettings(configuration);
    
        return configuration;
    }
    
    private static void setSettings(Configuration configuration) {
    
        // omitted
    
        configuration.setJdbcTypeForNull("NULL"); // (1)
    }
    

    項番

    説明

    jdbcTypeForNullに、JDBC型を指定する。

    上記例では、null値のJDBC型としてNULL型を指定している。

Tip

項目単位で解決する方法について

別の解決方法として、null値が設定される可能性があるプロパティのインラインパラメータに、Java型に対応する適切なJDBC型を個別に指定する方法もある。

ただし、インラインパラメータで個別に指定した場合、マッピングファイルの記述量及び指定ミスが発生する可能性が増えることが予想されるため、本ガイドラインとしては、全体の設定でエラーを解決することを推奨している。

全体の設定を変更してもエラーが解決しない場合は、エラーが発生するプロパティについてのみ、個別に設定を行えばよい。


6.2.2.3.4. TypeAliasの設定

TypeAliasを使用すると、マッピングファイルで指定するJavaクラスに対して、エイリアス名(短縮名)を割り当てる事ができる。

TypeAliasを使用しない場合、マッピングファイルで指定するtype属性、parameterType属性、resultType属性などには、Javaクラスの完全修飾クラス名(FQCN)を指定する必要があるため、マッピングファイルの記述効率の低下、記述ミスの増加などが懸念される。

本ガイドラインでは、記述効率の向上、記述ミスの削減、マッピングファイルの可読性向上などを目的として、TypeAliasを使用することを推奨する。

MyBatis3用のブランクプロジェクト からプロジェクトを生成した場合は、Entityを格納するパッケージ(${projectPackage}.domain.model)配下に格納されるクラスがTypeAliasの対象となっている。
必要に応じて、設定を追加されたい。
6.2.2.3.4.1. パッケージ名を指定してTypeAliasを設定する

パッケージ名を指定することで、指定したパッケージ配下に格納されているクラスは、パッケージの部分が除去された部分がエイリアス名となる。

TypeAliasの設定方法は以下の通り。

  • projectName-domain/src/main/xxx/yyy/zzz/config/app/mybatis/MybatisConfig.java

    public static Configuration configuration() throws IOException {
        Configuration configuration = new Configuration();
    
        // omitted
    
        setTypeAliases(configuration.getTypeAliasRegistry());
    
        return configuration;
    }
    
    private static void setTypeAliases(TypeAliasRegistry typeAliasRegistry) {
        typeAliasRegistry.registerAliases("com.example.domain.model"); // (1)
    }
    

    項番

    説明

    TypeAliasRegistryregisterAliasesに、エイリアスを設定するクラスが格納されているパッケージ名を指定する。

    指定したパッケージ配下に格納されているクラスは、パッケージの部分が除去された部分がエイリアス名となる。
    上記例だと、com.example.domain.model.Accountクラスのエイリアス名は、Accountとなる。

    【指定するパッケージは、各プロジェクトで決められたパッケージにすること】


6.2.2.3.4.2. クラスを指定してTypeAliasを設定する

TypeAliasの設定は、クラス単位で設定する事もできる。

  • projectName-domain/src/main/xxx/yyy/zzz/config/app/mybatis/MybatisConfig.java

    public static Configuration configuration() throws IOException {
        Configuration configuration = new Configuration();
    
        // omitted
    
        setTypeAliases(configuration.getTypeAliasRegistry());
    
        return configuration;
    }
    
    private static void setTypeAliases(TypeAliasRegistry typeAliasRegistry) {
        typeAliasRegistry.registerAlias(AccountSearchCriteria.class); // (1)
    }
    

    項番

    説明

    TypeAliasRegistryregisterAliasに、エイリアスを設定するクラスの完全修飾クラス名(FQCN)を指定する。

    上記例だと、com.example.domain.repository.account.AccountSearchCriteriaクラスのエイリアス名は、AccountSearchCriteria(パッケージの部分が除去された部分)となる。

    エイリアス名に任意の値を指定したい場合は、第一引数に任意のエイリアス名を指定することができる。


6.2.2.3.4.3. デフォルトで付与されるエイリアス名の上書き

package要素を使用してエイリアスを設定した場合や、typeAlias要素のalias属性を省略してエイリアスを設定した場合は、TypeAliasのエイリアス名は、完全修飾クラス名(FQCN)からパッケージの部分が除去された部分となる。

デフォルトで付与されるエイリアス名ではなく、任意のエイリアス名にしたい場合は、TypeAliasを設定したいクラスに@org.apache.ibatis.type.Aliasアノテーションを指定する事で、 任意のエイリアス名を指定する事ができる。

  • エイリアス設定対象のJavaクラス

    package com.example.domain.model.book;
    
    @Alias("BookAuthor") // (1)
    public class Author {
       // omitted
    }
    
    package com.example.domain.model.article;
    
    @Alias("ArticleAuthor") // (1)
    public class Author {
       // omitted
    }
    

    項番

    説明

    @Aliasアノテーションのvalue属性に、エイリアス名を指定する。

    上記例だと、com.example.domain.model.book.Authorクラスのエイリアス名は、BookAuthorとなる。

    異なるパッケージの中に同じクラス名のクラスが格納されている場合は、この方法を使用することで、それぞれ異なるエイリアス名を設定する事ができる。ただし、本ガイドラインでは、クラス名は重複しないように設計する事を推奨する。
    上記例であれば、クラス名自体をBookAuthorArticleAuthorにすることを検討して頂きたい。

    Tip

    TypeAliasの エイリアス名は、

    • typeAlias要素のalias属性の指定値

    • @Aliasアノテーションのvalue属性の指定値

    • デフォルトで付与されるエイリアス名(完全修飾クラス名からパッケージの部分が除去された部分)

    の優先順で適用される。


TypeAliasを使用した際の、マッピングファイルの記述例は以下の通り。

<?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="com.example.domain.repository.account.AccountRepository">

    <resultMap id="accountResultMap"
        type="Account">
        <!-- omitted -->
    </resultMap>

    <select id="findByUsername"
        parameterType="string"
        resultMap="accountResultMap">
        <!-- omitted -->
    </select>

    <select id="findByCriteria"
        parameterType="AccountSearchCriteria"
        resultMap="accountResultMap">
        <!-- omitted -->
    </select>

</mapper>

Tip

MyBatis3標準のエイリアス名について

プリミティブ型やプリミティブラッパ型などの一般的なJavaクラスについては、予めエイリアス名が設定されている。

予め設定されるエイリアス名については、「MyBatis 3 REFERENCE DOCUMENTATION(Configuration XML-typeAliases-) 」を参照されたい。


6.2.2.3.5. TypeHandlerの設定

TypeHandlerは、JavaクラスとJDBC型をマッピングする時に使用される。

具体的には、

  • SQLを発行する際に、Javaクラスのオブジェクトをjava.sql.PreparedStatementのバインドパラメータとして設定する

  • SQLの発行結果として取得したjava.sql.ResultSetから値を取得する

際に、使用される。

プリミティブ型やプリミティブラッパ型などの一般的なJavaクラスについては、MyBatis3からTypeHandlerが提供されており、特別な設定を行う必要はない。

Note

BLOB用とCLOB用の実装について

MyBatis 3.4で追加されたTypeHandlerは、JDBC 4.0 (Java 1.6)で追加されたAPIを使用することで、BLOBとjava.io.InputStream、CLOBとjava.io.Readerの変換を実現している。

JDBC 4.0サポートのJDBCドライバーであれば、BLOB⇔InputStream、CLOB⇔Reader変換用のタイプハンドラーがデフォルトで有効になるため、TypeHandlerを新たに実装する必要はない。

JDBC 4.0との互換性のないJDBCドライバを使う場合は、利用するJDBCドライバの互換バージョンを意識したTypeHandlerを作成する必要がある。

例えば、PostgreSQL用のJDBCドライバ(postgresql-42.2.9.jar)では、JDBC 4.0から追加されたメソッドの一部が、未実装の状態である。

Note

mybatis-typehandlers-jsr310で提供されていたJSR-310 Date and Time API用のTypeHandlerが、MyBatis 3.4.5からコアモジュールに統合された。

これにより、依存ライブラリとして別途mybatis-typehandlers-jsr310を追加する必要はなくなった。

Tip

MyBatis3から提供されているTypeHandlerについては、「MyBatis 3 REFERENCE DOCUMENTATION(Configuration XML-typeHandlers-) 」を参照されたい。

Tip

Enum型のマッピングについて

MyBatis3のデフォルトの動作では、Enum型はEnumの定数名(文字列)とマッピングされる。

下記のようなEnum型の場合は、WAITING_FOR_ACTIVE, ACTIVE, EXPIRED, LOCKEDという文字列とマッピングされてテーブルに格納される。

package com.example.domain.model;

public enum AccountStatus {
    WAITING_FOR_ACTIVE, ACTIVE, EXPIRED, LOCKED
}

MyBatisでは、Enum型を数値(定数の定義順)とマッピングする事もできる。数値とマッピングする方法については、「MyBatis 3 REFERENCE DOCUMENTATION(Configuration XML-Handling Enums-) 」を参照されたい。


TypeHandlerの作成が必要になるケースは、MyBatis3でサポートしていないJoda-TimeのクラスとJDBC型をマッピングする場合である。

具体的には、「日付操作(Joda Time)」のorg.joda.time.DateTime型と、JDBC型のTIMESTAMP型をマッピングする場合に、TypeHandlerの作成が必要となる。

Joda-TimeのクラスとJDBC型をマッピングするTypeHandlerの作成例については、「TypeHandlerの実装」を参照されたい。


ここでは、作成したTypeHandlerをMyBatisに適用する方法について説明を行う。


6.2.2.3.5.1. パッケージ名を指定してTypeHandlerを設定する

パッケージ名を指定することで、指定したパッケージ配下に格納されているTypeHandlerを抽出できる。

  • projectName-domain/src/main/xxx/yyy/zzz/config/app/mybatis/MybatisConfig.java

    public static Configuration configuration() throws IOException {
        Configuration configuration = new Configuration();
    
        // omitted
    
        setTypeHandlers(configuration.getTypeHandlerRegistry());
    
        return configuration;
    }
    
    private static void setTypeHandlers(
        TypeHandlerRegistry typeHandlerRegistry) {
        typeHandlerRegistry.register(
            "com.example.infra.mybatis.typehandler"); // (1)
    }
    

    項番

    説明

    MyBatis設定ファイルにTypeHandlerの設定を行う。

    TypeAliasRegistrytypeHandlerRegistryに、作成したTypeHandlerが格納されているパッケージ名を指定する。
    指定したパッケージ配下に格納されているTypeHandlerが、MyBatisによって自動検出される。

6.2.2.3.5.2. クラスを指定してTypeHandlerを設定する

クラスを指定することで、クラス単位にTypeHandlerを設定することが出来る。

  • projectName-domain/src/main/xxx/yyy/zzz/config/app/mybatis/MybatisConfig.java

    public static Configuration configuration() throws IOException {
        Configuration configuration = new Configuration();
    
        // omitted
    
        setTypeHandlers(configuration.getTypeHandlerRegistry());
    
        return configuration;
    }
    
    private static void setTypeHandlers(
        TypeHandlerRegistry typeHandlerRegistry) {
        typeHandlerRegistry.register(CustomTypeHandler.class);
    }
    

DIコンテナが管理しているbeanを使用したい場合は、MyBatis設定ファイルではなくbean定義ファイル内でTypeHandlerを指定すればよい。

  • projectName-domain/src/main/xxx/yyy/zzz/config/app/ProjectNameInfraConfig.java

    @Bean("sqlSessionFactory")
    public SqlSessionFactoryBean sqlSessionFactory(
            DataSource dataSource) throws IOException {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(dataSource);
        bean.setConfiguration(MybatisConfig.configuration());
        bean.setTypeHandlers(customTypeHandler());
        return bean;
    }
    
    @Bean
    public CustomTypeHandler customTypeHandler(){
        new CustomTypeHandler();
    }
    

TypeHandlerを適用するJavaクラスとJDBC型のマッピングの指定は、

  • MyBatis設定ファイル内のtypeHandler要素の属性値として指定

  • @org.apache.ibatis.type.MappedTypesアノテーションと@org.apache.ibatis.type.MappedJdbcTypesアノテーションに指定

  • MyBatis3から提供されているTypeHandlerの基底クラス(org.apache.ibatis.type.BaseTypeHandler)を継承することで指定

する方法がある。

詳しくは、「MyBatis 3 REFERENCE DOCUMENTATION(Configuration XML-typeHandlers-)」を参照されたい。


6.2.2.3.5.3. フィールド毎にTypeHandlerを設定する

アプリケーション全体に適用するのではなく、フィールド毎に個別のTypeHandlerを指定する事も可能である。これは、アプリケーション全体に適用しているTypeHandlerを上書きする際に使用する。

<?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="com.example.domain.repository.image.ImageRepository">
    <resultMap id="resultMapImage" type="Image">
        <id property="id" column="id" />
        <!-- (1) -->
        <result property="imageData" column="image_data" typeHandler="XxxBlobInputStreamTypeHandler" />
        <result property="createdAt" column="created_at"  />
    </resultMap>
    <select id="findById" parameterType="string" resultMap="resultMapImage">
        SELECT
            id
            ,image_data
            ,created_at
        FROM
            t_image
        WHERE
            id = #{id}
    </select>
    <insert id="create" parameterType="Image">
        INSERT INTO
            t_image
        (
            id
            ,image_data
            ,created_at
        )
        VALUES
        (
            #{id}
            /* (2) */
            ,#{imageData,typeHandler=XxxBlobInputStreamTypeHandler}
            ,#{createdAt}
        )
    </insert>
</mapper>

項番

説明

検索結果(ResultSet)から値を取得する際は、id又はresult要素のtypeHandler属性に適用するTypeHandlerを指定する。

PreparedStatementに値を設定する際は、インラインパラメータのtypeHandler属性に適用するTypeHandlerを指定する。

TypeHandlerをフィールド毎に個別に指定する場合は、TypeHandlerのクラスにTypeAliasを設けることを推奨する。

TypeAliasの設定方法については、「TypeAliasの設定」を参照されたい。


6.2.2.4. データベースアクセス処理の実装

MyBatis3の機能を使用してデータベースにアクセスするための、具体的な実装方法について説明する。

6.2.2.4.1. Repositoryインタフェースの作成

Entity毎にRepositoryインタフェースを作成する。

package com.example.domain.repository.todo;

// (1)
public interface TodoRepository {
}

項番

説明

JavaのインタフェースとしてRepositoryインタフェースを作成する。

上記例では、TodoというEntityに対するRepositoryインタフェースを作成している。


6.2.2.4.2. マッピングファイルの作成

Repositoryインタフェース毎にマッピングファイルを作成する。

<?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">
<!-- (1)  -->
<mapper namespace="com.example.domain.repository.todo.TodoRepository">
</mapper>

項番

説明

mapper要素のnamespace属性に、Repositoryインタフェースの完全修飾クラス名(FQCN)を指定する。

Note

マッピングファイルの格納先について

マッピングファイルの格納先は、

  • MyBatis3が自動的にマッピングファイルを読み込むために定めたルールに則ったディレクトリ

  • 任意のディレクトリ

のどちらかを選択することができる。

本ガイドラインでは、MyBatis3が定めたルールに則ったディレクトリに格納し、マッピングファイルを自動的に読み込む仕組みを利用することを推奨する。

マッピングファイルを自動的に読み込ませるためには、Repositoryインタフェースのパッケージ階層と同じ階層で、マッピングファイルをクラスパス上に格納する必要がある。

具体的には、 com.example.domain.repository.todo.TodoRepositoryというRepositoryインターフェースに対するマッピングファイル(TodoRepository.xml)は、projectName-domain/src/main/resources/com/example/domain/repository/todoディレクトリに格納すればよい。


6.2.2.4.3. CRUD処理の実装

ここからは、基本的なCRUD処理の実装方法と、SQL実装時の考慮点について説明を行う。

基本的なCRUD処理として、以下の処理の実装方法について説明を行う。

  • 検索結果とJavaBeanのマッピング方法

  • Entityの検索処理

  • Entityの登録処理

  • Entityの更新処理

  • Entityの削除処理

  • 動的SQLの実装

    Note

    MyBatis3を使用してCRUD処理を実装する際は、検索したEntityがローカルキャッシュと呼ばれる領域にキャッシュされる仕組みになっている点を意識しておく必要がある。

    MyBatis3が提供するローカルキャッシュのデフォルトの動作は以下の通りである。

    • ローカルキャッシュは、トランザクション単位で管理する。

    • Entityのキャッシュは、「ステートメントID + 組み立てられたSQLのパターン + 組み立てられたSQLにバインドするパラメータ値 + ページ位置(取得範囲)」毎に行う。

    つまり、同一トランザクション内の処理において、MyBatisが提供している検索APIを全て同じパラメータで呼び出すと、2回目以降はSQLを発行せずに、キャッシュされているEntityのインスタンスが返却される。

    ここでは、MyBatisのAPIが返却するEntityとローカルキャッシュで管理しているEntityが同じインスタンスという点を意識しておいてほしい。

    Tip

    ローカルキャッシュは、ステートメント単位で管理するように変更する事もできる。

    ローカルキャッシュをステートメント単位で管理する場合、MyBatisは毎回SQLを実行して最新のEntityを取得する。


SQL実装時の考慮点として、以下の点について説明を行う。


具体的な実装方法の説明を行う前に、以降の説明で登場するコンポーネントについて、簡単に説明しておく。

項番

コンポーネント

説明

Entity

アプリケーションで扱う業務データを保持するJavaBeanクラス。

Entityの詳細については、「Entityの実装」を参照されたい。

Repositoryインタフェース

EntityのCRUD操作を行うためのメソッドを定義するインタフェース。

Repositoryの詳細については、「Repositoryの実装」を参照されたい。

Serviceクラス

業務ロジックを実行するためのクラス。

Serviceの詳細については、「Serviceの実装」を参照されたい。

Note

本ガイドラインでは、アーキテクチャ上の用語を統一するために、MyBatis3のMapperインタフェースの事をRepositoryインタフェースと呼んでいる。

以降の説明では、「Entityの実装」「Repositoryの実装」「Serviceの実装」を読んでいる前提で説明を行う。


6.2.2.5. 検索結果とJavaBeanのマッピング方法

Entityの検索処理の説明を行う前に、検索結果とJavaBeanのマッピング方法について説明を行う。

MyBatis3では、検索結果(ResultSet)をJavaBean(Entity)にマッピングする方法として、自動マッピング と手動マッピングの2つの方法が用意されている。
それぞれ特徴があるので、プロジェクトの特性やアプリケーションで実行するSQLの特性などを考慮して、使用するマッピング方法を決めて頂きたい。

Note

使用するマッピング方法について

本ガイドラインでは、

  • シンプルなマッピング(単一オブジェクトへのマッピング)の場合は自動マッピングを使用し、高度なマッピング(関連オブジェクトへのマッピング)が必要な場合は手動マッピングを使用する。

  • 一律手動マッピングを使用する

の、2つの案を提示する。これは、上記2案のどちらかを選択する事を強制するものではなく、あくまで選択肢のひとつと考えて頂きたい。

アーキテクトは、自動マッピングと手動マッピングを使うケースの判断基準をプログラマに対して明確に示すことで、アプリケーション全体として統一されたマッピング方法が使用されるように心がけてほしい。

以下に、自動マッピングと手動マッピングに対して、それぞれの特徴と使用例を説明する。


6.2.2.5.1. 検索結果の自動マッピング

MyBatis3では、検索結果(ResultSet)のカラムとJavaBeanのプロパティをマッピングする方法として、 カラム名とプロパティ名を一致させることで、自動的に解決する仕組みを提供している。

Note

自動マッピングの特徴について

自動マッピングを使用すると、マッピングファイルには実行するSQLのみ記述すればよいため、 マッピングファイルの記述量を減らすことができる点が特徴である。

記述量が減ることで、単純ミスの削減や、カラム名やプロパティ名変更時の修正箇所の削減といった効果も期待できる。

ただし、自動マッピングが行えるのは、単一オブジェクトに対するマッピングのみである。 ネストした関連オブジェクトに対してマッピングを行いたい場合は、手動マッピングを使用する必要がある。

Tip

カラム名について

ここで言うカラム名とは、テーブルの物理的なカラム名ではなく、SQLを発行して取得した検索結果(ResultSet)がもつカラム名の事である。そのため、AS句を使うことで、物理的なカラム名とJavaBeanのプロパティ名を一致させることは、比較的容易に行うことができる。


以下に、自動マッピングを使用して検索結果をJavaBeanにマッピングする実装例を示す。

  • projectName-domain/src/main/resources/com/example/domain/repository/todo/TodoRepository.xml

    <?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="com.example.domain.repository.todo.TodoRepository">
    
        <select id="findByTodoId" parameterType="string" resultType="Todo">
            SELECT
                todo_id AS "todoId", /* (1) */
                todo_title AS "todoTitle",
                finished, /* (2) */
                created_at AS "createdAt",
                version
            FROM
                t_todo
            WHERE
                todo_id = #{todoId}
        </select>
    
    </mapper>
    

    項番

    説明

    テーブルの物理カラム名とJavaBeanのプロパティ名が異なる場合は、AS句を使用して一致させることで、自動マッピング対象にすることができる。

    テーブルの物理カラム名とJavaBeanのプロパティ名が一致している場合は、AS句を指定する必要はない。

  • JavaBean

    package com.example.domain.model;
    
    import java.io.Serializable;
    import java.util.Date;
    
    public class Todo implements Serializable {
    
        private static final long serialVersionUID = 1L;
    
        private String todoId;
    
        private String todoTitle;
    
        private boolean finished;
    
        private Date createdAt;
    
        private long version;
    
        public String getTodoId() {
            return todoId;
        }
    
        public void setTodoId(String todoId) {
            this.todoId = todoId;
        }
    
        public String getTodoTitle() {
            return todoTitle;
        }
    
        public void setTodoTitle(String todoTitle) {
            this.todoTitle = todoTitle;
        }
    
        public boolean isFinished() {
            return finished;
        }
    
        public void setFinished(boolean finished) {
            this.finished = finished;
        }
    
        public Date getCreatedAt() {
            return createdAt;
        }
    
        public void setCreatedAt(Date createdAt) {
            this.createdAt = createdAt;
        }
    
        public long getVersion() {
            return version;
        }
    
        public void setVersion(long version) {
            this.version = version;
        }
    
    }
    

    Tip

    アンダースコア区切りのカラム名とキャメルケース形式のプロパティ名のマッピング方法について

    上記例では、アンダースコア区切りのカラム名とキャメルケース形式のプロパティ名の違いについてAS句を使って吸収しているが、アンダースコア区切りのカラム名とキャメルケース形式のプロパティ名の違いを吸収するだけならば、MyBatis3の設定を変更する事で実現可能である。


テーブルの物理カラム名をアンダースコア区切りにしている場合は、MyBatis設定ファイル(mybatis-config.xml)に以下の設定を追加することで、キャメルケースのJavaBeanのプロパティに自動マッピングする事ができる。

  • projectName-domain/src/main/xxx/yyy/zzz/config/app/mybatis/MybatisConfig.java

    public static Configuration configuration() throws IOException {
        Configuration configuration = new Configuration();
    
        // omitted
    
        setSettings(configuration);
    
        return configuration;
    }
    
    private static void setSettings(Configuration configuration) {
    
        // omitted
    
        configuration.setMapUnderscoreToCamelCase(true); // (3)
    }
    

    項番

    説明

    mapUnderscoreToCamelCasetrueにする設定を追加する。

    設定をtrueにすると、アンダースコア区切りのカラム名がキャメルケース形式に自動変換される。
    具体例としては、カラム名がtodo_idの場合、todoIdに変換されてマッピングが行われる。
  • projectName-domain/src/main/resources/com/example/domain/repository/todo/TodoRepository.xml

    <?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="com.example.domain.repository.todo.TodoRepository">
    
        <select id="findByTodoId" parameterType="string" resultType="Todo">
            SELECT
                todo_id, /* (4) */
                todo_title,
                finished,
                created_at,
                version
            FROM
                t_todo
            WHERE
                todo_id = #{todoId}
        </select>
    
    </mapper>
    

    項番

    説明

    アンダースコア区切りのカラム名とキャメルケース形式のプロパティ名の違いを吸収するために、AS句の指定が不要になるため、よりシンプルなSQLとなる。


6.2.2.5.2. 検索結果の手動マッピング

MyBatis3では、検索結果(ResultSet)のカラムとJavaBeanのプロパティの対応付けを、 マッピングファイルに定義する事で、手動で解決する仕組みを用意している。

Note

手動マッピングの特徴について

手動マッピングを使用すると、検索結果(ResultSet)のカラムとJavaBeanのプロパティの対応付けを、マッピングファイルに1項目ずつ定義することになる。そのため、マッピングの柔軟性が非常に高く、より複雑なマッピングを実現する事ができる点が特徴である。

手動マッピングは、

  • アプリケーションが扱うデータモデル(JavaBean)と物理テーブルのレイアウトが一致しない

  • JavaBeanがネスト構造になっている(別のJavaBeanをネストしている)

といったケースにおいて、検索結果(ResultSet)のカラムとJavaBeanのプロパティをマッピングする際に力を発揮するマッピング方法である。

また、自動マッピングに比べて効率的にマッピングを行う事ができる。

処理の効率性を優先するアプリケーションの場合は、自動マッピングの代わりに手動マッピングを使用した方がよい。


以下に、手動マッピングを使用して検索結果をJavaBeanにマッピングする実装例を示す。
ここでは、手動マッピングの使用方法を示す事が目的なので、自動マッピングでもマッピング可能なもっともシンプルなパターンを例に、説明を行う。

実践的なマッピングの実装例については、

を参照されたい。

  • projectName-domain/src/main/resources/com/example/domain/repository/todo/TodoRepository.xml

    <?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="com.example.domain.repository.todo.TodoRepository">
    
        <!-- (1) -->
        <resultMap id="todoResultMap" type="Todo">
            <!-- (2) -->
            <id column="todo_id" property="todoId" />
            <!-- (3) -->
            <result column="todo_title" property="todoTitle" />
            <result column="finished" property="finished" />
            <result column="created_at" property="createdAt" />
            <result column="version" property="version" />
        </resultMap>
    
        <!-- (4) -->
        <select id="findByTodoId" parameterType="string" resultMap="todoResultMap">
            SELECT
                todo_id,
                todo_title,
                finished,
                created_at,
                version
            FROM
                t_todo
            WHERE
                todo_id = #{todoId}
        </select>
    
    </mapper>
    

    項番

    説明

    <resultMap>要素に、検索結果(ResultSet)とJavaBeanのマッピング定義を行う。

    id属性にマッピングを識別するためのIDを、type属性にマッピングするJavaBeanのクラス名(又はエイリアス名)を指定する。

    <resultMap>要素の詳細は、「MyBatis 3 REFERENCE DOCUMENTATION(Mapper XML Files-resultMap-) 」を参照されたい。

    検索結果(ResultSet)のID(PK)のカラムとJavaBeanのプロパティのマッピングを行う。

    ID(PK)のマッピングは、<id>要素を使って指定する。 column属性には検索結果(ResultSet)のカラム名、property属性にはJavaBeanのプロパティ名を指定する。

    <id>要素の詳細は、「MyBatis 3 REFERENCE DOCUMENTATION(Mapper XML Files-id & result-) 」を参照されたい。

    検索結果(ResultSet)のID(PK)以外のカラムとJavaBeanのプロパティのマッピングを行う。

    ID(PK)以外のマッピングは、<result>要素を使って指定する。 column属性には検索結果(ResultSet)のカラム名、property属性にはJavaBeanのプロパティ名を指定する。

    <result>要素の詳細は、「MyBatis 3 REFERENCE DOCUMENTATION(Mapper XML Files-id & result-) 」を参照されたい。

    <select>要素のresultMap属性に、適用するマッピング定義のIDを指定する。

    Note

    id要素とresult要素の使い分けについて

    <id>要素と<result>要素は、どちらも検索結果(ResultSet)のカラムとJavaBeanのプロパティをマッピングするための要素であるが、ID(PK)カラムに対してマッピングは、<id>要素を使うことを推奨する。

    理由は、ID(PK)カラムに対して<id>要素を使用してマッピングを行うと、MyBatis3が提供しているオブジェクトのキャッシュ制御の処理や、関連オブジェクトへのマッピングの処理のパフォーマンスを、全体的に向上させることが出来るためである。


6.2.2.6. Entityの検索処理

Entityの検索処理の実装方法について、目的別に説明を行う。

Entityの検索処理の実装方法に対する説明を読む前に、「検索結果とJavaBeanのマッピング方法」を一読して頂きたい。

以降の説明では、アンダースコア区切りのカラム名をキャメルケース形式のプロパティ名に自動でマッピングする設定を有効にした場合の実装例となる。

  • projectName-domain/src/main/xxx/yyy/zzz/config/app/mybatis/MybatisConfig.java

    public static Configuration configuration() throws IOException {
        Configuration configuration = new Configuration();
    
        // omitted
    
        setSettings(configuration);
    
        return configuration;
    }
    
    private static void setSettings(Configuration configuration) {
    
        // omitted
    
        configuration.setMapUnderscoreToCamelCase(true);
    }
    

6.2.2.6.1. 単一キーのEntityの取得

PKが単一カラムで構成されるテーブルより、PKを指定してEntityを1件取得する際の実装例を以下に示す。

  • Repositoryインタフェースにメソッドを定義する。

    package com.example.domain.repository.todo;
    
    import com.example.domain.model.Todo;
    
    public interface TodoRepository {
    
        // (1)
        Todo findByTodoId(String todoId);
    
    }
    

    項番

    説明

    上記例では、引数に指定されたtodoId(PK)に一致するTodoオブジェクトを1件取得するためのメソッドとして、findByTodoIdメソッドを定義している。


  • マッピングファイルに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="com.example.domain.repository.todo.TodoRepository">
    
        <!-- (2) -->
        <select id="findByTodoId" parameterType="string" resultType="Todo">
            /* (3) */
            SELECT
                todo_id,
                todo_title,
                finished,
                created_at,
                version
            FROM
                t_todo
            /* (4) */
            WHERE
                todo_id = #{todoId}
        </select>
    
    </mapper>
    

    項番

    属性

    説明

    -

    select要素の中に、検索結果が0~1件となるSQLを実装する。

    上記例では、ID(PK)が一致するレコードを取得するSQLを実装している。

    select要素の詳細については、「MyBatis3 REFERENCE DOCUMENTATION (Mapper XML Files-select-)」を参照されたい。

    id

    Repositoryインタフェースに定義したメソッドのメソッド名を指定する。

    parameterType

    パラメータ完全修飾クラス名(又はエイリアス名)を指定する。

    resultType

    検索結果(ResultSet)をマッピングするJavaBeanの完全修飾クラス名(又はエイリアス名)を指定する。

    手動マッピングを使用する場合は、resultType属性の代わりにresultMap属性を使用して、適用するマッピング定義を指定する。
    手動マッピングについては、「検索結果の手動マッピング」を参照されたい。

    -

    取得対象のカラムを指定する。

    上記例では、検索結果(ResultSet)をJavaBeanへマッピングする方法として、自動マッピングを使用している。
    自動マッピングについては、「検索結果の自動マッピング」を参照されたい。

    -

    WHERE句に検索条件を指定する。

    検索条件にバインドする値は、#{variableName}形式のバインド変数として指定する。上記例では、#{todoId}がバインド変数となる。

    Repositoryインタフェースの引数の型がStringのような単純型の場合は、バインド変数名は任意の名前でよいが、引数の型がJavaBeanの場合は、バインド変数名にはJavaBeanのプロパティ名を指定する必要がある。

    Note

    単純型のバインド変数名について

    Stringのような単純型の場合は、バインド変数名に制約はないが、メソッドの引数名と同じ値にしておくことを推奨する。


  • ServiceクラスにRepositoryをDIし、Repositoryインターフェースのメソッドを呼び出す。

    package com.example.domain.service.todo;
    
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;
    import org.terasoluna.gfw.common.exception.ResourceNotFoundException;
    import org.terasoluna.gfw.common.message.ResultMessages;
    
    import com.example.domain.model.Todo;
    import com.example.domain.repository.todo.TodoRepository;
    
    import jakarta.inject.Inject;
    
    @Transactional
    @Service
    public class TodoServiceImpl implements TodoService {
    
        // (5)
        @Inject
        TodoRepository todoRepository;
    
        @Transactional(readOnly = true)
        @Override
        public Todo getTodo(String todoId) {
            // (6)
            Todo todo = todoRepository.findByTodoId(todoId);
            if (todo == null) { // (7)
                throw new ResourceNotFoundException(ResultMessages.error().add(
                        "e.ex.td.5001", todoId));
            }
            return todo;
        }
    
    }
    

    項番

    説明

    ServiceクラスにRepositoryインターフェースをDIする。

    Repositoryインターフェースのメソッドを呼び出し、Entityを1件取得する。

    検索結果が0件の場合はnullが返却されるため、 必要に応じてEntityが取得できなかった時の処理を実装する。

    上記例では、Entityが取得できなかった場合は、リソース未検出エラーを発生させている。


6.2.2.6.2. 複合キーのEntityの取得

PKが複数カラムで構成されるテーブルより、PKを指定してEntityを1件取得する際の実装例を以下に示す。
基本的な構成は、PKが単一カラムで構成される場合と同じであるが、Repositoryインタフェースのメソッド引数の指定方法が異なる。
  • Repositoryインタフェースにメソッドを定義する。

    package com.example.domain.repository.order;
    
    import org.apache.ibatis.annotations.Param;
    
    import com.example.domain.model.OrderHistory;
    
    public interface OrderHistoryRepository {
    
        // (1)
        OrderHistory findByIds(@Param("orderId") String orderId,
                @Param("historyId") int historyId);
    
    }
    

    項番

    説明

    PKを構成するカラムに対応する引数を、メソッドに定義する。

    上記例では、受注の変更履歴を管理するテーブルのPKとして、orderIdhistoryIdを引数に定義している。

    Tip

    メソッド引数を複数指定する場合のバインド変数名について

    Repositoryインタフェースのメソッド引数を複数指定する場合は、引数に@org.apache.ibatis.annotations.Paramアノテーションを指定することを推奨する。

    @Paramアノテーションのvalue属性には、マッピングファイルから値を参照する際に指定する「バインド変数名」を指定する。

    上記例だと、マッピングファイルから#{orderId}及び#{historyId}と指定することで、引数に指定された値を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="com.example.domain.repository.order.OrderHistoryRepository">
    
        <select id="findByIds" resultType="OrderHistory">
            SELECT
                order_id,
                history_id,
                order_name,
                operation_type,
                created_at"
            FROM
                t_order_history
            WHERE
                order_id = #{orderId}
            AND
                history_id = #{historyId}
        </select>
    
    </mapper>
    

    @Paramアノテーションの指定は必須ではないが、指定しないと以下に示すような機械的なバインド変数名を指定する必要がある。

    @Paramアノテーションの指定しない場合のバインド変数名は、「”param” + 引数の宣言位置(1から開始)」という名前になるため、ソースコードのメンテナンス性及び可読性を損なう要因となる。

    <!-- omitted -->
    
    WHERE
        order_id = #{param1}
    AND
        history_id = #{param2}
    
    <!-- omitted -->
    

    MyBatis 3.4.1以降では、JDK 8 から追加されたコンパイルオプション(-parameters)を使用することで、@Paramアノテーションを省略する事ができる。


6.2.2.6.3. Entityの検索

検索結果が0~N件となるSQLを発行し、Entityを複数件取得する際の実装例を以下に示す。

Warning

検索結果が大量のデータになる可能性がある場合は、「ResultHandlerの実装」の利用を検討すること。


  • Entityを複数件取得するためのメソッドを定義する。

    package com.example.domain.repository.todo;
    
    import java.util.List;
    
    import com.example.domain.model.Todo;
    
    public interface TodoRepository {
    
        // (1)
        List<Todo> findAllByCriteria(TodoCriteria criteria);
    
    }
    

    項番

    説明

    上記例では、検索条件を保持するJavaBean(TodoCriteria)に一致するTodoオブジェクトをリスト形式で複数件取得するためのメソッドとして、findAllByCriteriaメソッドを定義している。

    Tip

    上記例では、メソッドの返り値にjava.util.Listを指定しているが、検索結果をjava.util.Mapとして受け取る事も出来る。

    Mapで受け取る場合は、

    • MapkeyにはPKの値

    • MapvalueにはEntityオブジェクト

    を格納する事になる。

    検索結果をMapで受け取る場合、java.util.HashMapのインスタンスが返却されるため、Mapの並び順は保証されないという点に注意すること。

    以下に、実装例を示す。

    package com.example.domain.repository.todo;
    
    import java.util.Map;
    
    import com.example.domain.model.Todo;
    import org.apache.ibatis.annotations.MapKey;
    
    public interface TodoRepository {
    
        @MapKey("todoId")
        Map<String, Todo> findAllByCriteria(TodoCriteria criteria);
    
    }
    

    検索結果をMapで受け取る場合は、@org.apache.ibatis.annotations.MapKeyアノテーションをメソッドに指定する。アノテーションのvalue属性には、Mapkeyとして扱うプロパティ名を指定する。

    上記例では、TodoオブジェクトのPK(todoId)を指定している。


  • 検索条件を保持するJavaBeanを作成する。

    package com.example.domain.repository.todo;
    
    import java.io.Serializable;
    import java.util.Date;
    
    public class TodoCriteria implements Serializable {
    
        private static final long serialVersionUID = 1L;
    
        private String title;
    
        private Date createdAt;
    
        public String getTitle() {
            return title;
        }
    
        public void setTitle(String title) {
            this.title = title;
        }
    
        public Date getCreatedAt() {
            return createdAt;
        }
    
        public void setCreatedAt(Date createdAt) {
            this.createdAt = createdAt;
        }
    
    }
    

    Note

    検索条件を保持するためのJavaBeanの作成について

    検索条件を保持するためのJavaBeanの作成は必須ではないが、格納されている値の役割が明確になるため、JavaBeanを作成することを推奨する。ただし、JavaBeanを作成しない方法で実装してもよい。

    アーキテクトは、JavaBeanを作成するケースと作成しないケースの判断基準をプログラマに対して明確に示すことで、アプリケーション全体として統一された作りになるようにすること。

    JavaBeanを作成しない場合の実装例を以下に示す。

    package com.example.domain.repository.todo;
    
    import java.util.List;
    
    import com.example.domain.model.Todo;
    
    public interface TodoRepository {
    
        List<Todo> findAllByCriteria(@Param("title") String title,
                @Param("createdAt") Date createdAt);
    
    }
    

    JavaBeanを作成しない場合は、検索条件を1項目ずつ引数に宣言し、@Paramアノテーションのvalue属性に「バインド変数名」を指定する。

    上記のようなメソッドを定義することで、複数の検索条件をSQLに引き渡すことができる。


  • マッピングファイルに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="com.example.domain.repository.todo.TodoRepository">
    
        <!-- (2) -->
        <select id="findAllByCriteria" parameterType="TodoCriteria" resultType="Todo">
            <![CDATA[
            SELECT
                todo_id,
                todo_title,
                finished,
                created_at,
                version
            FROM
                t_todo
            WHERE
                todo_title LIKE #{title} || '%' ESCAPE '~'
            AND
                created_at < #{createdAt}
            /* (3) */
            ORDER BY
                todo_id
            ]]>
        </select>
    
    </mapper>
    

    項番

    説明

    select要素の中に、検索結果が0~N件となるSQLを実装する。

    上記例では、todo_titlecreated_atが指定した条件に一致するTodoレコードを取得する実装している。

    ソート条件を指定する。

    複数件のレコードを取得する場合は、ソート条件を指定する。
    特に画面に表示するレコードを取得するSQLでは、ソート条件の指定は必須である。

    Tip

    CDATAセクションの活用方法について

    SQL内にXMLのエスケープが必要な文字(”<“や”>“など)を指定する場合は、CDATAセクションを使用すると、SQLの可読性を保つことができる。

    CDATAセクションを使用しない場合は、&lt;&gt;といったエンティティ参照文字を指定する必要があり、SQLの可読性を損なう要因となる。

    上記例では、created_atに対する条件として”<“を使用しているため、CDATAセクションを指定している。


6.2.2.6.4. Entityの件数の取得

検索条件に一致するEntityの件数を取得する際の実装例を以下に示す。

  • 検索条件に一致するEntityの件数を取得するためのメソッドを定義する。

    package com.example.domain.repository.todo;
    
    public interface TodoRepository {
    
        // (1)
        long countByFinished(boolean finished);
    
    }
    

    項番

    説明

    件数を取得ためのメソッドの返り値は、数値型(intlongなど)を指定する。

    上記例では、longを指定している。


  • マッピングファイルに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="com.example.domain.repository.todo.TodoRepository">
    
        <!-- (2) -->
        <select id="countByFinished" parameterType="_boolean" resultType="_long">
            SELECT
                COUNT(*)
            FROM
                t_todo
            WHERE
                finished = #{finished}
        </select>
    
    </mapper>
    

    項番

    説明

    件数を取得するSQLを実行する。

    resultType属性には、返り値の型を指定する。

    上記例では、プリミティブ型のlongを指定するためのエイリアス名を指定している。

    Tip

    プリミティブ型のエイリアス名について

    プリミティブ型のエイリアス名は、先頭に”_“(アンダースコア)を指定する必要がある。

    _“(アンダースコア)を指定しない場合は、プリミティブのラッパ型(java.lang.Longなど)として扱われる。


6.2.2.6.5. Entityのページネーション検索(MyBatis3標準方式)

MyBatis3の取得範囲指定機能を使用してEntityを検索する際の実装例を以下に示す。

MyBatisでは取得範囲を指定するクラスとしてorg.apache.ibatis.session.RowBoundsクラスが用意されており、SQLに取得範囲の条件を記述する必要がない。

Warning

検索条件に一致するデータ件数が多くなる場合の注意点について

MyBatis3標準の方式は、検索結果(ResultSet)のカーソルを移動することで、取得範囲外のデータをスキップする方式である。そのため、検索条件に一致するデータ件数に比例して、メモリ枯渇やカーソル移動処理の性能劣化が発生する可能性が高くなる。

カーソルの移動処理は、JDBCの結果セット型に応じて以下の2種類がサポートされており、デフォルトの動作は、JDBCドライバのデフォルトの結果セット型に依存する。

  • 結果セット型がFORWARD_ONLYの場合は、ResultSet#next()を繰り返し呼び出して取得範囲外のデータをスキップする。

  • 結果セット型がSCROLL_SENSITIVE又はSCROLL_INSENSITIVEの場合は、ResultSet#absolute(int)を呼び出して取得範囲外のデータをスキップする。

ResultSet#absolute(int)を使用することで、性能劣化を最小限に抑える事ができる可能性はあるが、JDBCドライバの実装次第であり、内部でResultSet#next()と同等の処理が行われている場合は、メモリ枯渇や性能劣化が発生する可能性を抑える事はできない。

検索条件に一致するデータ件数が多くなる可能性がある場合は、MyBatis3標準方式のページネーション検索ではなく、SQL絞り込み方式の採用を検討した方がよい。


  • Entityのページネーション検索を行うためのメソッドを定義する。

    ackage com.example.domain.repository.todo;
    
    import java.util.List;
    
    import org.apache.ibatis.session.RowBounds;
    
    import com.example.domain.model.Todo;
    
    public interface TodoRepository {
    
        // (1)
        long countByCriteria(TodoCriteria criteria);
    
        // (2)
        List<Todo> findPageByCriteria(TodoCriteria criteria,
            RowBounds rowBounds);
    
    }
    

    項番

    説明

    検索条件に一致するEntityの総件数を取得するメソッドを定義する。

    検索条件に一致するEntityの中から、取得範囲のEntityを抽出するメソッドを定義する。

    定義したメソッドの引数として、取得範囲の情報(offsetとlimit)を保持するRowBoundsを指定する。


  • マッピングファイルにSQLを定義する。

    検索結果から該当範囲のレコードを抽出する処理は、MyBatis3が行うため、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="com.example.domain.repository.todo.TodoRepository">
    
        <select id="countByCriteria" parameterType="TodoCriteria" resultType="_long">
            <![CDATA[
            SELECT
                COUNT(*)
            FROM
                t_todo
            WHERE
                todo_title LIKE #{title} || '%' ESCAPE '~'
            AND
                created_at < #{createdAt}
            ]]>
        </select>
    
        <select id="findPageByCriteria" parameterType="TodoCriteria" resultType="Todo">
            <![CDATA[
            SELECT
                todo_id,
                todo_title,
                finished,
                created_at,
                version
            FROM
                t_todo
            WHERE
                todo_title LIKE #{title} || '%' ESCAPE '~'
            AND
                created_at < #{createdAt}
            ORDER BY
                todo_id
            ]]>
        </select>
    
    </mapper>
    

    Note

    WHERE句の共通化について

    ページネーション検索を実現する場合、「検索条件に一致するEntityの総件数を取得するSQL」と「 検索条件に一致するEntityのリストを取得するSQL」で指定するWHERE句は、MyBatis3のinclude機能を使って共通化することを推奨する。

    上記SQLのWHERE句を共通化した場合、以下のような定義となる。

    詳細は、「SQL文の共有」を参照されたい。

    <sql id="findPageByCriteriaWherePhrase">
        <![CDATA[
        WHERE
            todo_title LIKE #{title} || '%' ESCAPE '~'
        AND
            created_at < #{createdAt}
        ]]>
    </sql>
    
    <select id="countByCriteria" parameterType="TodoCriteria" resultType="_long">
        SELECT
            COUNT(*)
        FROM
            t_todo
        <include refid="findPageByCriteriaWherePhrase"/>
    </select>
    
    <select id="findPageByCriteria" parameterType="TodoCriteria" resultType="Todo">
        SELECT
            todo_id,
            todo_title,
            finished,
            created_at,
            version
        FROM
            t_todo
        <include refid="findPageByCriteriaWherePhrase"/>
        ORDER BY
            todo_id
    </select>
    

    Note

    結果セット型を明示的に指定する方法について

    結果セット型を明示的に指定する場合は、resultSetType属性に結果セット型を指定する。

    JDBCドライバのデフォルトの結果セット型が、FORWARD_ONLYの場合は、SCROLL_INSENSITIVEを指定することを推奨する。

    <select id="findPageByCriteria" parameterType="TodoCriteria" resultType="Todo"
        resultSetType="SCROLL_INSENSITIVE">
        <!-- omitted -->
    </select>
    

  • Serviceクラスにページネーション検索処理を実装する。

    // omitted
    
    @Transactional
    @Service
    public class TodoServiceImpl implements TodoService {
    
        @Inject
        TodoRepository todoRepository;
    
        // omitted
    
        @Transactional(readOnly = true)
        @Override
        public Page<Todo> searchTodos(TodoCriteria criteria, Pageable pageable) {
            // (3)
            long total = todoRepository.countByCriteria(criteria);
            List<Todo> todos;
            if (0 < total) {
                // (4)
                RowBounds rowBounds = new RowBounds((int) pageable.getOffset(),
                    pageable.getPageSize());
                // (5)
                todos = todoRepository.findPageByCriteria(criteria, rowBounds);
            } else {
                // (6)
                todos = Collections.emptyList();
            }
            // (7)
            return new PageImpl<>(todos, pageable, total);
        }
    
        // omitted
    
    }
    

    項番

    説明

    まず、検索条件に一致するEntityの総件数を取得する。

    検索条件に一致するEntityが存在する場合は、ページネーション検索の取得範囲を指定するRowBoundsオブジェクトを生成する。

    RowBoundsの第1引数(offset)には「スキップ件数」、第2引数(limit)には「最大取得件数」を指定する。
    引数に指定する値、Spring Data Commonsから提供されているPageableオブジェクトのgetOffsetメソッドとgetPageSizeメソッドを呼び出して取得した値を指定すればよい。

    具体的には、

    • offsetに”0“、limitに20を指定した場合、1~20件目

    • offsetに20、limitに20を指定した場合、21~40件目

    が取得範囲となる。

    Repositoryのメソッドを呼び出し、検索条件に一致した取得範囲のEntityを取得する。

    検索条件に一致するEntityが存在しない場合は、空のリストを検索結果に設定する。

    ページ情報(org.springframework.data.domain.PageImpl)を作成し返却する。


6.2.2.6.6. Entityのページネーション検索(SQL絞り込み方式)

データベースから提供されている範囲検索の仕組みを使用してEntityを検索する際の実装例を以下に示す。

SQL絞り込み方式は、データベースから提供されている範囲検索の仕組みを使用するため、MyBatis3標準方式に比べて効率的に取得範囲のEntityを取得することができる。

Note

検索条件に一致するデータ件数が大量にある場合は、SQL絞り込み方式を採用する事を推奨する。


  • Entityのページネーション検索を行うためのメソッドを定義する。

    package com.example.domain.repository.todo;
    
    import java.util.List;
    
    import org.apache.ibatis.annotations.Param;
    import org.springframework.data.domain.Pageable;
    
    import com.example.domain.model.Todo;
    
    public interface TodoRepository {
    
        // (1)
        long countByCriteria(
                @Param("criteria") TodoCriteria criteria);
    
        // (2)
        List<Todo> findPageByCriteria(
                @Param("criteria") TodoCriteria criteria,
                @Param("pageable") Pageable pageable);
    }
    

    項番

    説明

    検索条件に一致するEntityの総件数を取得するメソッドを定義する。

    検索条件に一致するEntityの中から、取得範囲のEntityを抽出するメソッドを定義する。

    定義したメソッドの引数として、取得範囲の情報(offsetとlimit)を保持するorg.springframework.data.domain.Pageableを指定する。

    Note

    引数が1つのメソッドに@Paramアノテーションを指定する理由について

    上記例では、引数が1つのメソッド(countByCriteria)に対して@Paramアノテーションを指定している。これは、findPageByCriteriaメソッド呼び出し時に実行されるSQLとWHERE句を共通化するためである。

    @Paramアノテーションを使用して引数にバインド変数名を指定することで、SQL内で指定するバインド変数名のネスト構造を合わせている。

    具体的なSQLの実装例については、次に示す。


  • マッピングファイルにSQLを定義する。

    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="com.example.domain.repository.todo.TodoRepository">
    
        <sql id="findPageByCriteriaWherePhrase">
            <![CDATA[
            /* (3) */
            WHERE
                todo_title LIKE #{criteria.title} || '%' ESCAPE '~'
            AND
                created_at < #{criteria.createdAt}
            ]]>
        </sql>
    
        <select id="countByCriteria" resultType="_long">
            SELECT
                COUNT(*)
            FROM
                t_todo
            <include refid="findPageByCriteriaWherePhrase" />
        </select>
    
        <select id="findPageByCriteria" resultType="Todo">
            SELECT
                todo_id,
                todo_title,
                finished,
                created_at,
                version
            FROM
                t_todo
            <include refid="findPageByCriteriaWherePhrase" />
            ORDER BY
                todo_id
            LIMIT
                #{pageable.pageSize} /* (4) */
            OFFSET
                #{pageable.offset}  /* (4) */
        </select>
    
    </mapper>
    

    項番

    説明

    countByCriteriafindPageByCriteriaメソッドの引数に@Param("criteria")を指定しているため、SQL内で指定するバインド変数名はcriteria.フィールド名の形式となる。

    データベースから提供されている範囲検索の仕組みを使用して、必要なレコードのみ抽出する。

    Pageableオブジェクトのoffsetには「スキップ件数」、pageSizeには「最大取得件数」が格納されている。

    上記例は、データベースとしてH2 Databaseを使用した際の実装例である。


  • Serviceクラスにページネーション検索処理を実装する。

    // omitted
    
    @Transactional
    @Service
    public class TodoServiceImpl implements TodoService {
    
        @Inject
        TodoRepository todoRepository;
    
        // omitted
    
        @Transactional(readOnly = true)
        @Override
        public Page<Todo> searchTodos(TodoCriteria criteria,
                Pageable pageable) {
            long total = todoRepository.countByCriteria(criteria);
            List<Todo> todos;
            if (0 < total) {
                // (5)
                todos = todoRepository.findPageByCriteria(criteria,
                        pageable);
            } else {
                todos = Collections.emptyList();
            }
            return new PageImpl<>(todos, pageable, total);
        }
    
        // omitted
    
    }
    

    項番

    説明

    Repositoryのメソッドを呼び出し、検索条件に一致した取得範囲のEntityを取得する。

    Repositoryのメソッドを呼び出す際は、引数で受け取ったPageableオブジェクトをそのまま渡せばよい。


6.2.2.6.7. Entityのページネーション検索(検索結果のソート)

Pageableオブジェクトのsortプロパティを利用して、SQLで検索結果をソートする実装例を以下に示す。

RepositoryおよびServiceについては、前述のEntityのページネーション検索(SQL絞り込み方式)と同様とし、実装例を省略する。


  • マッピングファイルにSQLを定義する。

    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="com.example.domain.repository.todo.TodoRepository">
    
        <select id="findPageByCriteria" resultType="Todo">
            SELECT
                todo_id,
                todo_title,
                finished,
                created_at,
                version
            FROM
                t_todo
            WHERE
                todo_title LIKE #{criteria.title} || '%' ESCAPE '~'
            AND
            <![CDATA[
                created_at < #{criteria.createdAt}
            ]]>
            <choose>
                <!-- (1)  -->
                <when test="!pageable.sort.isEmpty()">
                    ORDER BY
                    <!-- (2)  -->
                    <foreach item="order" collection="pageable.sort" separator=",">
                        ${order.property}
                        ${order.direction}
                    </foreach>
                </when>
                <!-- (3)  -->
                <otherwise>
                    ORDER BY todo_id
                </otherwise>
            </choose>
            LIMIT
                #{pageable.pageSize}
            OFFSET
                #{pageable.offset}
        </select>
    
    </mapper>
    

    項番

    説明

    Pageableオブジェクトのsortプロパティが空でない場合、ソート条件を指定する。

    sortプロパティに格納されているソート条件をマッピングファイルに引き渡す。
    order.propertyはソートする列、order.directionはASC,DESCなどのソート順を表す。

    具体的にはsort=todo_id,DESC&sort=created_atが指定された場合、ORDER BY todo_id DESC, created_at ASCが生成される。

    ソート条件がセットされていない場合はプライマリキーtodo_idでソートを行う。

    Warning

    ページネーションのSQL Injection対策ついて

    ソート条件は${order.property}${order.direction}のように置換変数による埋め込みを行うため、SQL Injectionが発生しないように注意する必要がある。

    いずれもリクエストパラメータsortで指定した値が格納されるが、不正な値が送信された場合の動作に以下の違いがあり、${order.property}でSQL Injectionが発生する可能性がある。

    • propertyには、送信されたソートする列名の値がそのまま格納される。

    • directionにはASCまたはDESCのどちらかが格納される。それ以外の値が送信された場合はSortHandlerMethodArgumentResolver内で例外となる。

    SQL Injection対策については、SQL Injection対策 を参照されたい。


6.2.2.7. Entityの登録処理

Entityの登録方法について、目的別に実装例を説明する。

6.2.2.7.1. Entityの1件登録

Entityを1件登録する際の実装例を以下に示す。

  • Repositoryインタフェースにメソッドを定義する。

    package com.example.domain.repository.todo;
    
    import com.example.domain.model.Todo;
    
    public interface TodoRepository {
    
        // (1)
        void create(Todo todo);
    
    }
    

    項番

    説明

    上記例では、引数に指定されたTodoオブジェクトを1件登録するためのメソッドとして、createメソッドを定義している。

    Note

    Entityを登録するメソッドの返り値について

    Entityを登録するメソッドの返り値は、基本的にはvoidでよい。

    ただし、SELECTした結果をINSERTするようなSQLを発行する場合は、アプリケーション要件に応じてbooleanや数値型(int又はlong)を返り値とすること。

    • 返り値としてbooleanを指定した場合は、登録件数が0件の際はfalse、登録件数が1件以上の際はtrueが返却される。

    • 返り値として数値型を指定した場合は、登録件数が返却される。


  • マッピングファイルに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="com.example.domain.repository.todo.TodoRepository">
    
        <!-- (2) -->
        <insert id="create" parameterType="Todo">
            INSERT INTO
                t_todo
            (
                todo_id,
                todo_title,
                finished,
                created_at,
                version
            )
            /* (3) */
            VALUES
            (
                #{todoId},
                #{todoTitle},
                #{finished},
                #{createdAt},
                #{version}
            )
        </insert>
    
    </mapper>
    

    項番

    説明

    insert要素の中に、INSERTするSQLを実装する。

    id属性には、Repositoryインタフェースに定義したメソッドのメソッド名を指定する。

    insert要素の詳細については、「MyBatis3 REFERENCE DOCUMENTATION (Mapper XML Files-insert, update and delete-)」を参照されたい。

    VALUE句にレコード登録時の設定値を指定する。

    VALUE句にバインドする値は、#{variableName}形式のバインド変数として指定する。
    上記例では、Repositoryインタフェースの引数としてJavaBean(Todo)を指定しているため、バインド変数名にはJavaBeanのプロパティ名を指定する。

  • ServiceクラスにRepositoryをDIし、Repositoryインターフェースのメソッドを呼び出す。

    package com.example.domain.service.todo;
    
    import java.util.Date;
    import java.util.UUID;
    
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;
    import org.terasoluna.gfw.common.time.ClockFactory;
    
    import com.example.domain.model.Todo;
    import com.example.domain.repository.todo.TodoRepository;
    
    import jakarta.inject.Inject;
    
    @Transactional
    @Service
    public class TodoServiceImpl implements TodoService {
    
        // (4)
        @Inject
        TodoRepository todoRepository;
    
        @Inject
        ClockFactory clockFactory;
    
        @Override
        public Todo create(Todo todo) {
            // (5)
            todo.setTodoId(UUID.randomUUID().toString());
            todo.setCreatedAt(Date.from(clockFactory.fixed().instant()));
            todo.setFinished(false);
            todo.setVersion(1);
            // (6)
            todoRepository.create(todo);
            // (7)
            return todo;
        }
    
    }
    

    項番

    説明

    ServiceクラスにRepositoryインターフェースをDIする。

    引数で渡されたEntityオブジェクトに対して、アプリケーション要件に応じて値を設定する。

    上記例では、

    • IDとして「UUID」

    • 登録日時として「システム日時」

    • 完了フラグに「false: 未完了」

    • バージョンに「”1“」

    を設定している。

    Repositoryインターフェースのメソッドを呼び出し、Entityを1件登録する。

    登録したEntityを返却する。

    Serviceクラスの処理で登録値を設定する場合は、登録したEntityオブジェクトを返り値として返却する事を推奨する。


6.2.2.7.2. キーの生成

Entityの1件登録」では、Serviceクラスでキー(ID)の生成をする実装例になっているが、MyBatis3では、マッピングファイル内でキーを生成する仕組みが用意されている。

Note

MyBatis3のキー生成機能の使用ケースについて

キーを生成するために、データベースの機能(関数やID列など)を使用する場合は、MyBatis3のキー生成機能の仕組みを使用する事を推奨する。


キーの生成方法は、2種類用意されている。

  • データベースから用意されている関数などを呼び出した結果をキーとして扱う方法

  • データベースから用意されているID列(IDENTITY型、AUTO_INCREMENT型など) + JDBC3.0から追加されたStatement#getGeneratedKeys()を呼び出した結果をキーとして扱う方法


まず、データベースから用意されている関数などを呼び出した結果をキーとして扱う方法について説明する。
下記例は、データベースとしてH2 Databaseを使用している。
<?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="com.example.domain.repository.todo.TodoRepository">

    <insert id="create" parameterType="Todo">
        <!-- (1) -->
        <selectKey keyProperty="todoId" resultType="string" order="BEFORE">
            /* (2) */
            SELECT RANDOM_UUID()
        </selectKey>
        INSERT INTO
            t_todo
        (
            todo_id,
            todo_title,
            finished,
            created_at,
            version
        )
        VALUES
        (
            #{todoId},
            #{todoTitle},
            #{finished},
            #{createdAt},
            #{version}
        )
    </insert>

</mapper>

項番

属性

説明

-

selectKey要素の中に、キーを生成するためのSQLを実装する。

上記例では、データベースから提供されている関数を使用してUUIDを取得している。

selectKeyの詳細については、「MyBatis3 REFERENCE DOCUMENTATION (Mapper XML Files-insert, update and delete-)」を参照されたい。

keyProperty

取得したキー値を格納するEntityのプロパティ名を指定する。

上記例では、EntityのtodoIdプロパティに生成したキーが設定される。

resultType

SQLを発行して取得するキー値の型を指定する。

order

キー生成用SQLを実行するタイミング(BEFORE又はAFTER)を指定する。

  • BEFOREを指定した場合、selectKey要素で指定したSQLを実行した結果をEntityに反映した後にINSERT文が実行される。

  • AFTERを指定した場合、INSERT文を実行した後にselectKey要素で指定したSQLを実行され、取得した値がEntityに反映される。

-

キーを生成するためのSQLを実装する。

上記例では、H2 DatabaseのUUIDを生成する関数を呼び出して、キーを生成している。
キー生成の代表的な実装としては、シーケンスオブジェクトから取得した値を文字列にフォーマットする実装があげられる。

次に、データベースから用意されているID列 + JDBC3.0から追加されたStatement#getGeneratedKeys()を呼び出した結果をキーとして扱う方法について説明する。
下記例は、データベースとしてH2 Databaseを使用している。
<?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="com.example.domain.repository.audit.AuditLogRepository">

    <!-- (3) -->
    <insert id="create" parameterType="Todo" useGeneratedKeys="true" keyProperty="logId">
        INSERT INTO
            t_audit_log
        (
            level,
            message,
            created_at,
        )
        VALUES
        (
            #{level},
            #{message},
            #{createdAt},
        )
    </insert>

</mapper>

項番

属性

説明

useGeneratedKeys

trueを指定すると、ID列+Statement#getGeneratedKeys()を呼び出してキーを取得する機能が利用可能となる。

useGeneratedKeysの詳細については、「MyBatis3 REFERENCE DOCUMENTATION (Mapper XML Files-insert, update and delete-)」を参照されたい。

keyProperty

データベース上で自動的にインクリメントされたキー値を格納するEntityのプロパティ名を指定する。

上記例では、INSERT文実行後に、EntityのlogIdプロパティにStatement#getGeneratedKeys()で取得したキー値が設定される。


6.2.2.7.3. Entityの一括登録

Entityを一括で登録する際の実装例を以下に示す。

Entityを一括で登録する場合は、

  • 複数のレコードを同時に登録するINSERT文を発行する

  • JDBCのバッチ更新機能を使用する

方法がある。

JDBCのバッチ更新機能を使用する方法については、「バッチモードの利用」を参照されたい。

ここでは、複数のレコードを同時に登録するINSERT文を発行する方法について説明する。
下記例は、データベースとしてH2 Databaseを使用している。
  • Repositoryインタフェースにメソッドを定義する。

    package com.example.domain.repository.todo;
    
    import java.util.List;
    
    import com.example.domain.model.Todo;
    
    public interface TodoRepository {
    
        // (1)
        void createAll(List<Todo> todos);
    
    }
    

    項番

    説明

    上記例では、引数に指定されたTodoオブジェクトのリストを一括登録するためのメソッドとして、createAllメソッドを定義している。


  • マッピングファイルに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="com.example.domain.repository.todo.TodoRepository">
    
        <insert id="createAll" parameterType="list">
            INSERT INTO
                t_todo
            (
                todo_id,
                todo_title,
                finished,
                created_at,
                version
            )
            /* (2) */
            VALUES
            /* (3) */
            <foreach collection="list" item="todo" separator=",">
            (
                #{todo.todoId},
                #{todo.todoTitle},
                #{todo.finished},
                #{todo.createdAt},
                #{todo.version}
            )
            </foreach>
        </insert>
    
    </mapper>
    

    項番

    属性

    説明

    -

    VALUE句にレコード登録時の設定値を指定する。

    -

    foreach要素を使用して、引数で渡されたTodoオブジェクトのリストに対して繰り返し処理を行う。

    foreachの詳細については、「MyBatis3 REFERENCE DOCUMENTATION (Dynamic SQL-foreach-)」を参照されたい。

    collection

    処理対象のコレクションを指定する。

    上記例では、Repositoryのメソッド引数のリストに対して繰り返し処理を行っている。
    Repositoryメソッドの引数に@Paramを指定していない場合は、listを指定する。
    @Paramを指定した場合は、@Paramvalue属性に指定した値を指定する。

    item

    リストの中の1要素を保持するローカル変数名を指定する。

    foreach要素内のSQLからは、#{ローカル変数名.プロパティ名}の形式でJavaBeanのプロパティにアクセスする事ができる。

    separator

    リスト内の要素間を区切るための文字列を指定する。

    上記例では、”,“を指定することで、要素毎のVALUE句を”,“で区切っている。

    Note

    複数のレコードを同時に登録するSQLを使用する際の注意点

    複数のレコードを同時に登録するSQLを実行する場合は、前述の「キーの生成」を使用することが出来ない。


  • 以下のようなSQLが生成され、実行される。

    INSERT INTO
        t_todo
    (
        todo_id,
        todo_title,
        finished,
        created_at,
        version
    )
    VALUES
    (
        '99243507-1b02-45b6-bfb6-d9b89f044e2d',
        'todo title 1',
        false,
        '09/17/2014 23:59:59.999',
        1
    )
    ,
    (
        '66b096f1-791f-412f-9a0a-ee4a3a9186c2',
        'todo title 2',
        0,
        '09/17/2014 23:59:59.999',
        1
    )
    

    Tip

    一括登録するためのSQLは、データベースやバージョンによりサポート状況や文法が異なる。

    以下に主要なデータベースのリファレンスページへのリンクを記載しておく。


6.2.2.8. Entityの更新処理

Entityの更新方法について、目的別に実装例を説明する。

6.2.2.8.1. Entityの1件更新

Entityを1件更新する際の実装例を以下に示す。

Note

以降の説明では、バージョンカラムを使用して楽観ロックを行う実装例となっているが、楽観ロックの必要がない場合は、楽観ロック関連の処理を行う必要はない。

排他制御の詳細については、「排他制御」を参照されたい。


  • Repositoryインタフェースにメソッドを定義する。

    package com.example.domain.repository.todo;
    
    import com.example.domain.model.Todo;
    
    public interface TodoRepository {
    
        // (1)
        boolean update(Todo todo);
    
    }
    

    項番

    説明

    上記例では、引数に指定されたTodoオブジェクトを1件更新するためのメソッドとして、updateメソッドを定義している。

    Note

    Entityを1件更新するメソッドの返り値について

    Entityを1件更新するメソッドの返り値は、基本的にはbooleanでよい。

    ただし、更新結果が複数件になった場合にデータ不整合エラーとして扱う必要がある場合は、数値型(int又はlong)を返り値にし、更新件数が1件であることをチェックする必要がある。主キーが更新条件となっている場合は、更新結果が複数件になる事はないので、booleanでよい。

    • 返り値としてbooleanを指定した場合は、更新件数が0件の際はfalse、更新件数が1件以上の際はtrueが返却される。

    • 返り値として数値型を指定した場合は、更新件数が返却される。


  • マッピングファイルに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="com.example.domain.repository.todo.TodoRepository">
    
        <!-- (2) -->
        <update id="update" parameterType="Todo">
            UPDATE
                t_todo
            SET
                todo_title = #{todoTitle},
                finished = #{finished},
                version = version + 1
            WHERE
                todo_id = #{todoId}
            AND
                version = #{version}
        </update>
    
    </mapper>
    

    項番

    説明

    update要素の中に、UPDATEするSQLを実装する。

    id属性には、Repositoryインタフェースに定義したメソッドのメソッド名を指定する。

    update要素の詳細については、MyBatis3 REFERENCE DOCUMENTATION (Mapper XML Files-insert, update and delete-)」を参照されたい。

    SET句及びWHERE句にバインドする値は、#{variableName}形式のバインド変数として指定する。
    上記例では、Repositoryインタフェースの引数としてJavaBean(Todo)を指定しているため、バインド変数名にはJavaBeanのプロパティ名を指定する。

  • ServiceクラスにRepositoryをDIし、Repositoryインターフェースのメソッドを呼び出す。

    package com.example.domain.service.todo;
    
    import org.springframework.orm.ObjectOptimisticLockingFailureException;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;
    
    import com.example.domain.model.Todo;
    import com.example.domain.repository.todo.TodoRepository;
    
    import jakarta.inject.Inject;
    
    @Transactional
    @Service
    public class TodoServiceImpl implements TodoService {
    
        // (3)
        @Inject
        TodoRepository todoRepository;
    
        @Override
        public Todo update(Todo todo) {
    
            // (4)
            Todo currentTodo = todoRepository.findByTodoId(todo.getTodoId());
            if (currentTodo == null || currentTodo.getVersion() != todo.getVersion()) {
                throw new ObjectOptimisticLockingFailureException(Todo.class, todo
                        .getTodoId());
            }
    
            // (5)
            currentTodo.setTodoTitle(todo.getTodoTitle());
            currentTodo.setFinished(todo.isFinished());
    
            // (6)
            boolean updated = todoRepository.update(currentTodo);
            // (7)
            if (!updated) {
                throw new ObjectOptimisticLockingFailureException(Todo.class,
                        currentTodo.getTodoId());
            }
            currentTodo.setVersion(todo.getVersion() + 1);
    
            return currentTodo;
        }
    
    }
    

    項番

    説明

    ServiceクラスにRepositoryインターフェースをDIする。

    更新対象のEntityをデータベースより取得する。

    上記例では、Entityが更新されている場合(レコードが削除されている場合又はバージョンが更新されている場合)は、Spring Frameworkから提供されている楽観ロック例外(org.springframework.orm.ObjectOptimisticLockingFailureException)を発生させている。

    更新対象のEntityに対して、更新内容を反映する。

    上記例では、「タイトル」「完了フラグ」を反映している。更新項目が少ない場合は上記実装例のままでもよいが、更新項目が多い場合は、「Beanマッピング(MapStruct)」を使用することを推奨する。

    Repositoryインターフェースのメソッドを呼び出し、Entityを1件更新する。

    Entityの更新結果を判定する。

    上記例では、Entityが更新されなかった場合(レコードが削除されている場合又はバージョンが更新されている場合)は、Spring Frameworkから提供されている楽観ロック例外(org.springframework.orm.ObjectOptimisticLockingFailureException)を発生させている。

    Tip

    上記例では、更新処理が成功した後に、

    currentTodo.setVersion(todo.getVersion() + 1);
    

    としている。

    これはデータベースに更新したバージョンと、Entityが保持するバージョンを合わせるための処理である。

    呼び出し元(ControllerやJSPなど)の処理でバージョンを参照する場合は、データベースの状態とEntityの状態を一致させておかないと、データ不整合が発生し、アプリケーションが期待通りの動作しない事になる。


6.2.2.8.2. Entityの一括更新

Entityを一括で更新する際の実装例を以下に示す。

Entityを一括で更新する場合は、

  • 複数のレコードを同時に更新するUPDATE文を発行する

  • JDBCのバッチ更新機能を使用する

方法がある。

JDBCのバッチ更新機能を使用する方法については、「バッチモードの利用」を参照されたい。


ここでは、複数のレコードを同時に更新するUPDATE文を発行する方法について説明する。

  • Repositoryインタフェースにメソッドを定義する。

    package com.example.domain.repository.todo;
    
    import com.example.domain.model.Todo;
    import org.apache.ibatis.annotations.Param;
    
    import java.util.List;
    
    public interface TodoRepository {
    
        // (1)
        int updateFinishedByTodIds(@Param("finished") boolean finished,
                                   @Param("todoIds") List<String> todoIds);
    
    }
    

    項番

    説明

    上記例では、引数に指定されたIDのリストに該当するレコードのfinishedカラムを更新するためのメソッドとして、updateFinishedByTodIdsメソッドを定義している。

    Note

    Entityを一括更新するメソッドの返り値について

    Entityを一括更新するメソッドの返り値は、数値型(int又はlong)でよい。

    数値型にすると、更新されたレコード数を取得する事ができる。


  • マッピングファイルに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="com.example.domain.repository.todo.TodoRepository">
    
        <update id="updateFinishedByTodIds">
            UPDATE
                t_todo
            SET
                finished = #{finished},
                /* (2) */
                version = version + 1
            WHERE
                /* (3) */
                <foreach item="todoId" collection="todoIds"
                         open="todo_id IN (" separator="," close=")">
                    #{todoId}
                </foreach>
        </update>
    
    </mapper>
    

    項番

    属性

    説明

    -

    バージョンカラムを使用して楽観ロックを行う場合は、バージョンカラムを更新する。

    更新しないと、楽観ロック制御が正しく動作しなくなる。
    排他制御の詳細については、「排他制御」を参照されたい。

    -

    WHERE句に複数レコードを更新するための更新条件を指定する。

    -

    foreach要素を使用して、引数で渡されたIDのリストに対して繰り返し処理を行う。

    上記例では、引数で渡されたIDのリストより、IN句を生成している。

    foreachの詳細については、「MyBatis3 REFERENCE DOCUMENTATION (Dynamic SQL-foreach-)」を参照されたい。

    collection

    処理対象のコレクションを指定する。

    上記例では、Repositoryのメソッド引数のIDのリスト(todoIds)に対して繰り返し処理を行っている。

    item

    リストの中の1要素を保持するローカル変数名を指定する。

    separator

    リスト内の要素間を区切るための文字列を指定する。

    上記例では、IN句の区切り文字である”,“を指定している。


6.2.2.9. Entityの削除処理

6.2.2.9.1. Entityの1件削除

Entityを1件削除する際の実装例を以下に示す。

Note

以降の説明では、バージョンカラムを使用した楽観ロックを行う実装例となっているが、楽観ロックの必要がない場合は、楽観ロック関連の処理を行う必要はない。

排他制御の詳細については、「排他制御」を参照されたい。


  • Repositoryインタフェースにメソッドを定義する。

    package com.example.domain.repository.todo;
    
    import com.example.domain.model.Todo;
    
    public interface TodoRepository {
    
        // (1)
        boolean delete(Todo todo);
    
    }
    

    項番

    説明

    上記例では、引数に指定されたTodoオブジェクトを1件削除するためのメソッドとして、deleteメソッドを定義している。

    Note

    Entityを1件削除するメソッドの返り値について

    Entityを1件削除するメソッドの返り値は、基本的にはbooleanでよい。

    ただし、削除結果が複数件になった場合にデータ不整合エラーとして扱う必要がある場合は、数値型(int又はlong)を返り値にし、削除件数が1件であることをチェックする必要がある。

    主キーが削除条件となっている場合は、削除結果が複数件になる事はないので、booleanでよい。

    • 返り値としてbooleanを指定した場合は、削除件数が0件の際はfalse、削除件数が1件以上の際はtrueが返却される。

    • 返り値として数値型を指定した場合は、削除件数が返却される。


  • マッピングファイルに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="com.example.domain.repository.todo.TodoRepository">
    
        <!-- (2) -->
        <delete id="delete" parameterType="Todo">
            DELETE FROM
                t_todo
            WHERE
                todo_id = #{todoId}
            AND
                version = #{version}
        </delete>
    
    </mapper>
    

    項番

    説明

    delete要素の中に、DELETEするSQLを実装する。

    id属性には、Repositoryインタフェースに定義したメソッドのメソッド名を指定する。

    delete要素の詳細については、「MyBatis3 REFERENCE DOCUMENTATION (Mapper XML Files-insert, update and delete-)」を参照されたい。

    WHERE句にバインドする値は、#{variableName}形式のバインド変数として指定する。
    上記例では、Repositoryインタフェースの引数としてJavaBean(Todo)を指定しているため、
    バインド変数名にはJavaBeanのプロパティ名を指定する。

  • ServiceクラスにRepositoryをDIし、Repositoryインターフェースのメソッドを呼び出す。

    package com.example.domain.service.todo;
    
    import org.springframework.orm.ObjectOptimisticLockingFailureException;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;
    
    import com.example.domain.model.Todo;
    import com.example.domain.repository.todo.TodoRepository;
    
    import jakarta.inject.Inject;
    
    @Transactional
    @Service
    public class TodoServiceImpl implements TodoService {
    
        // (3)
        @Inject
        TodoRepository todoRepository;
    
        @Override
        public Todo delete(String todoId, long version) {
    
            // (4)
            Todo currentTodo = todoRepository.findByTodoId(todoId);
            if (currentTodo == null || currentTodo.getVersion() != version) {
                throw new ObjectOptimisticLockingFailureException(Todo.class, todoId);
            }
    
            // (5)
            boolean deleted = todoRepository.delete(currentTodo);
            // (6)
            if (!deleted) {
                throw new ObjectOptimisticLockingFailureException(Todo.class,
                        currentTodo.getTodoId());
            }
    
            return currentTodo;
        }
    
    }
    

    項番

    説明

    ServiceクラスにRepositoryインターフェースをDIする。

    削除対象のEntityをデータベースより取得する。

    上記例では、Entityが更新されている場合(レコードが削除されている場合又はバージョンが更新されている場合)は、Spring Frameworkから提供されている楽観ロック例外(org.springframework.orm.ObjectOptimisticLockingFailureException)を発生させている。

    Repositoryインターフェースのメソッドを呼び出し、Entityを1件削除する。

    Entityの削除結果を判定する。

    上記例では、Entityが削除されなかった場合(レコードが削除されている場合又はバージョンが更新されている場合)は、Spring Frameworkから提供されている楽観ロック例外(org.springframework.orm.ObjectOptimisticLockingFailureException)を発生させている。


6.2.2.9.2. Entityの一括削除

Entityを一括で削除する際の実装例を以下に示す。

Entityを一括で削除する場合は、

  • 複数のレコードを同時に削除するDELETE文を発行する

  • JDBCのバッチ更新機能を使用する

方法がある。

JDBCのバッチ更新機能を使用する方法については、「バッチモードの利用」を参照されたい。


ここでは、複数のレコードを同時に削除するDELETE文を発行する方法について説明する。

  • Repositoryインタフェースにメソッドを定義する。

    package com.example.domain.repository.todo;
    
    public interface TodoRepository {
    
        // (1)
        int deleteOlderFinishedTodo(Date criteriaDate);
    
    }
    

    項番

    説明

    上記例では、基準日より前に作成され完了済みのレコードを削除するためのメソッドとして、deleteOlderFinishedTodoメソッドを定義している。

    Note

    Entityを一括削除するメソッドの返り値について

    Entityを一括削除するメソッドの返り値は、数値型(int又はlong)でよい。

    数値型にすると、削除されたレコード数を取得する事ができる。


  • マッピングファイルに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="com.example.domain.repository.todo.TodoRepository">
    
        <delete id="deleteOlderFinishedTodo" parameterType="date">
            <![CDATA[
            DELETE FROM
                t_todo
            /* (2) */
            WHERE
                finished = TRUE
            AND
                created_at  < #{criteriaDate}
            ]]>
        </delete>
    
    </mapper>
    

    項番

    説明

    WHERE句に複数レコードを更新するための削除条件を指定する。

    上記例では、

    • 完了済み(finishedTRUE)

    • 基準日より前に作成された(created_atが基準日より前)

    を削除条件として指定している。


6.2.2.10. 動的SQLの実装

動的SQLを組み立てる実装例を以下に示す。

MyBatis3では、動的にSQLを組み立てるためのXML要素と、OGNLベースの式(Expression言語)を使用することで、動的SQLを組み立てる仕組みを提供している。

動的SQLの詳細については、「MyBatis3 REFERENCE DOCUMENTATION (Dynamic SQL)」を参照されたい。

MyBatis3では、動的にSQLを組み立てるために、以下のXML要素を提供している。

項番

要素名

説明

if

条件に一致した場合のみ、SQLの組み立てを行うための要素。

choose

複数の選択肢の中から条件に一致する1つを選んで、SQLの組み立てを行うための要素。

where

組み立てたWHERE句に対して、接頭語及び末尾の付与や除去など行うための要素。

set

組み立てたSET句用に対して、接頭語及び末尾の付与や除去など行うための要素。

foreach

コレクションや配列に対して繰り返し処理を行うための要素

bind

OGNL式の結果を変数に格納するための要素。

bind要素を使用して格納した変数は、SQL内で参照する事ができる。

Tip

一覧には記載していないが、動的SQLを組み立てるためのXML要素としてtrim要素が提供されている。

trim要素は、where要素とset要素をより汎用的にしたXML要素である。

ほとんどの場合は、where要素とset要素で要件を充たせるため、本ガイドラインではtrim要素の説明は割愛する。

trim要素が必要になる場合は、「MyBatis3 REFERENCE DOCUMENTATION (Dynamic SQL-trim, where, set-)」を参照されたい。


6.2.2.10.1. if要素の実装

if要素は、指定した条件に一致した場合のみ、SQLの組み立てを行うためのXML要素である。

<select id="findAllByCriteria" parameterType="TodoCriteria" resultType="Todo">
    SELECT
        todo_id,
        todo_title,
        finished,
        created_at,
        version
    FROM
        t_todo
    WHERE
        todo_title LIKE #{todoTitle} || '%' ESCAPE '~'
    <!-- (1) -->
    <if test="finished != null">
        AND
            finished = #{finished}
    </if>
    ORDER BY
        todo_id
</select>

項番

説明

if要素のtest属性に、条件を指定する。

上記例では、検索条件としてfinishedが指定されている場合に、finishedカラムに対する条件をSQLに加えている。

上記の動的SQLで生成されるSQL(WHERE句)は、以下2パターンとなる。

-- (1) finished == null
...
WHERE
    todo_title LIKE ? || '%' ESCAPE '~'
ORDER BY
    todo_id
-- (2) finished != null
...
WHERE
    todo_title LIKE ? || '%' ESCAPE '~'
AND
    finished = ?
ORDER BY
    todo_id

6.2.2.10.2. choose要素の実装

choose要素は、複数の選択肢の中から条件に一致する1つを選んで、SQLの組み立てを行うためのXML要素である。

<select id="findAllByCriteria" parameterType="TodoCriteria" resultType="Todo">
    SELECT
        todo_id,
        todo_title,
        finished,
        created_at,
        version
    FROM
        t_todo
    WHERE
        todo_title LIKE #{todoTitle} || '%' ESCAPE '~'
    <!-- (1) -->
    <choose>
        <!-- (2) -->
        <when test="createdAt != null">
            AND
                created_at <![CDATA[ > ]]> #{createdAt}
        </when>
        <!-- (3) -->
        <otherwise>
            AND
                created_at <![CDATA[ > ]]> CURRENT_DATE
        </otherwise>
    </choose>
    ORDER BY
        todo_id
</select>

項番

説明

choose要素に中に、when要素とotherwise要素を指定して、SQLを組み立てる条件を指定する。

when要素のtest属性に、条件を指定する。

上記例では、検索条件としてcreatedAtが指定されている場合に、create_atカラムの値が指定日以降のレコードを抽出するための条件をSQLに加えている。

otherwise要素に、全てのwhen要素に一致しない場合時に組み立てるSQLを指定する。

上記例では、create_atカラムの値が現在日以降のレコード(当日作成されたレコード)を抽出するための条件をSQLに加えている。

上記の動的SQLで生成されるSQL(WHERE句)は、以下2パターンとなる。

-- (1) createdAt!=null
...
WHERE
    todo_title LIKE ? || '%' ESCAPE '~'
AND
    created_at   >   ?
ORDER BY
    todo_id
-- (2) createdAt==null
...
WHERE
    todo_title LIKE ? || '%' ESCAPE '~'
AND
    created_at > CURRENT_DATE
ORDER BY
    todo_id

6.2.2.10.3. where要素の実装

where要素は、WHERE句を動的に生成するためのXML要素である。

where要素を使用すると、

  • WHERE句の付与

  • AND句、OR句の除去

などが行われるため、シンプルにWHERE句を組み立てる事ができる。

<select id="findAllByCriteria2" parameterType="TodoCriteria" resultType="Todo">
    SELECT
        todo_id,
        todo_title,
        finished,
        created_at,
        version
    FROM
        t_todo
    <!-- (1) -->
    <where>
        <!-- (2) -->
        <if test="finished != null">
            AND
                finished = #{finished}
        </if>
        <!-- (3) -->
        <if test="createdAt != null">
            AND
                created_at <![CDATA[ > ]]> #{createdAt}
        </if>
    </where>
    ORDER BY
        todo_id
</select>

項番

説明

where要素に中で、WHERE句を組み立てるための動的SQLを実装する。

where要素内で組み立てたSQLに応じて、WHERE句の付与や、AND句及びORの除去などが行われる。

動的SQLを組み立てる。

上記例では、検索条件としてfinishedが指定されている場合に、finishedカラムに対する条件をSQLに加えている。

動的SQLを組み立てる。

上記例では、検索条件としてcreatedAtが指定されている場合に、created_atカラムに対する条件をSQLに加えている。

上記の動的SQLで生成されるSQL(WHERE句)は、以下4パターンとなる。

-- (1) finished != null && createdAt != null
...
FROM
    t_todo
WHERE
    finished = ?
AND
    created_at  >  ?
ORDER BY
    todo_id
-- (2) finished != null && createdAt == null
...
FROM
    t_todo
WHERE
    finished = ?
ORDER BY
    todo_id
-- (3) finished == null && createdAt != null
...
FROM
    t_todo
WHERE
    created_at  >  ?
ORDER BY
    todo_id
-- (4) finished == null && createdAt == null
...
FROM
    t_todo
ORDER BY
    todo_id

6.2.2.10.4. set要素の実装例

set要素は、SET句を動的に生成するためのXML要素である。

set要素を使用すると、

  • SET句の付与

  • 末尾のカンマの除去

などが行われるため、シンプルにSET句を組み立てる事ができる。

<update id="update" parameterType="Todo">
    UPDATE
        t_todo
    <!-- (1)  -->
    <set>
        version = version + 1,
        <!-- (2) -->
        <if test="todoTitle != null">
            todo_title = #{todoTitle}
        </if>
    </set>
    WHERE
        todo_id = #{todoId}
</update>

項番

説明

set要素に中で、SET句を組み立てるための動的SQLを実装する。

set要素内で組み立てたSQLに応じて、SET句の付与や、末尾のカンマの除去などが行われる。

動的SQLを組み立てる。

上記例では、更新項目としてtodoTitleが指定されている場合に、todo_titleカラムを更新カラムとしてSQLに加えている。

上記の動的SQLで生成されるSQLは、以下2パターンとなる。

-- (1) todoTitle != null
UPDATE
    t_todo
SET
    version = version + 1,
    todo_title = ?
WHERE
    todo_id = ?
-- (2) todoTitle == null
UPDATE
    t_todo
SET
   version = version + 1
WHERE
    todo_id = ?

6.2.2.10.5. foreach要素の実装例

foreach要素は、コレクションや配列に対して繰り返し処理を行うためのXML要素である。

<select id="findAllByCreatedAtList" parameterType="list" resultType="Todo">
    SELECT
        todo_id,
        todo_title,
        finished,
        created_at,
        version
    FROM
        t_todo
    <where>
        <!-- (1) -->
        <if test="list != null">
            <!-- (2) -->
            <foreach collection="list" item="date" separator="OR">
            <![CDATA[
                (created_at >= #{date} AND created_at < DATEADD('DAY', 1, #{date}))
            ]]>
            </foreach>
        </if>
    </where>
    ORDER BY
        todo_id
</select>

項番

属性

説明

-

繰り返し処理を行う対象のコレクション又は配列に対して、nullチェックを行う。

nullになる事がない場合は、このチェックは実装しなくてもよい。

-

foreach要素を使用して、コレクションや配列に対して繰り返し処理を行い、動的SQLを組み立てる。

上記例では、レコードの作成日付が、指定された日付(日付リスト)の何れかと一致するレコードを検索するためのWHERE句を組み立てている。

collection

collection属性に、繰り返し処理を行うコレクションや配列を指定する。

上記例では、Repositoryメソッドの引数に指定されたコレクションを指定している。

item

item属性に、リストの中の1要素を保持するローカル変数名を指定する。

上記例では、collection属性に日付リストを指定しているので、dateという変数名を指定している。

separator

separator属性に、要素間の区切り文字列を指定する。

上記例では、OR条件のWHERE句を組み立てている。

Tip

上記例では使用していないが、 foreach要素には、以下の属性が存在する。

項番

属性

説明

open

コレクションの先頭要素を処理する前に設定する文字列を指定する。

close

コレクションの末尾要素を処理した後に設定する文字列を指定する。

index

ループ番号を格納する変数名を指定する。

index属性を使用するケースはあまりないが、open属性と close属性は、IN句などを動的に生成する際に使用される。

以下に、IN句を作成する際のforeach要素の使用例を記載しておく。

<foreach collection="list" item="statusCode"
        open="AND order_status IN ("
        separator=","
        close=")">
    #{statusCode}
</foreach>

以下の様なSQLが組み立てられる。

-- list=['accepted','checking']
...
AND order_status IN (?,?)
上記の動的SQLで生成されるSQL(WHERE句)は、以下3パターンとなる。
-- (1) list=null or statusCodes=[]
...
FROM
    t_todo
ORDER BY
    todo_id
-- (2) list=['2014-01-01']
...
FROM
    t_todo
WHERE
    (created_at >= ? AND created_at < DATEADD('DAY', 1, ?))
ORDER BY
    todo_id
-- (3) list=['2014-01-01','2014-01-02']
...
FROM
    t_todo
WHERE
    (created_at >= ? AND created_at < DATEADD('DAY', 1, ?))
OR
    (created_at >= ? AND created_at < DATEADD('DAY', 1, ?))
ORDER BY
    todo_id

6.2.2.10.6. bind要素の実装例

bind要素は、OGNL式の結果を変数に格納するためのXML要素である。

<select id="findAllByCriteria" parameterType="TodoCriteria" resultType="Todo">
    <!-- (1) -->
    <bind name="escapedTodoTitle"
          value="@org.terasoluna.gfw.common.query.QueryEscapeUtils@toLikeCondition(todoTitle)" />
    SELECT
        todo_id,
        todo_title,
        finished,
        created_at,
        version
    FROM
        t_todo
    WHERE
        /* (2) */
        todo_title LIKE #{escapedTodoTitle} || '%' ESCAPE '~'
    ORDER BY
        todo_id
</select>

項番

属性

説明

-

bind要素を使用して、OGNL式の結果を変数に格納する

上記例では、OGNL式を使ってメソッドを呼び出した結果を、変数に格納している。

name

name属性には、変数名を指定する。

ここで指定した変数名は、SQLのバインド変数として使用する事ができる。

value

value属性には、OGNL式を指定する。

OGNL式を実行した結果が、name属性で指定した変数に格納される。

上記例では、共通ライブラリから提供しているメソッド(QueryEscapeUtils#toLikeCondition(String))を呼び出した結果を、escapedTodoTitleという変数に格納している。

-

bind要素を使用して作成した変数を、バインド変数として指定する。

上記例では、bind要素を使用して作成した変数(escapedTodoTitle)を、バインド変数として指定している。

Tip

上記例では、bind要素を使用して作成した変数をバインド変数として指定しているが、置換変数として使用する事もできる。

バインド変数と置換変数については、「SQL Injection対策」を参照されたい。


6.2.2.11. LIKE検索時のエスケープ

LIKE検索を行う場合は、検索条件として使用する値をLIKE検索用にエスケープする必要がある。

LIKE検索用のエスケープ処理は、共通ライブラリから提供しているorg.terasoluna.gfw.common.query.QueryEscapeUtilsクラスのメソッドを使用することで実現する事ができる。

共通ライブラリから提供しているエスケープ処理の仕様については、「LIKE検索時のエスケープについて」を参照されたい。

<select id="findAllByCriteria" parameterType="TodoCriteria" resultType="Todo">
    <!-- (1) -->
    <bind name="todoTitleContainingCondition"
          value="@org.terasoluna.gfw.common.query.QueryEscapeUtils@toContainingCondition(todoTitle)" />
    SELECT
        todo_id,
        todo_title,
        finished,
        created_at,
        version
    FROM
        t_todo
    WHERE
        /* (2) (3) */
        todo_title LIKE #{todoTitleContainingCondition} ESCAPE '~'
    ORDER BY
        todo_id
</select>

項番

説明

bind要素(OGNL式)を使用して、共通ライブラリから提供しているLIKE検索用のエスケープ処理メソッドを呼び出す。

上記例では、部分一致用のエスケープ処理を行いtodoTitleContainingConditionという変数に格納している。
QueryEscapeUtils@toContainingCondition(String)メソッドは、エスケープした文字列の前後に”%“を付与するメソッドである。

部分一致用のエスケープを行った文字列を、LIKE句のバインド変数として指定する。

ESCAPE句にエスケープ文字を指定する。

共通ライブラリから提供しているエスケープ処理では、エスケープ文字として”~“を使用しているため、ESCAPE句に'~'を指定している。

Tip

上記例では、部分一致用のエスケープ処理を行うメソッドを呼び出しているが、

  • 前方一致用のエスケープ(QueryEscapeUtils@toStartingWithCondition(String))

  • 後方一致用のエスケープ(QueryEscapeUtils@toEndingWithCondition(String))

  • エスケープのみ(QueryEscapeUtils@toLikeCondition(String))

を行うメソッドも用意されている。

詳細は「LIKE検索時のエスケープについて」を参照されたい。

Note

上記例では、マッピングファイル内でエスケープ処理を行うメソッドを呼び出しているが、Repositoryのメソッドを呼び出す前に、Serviceの処理としてエスケープ処理を行う方法もある。

コンポーネントの役割としては、マッピングファイルでエスケープ処理を行う方が適切なため、本ガイドラインとしては、マッピングファイル内でエスケープ処理を行う事を推奨する。


6.2.2.12. SQL Injection対策

SQLを組み立てる際は、SQL Injectionが発生しないように注意する必要がある。

MyBatis3では、SQLに値を埋め込む仕組みとして、以下の2つの方法を提供している。

項番

方法

説明

バインド変数を使用して埋め込む

この方法を使用すると、 SQL組み立て後にjava.sql.PreparedStatementを使用して値が埋め込められるため、安全に値を埋め込むことができる。

ユーザからの入力値をSQLに埋め込む場合は、原則バインド変数を使用すること。

置換変数を使用して埋め込む

この方法を使用すると、SQLを組み立てるタイミングで文字列として置換されてしまうため、安全な値の埋め込みは保証されない。

Warning

ユーザからの入力値を置換変数を使って埋め込むと、SQL Injectionが発生する危険性が高くなることを意識すること。

ユーザからの入力値を置換変数を使って埋め込む必要がある場合は、SQL Injectionが発生しないことを保障するために、かならず入力チェックを行うこと。

基本的には、ユーザからの入力値はそのまま使わないことを強く推奨する。


6.2.2.12.1. バインド変数を使って埋め込む方法

バインド変数の使用例を以下に示す。

<insert id="create" parameterType="Todo">
    INSERT INTO
        t_todo
    (
        todo_id,
        todo_title,
        finished,
        created_at,
        version
    )
    VALUES
    (
        /* (1) */
        #{todoId},
        #{todoTitle},
        #{finished},
        #{createdAt},
        #{version}
    )
</insert>

項番

説明

バインドする値が格納されているプロパティのプロパティ名を、#{と”}“で囲み、バインド変数として指定する。

Tip

バインド変数には、いくつかの属性を指定する事が出来る。

指定できる属性としては、

  • javaType

  • jdbcType

  • typeHandler

  • numericScale

  • mode

  • resultMap

  • jdbcTypeName

がある。

基本的には、単純にプロパティ名を指定するだけで、MyBatisが適切な振る舞いを選択してくれる。上記属性は、MyBatisが適切な振る舞いを選択してくれない時に指定すればよい。

属性の使い方については、「MyBatis3 REFERENCE DOCUMENTATION(Mapper XML Files-Parameters-) 」を参照されたい。


6.2.2.12.2. 置換変数を使って埋め込む方法

置換変数の使用例を以下に示す。

  • Repositoryインタフェースにメソッドを定義する。

    public interface TodoRepository {
        List<Todo> findAllByCriteria(@Param("criteria") TodoCriteria criteria,
                                     @Param("direction") String direction);
    }
    
  • マッピングファイルにSQLを実装する。

    <select id="findAllByCriteria" parameterType="TodoCriteria" resultType="Todo">
        <bind name="todoTitleContainingCondition"
              value="@org.terasoluna.gfw.common.query.QueryEscapeUtils@toContainingCondition(criteria.todoTitle)" />
        SELECT
            todo_id,
            todo_title,
            finished,
            created_at,
            version
        FROM
            t_todo
        WHERE
            todo_title LIKE #{todoTitleContainingCondition} ESCAPE '~'
        ORDER BY
            /* (1) */
            todo_id ${direction}
    </select>
    

    項番

    説明

    置換する値が格納されているプロパティのプロパティ名を${と”}“で囲み、置換変数として指定する。上記例では、${direction}の部分は、DESCまたはASCで置換される。

    Warning

    置換変数による埋め込みは、必ずアプリケーションとして安全な値であることを担保した上で、テーブル名、カラム名、ソート条件などに限定して使用することを推奨する。

    例えば以下のように、コード値とSQLに埋め込むための値のペアをMapに格納しておき、

    Map<String, String> directionMap = new HashMap<String, String>();
    directionMap.put("1", "ASC");
    directionMap.put("2", "DESC");
    

    入力値はコード値として扱い、SQLを実行する処理の中で安全な値に変換することが望ましい。

    String direction = directionMap.get(directionCode);
    todoRepository.findAllByCriteria(criteria, direction);
    

    上記例ではMapを使用しているが、共通ライブラリから提供している「コードリスト」を使用しても良い。

    コードリスト」を使用すると、入力チェックと連動する事ができるため、より安全に値の埋め込みを行う事ができる。

    • projectName-domain/src/main/xxx/yyy/zzz/config/app/ProjectNameCodelistConfig.java

      @Bean("CL_DIRECTION")
      public SimpleMapCodeList simpleMapCodeList() {
          Map<String, String> map = new LinkedHashMap<>();
          map.put("1", "ASC");
          map.put("2", "DESC");
          SimpleMapCodeList bean = new SimpleMapCodeList();
          bean.setMap(map);
          return bean;
      }
      
    • Serviceクラス

      @Inject
      @Named("CL_DIRECTION")
      CodeList directionCodeList;
      
      // omitted
      
      public List<Todo> searchTodos(TodoCriteria criteria, String directionCode){
          String direction = directionCodeList.asMap().get(directionCode);
          List<Todo> todos = todoRepository.findAllByCriteria(criteria, direction);
          return todos;
      }
      

6.2.3. How to extend

6.2.3.1. SQL文の共有

SQL文を複数のSQLで共有する方法について、説明を行う。

MyBatis3では、 sql要素とinclude要素を使用することで、SQL文(又はSQL文の一部)を共有する事ができる。

Note

SQL文の共有化の使用例

ページネーション検索を実現する場合は、「検索条件に一致するEntityの総件数を取得するSQL」と「 検索条件に一致するEntityのリストを取得するSQL」のWHERE句は共有した方がよい。


マッピングファイルの実装例は以下の通り。

<!-- (1)  -->
<sql id="findPageByCriteriaWherePhrase">
    <![CDATA[
    WHERE
        todo_title LIKE #{title} || '%' ESCAPE '~'
    AND
        created_at < #{createdAt}
    ]]>
</sql>

<select id="countByCriteria" resultType="_long">
    SELECT
        COUNT(*)
    FROM
        t_todo
    <!-- (2)  -->
    <include refid="findPageByCriteriaWherePhrase"/>
</select>

<select id="findPageByCriteria" resultType="Todo">
    SELECT
        todo_id,
        todo_title,
        finished,
        created_at,
        version
    FROM
        t_todo
    <!-- (2)  -->
    <include refid="findPageByCriteriaWherePhrase"/>
    ORDER BY
        todo_id
</select>

項番

説明

sql要素の中に、複数のSQLで共有するSQL文を実装する。

id属性には、マッピングファイル内でユニークとなるIDを指定する。

include要素を使用して、インクルードするSQLを指定する。

refid属性には、インクルードするSQLのID(sql要素のid属性に指定した値)を指定する。


6.2.3.2. TypeHandlerの実装

MyBatis3の標準でサポートされていないJoda-Timeのクラスとのマッピングが必要の場合、 独自のTypeHandlerの作成が必要となる。

本ガイドラインでは「Joda-Time用のTypeHandlerの実装」を例に、TypeHandlerの実装方法について説明する。

作成したTypeHandlerをアプリケーションに適用する方法については、「TypeHandlerの設定」を参照されたい。

Note

BLOB用とCLOB用の実装について

MyBatis 3.4で追加されたTypeHandlerは、JDBC 4.0 (Java 1.6)で追加されたAPIを使用することで、BLOBとjava.io.InputStream、CLOBとjava.io.Readerの変換を実現している。JDBC 4.0サポートのJDBCドライバーであれば、BLOB⇔InputStream、CLOB⇔Reader変換用のタイプハンドラーがデフォルトで有効になるため、TypeHandlerを新たに実装する必要はない。

JDBC 4.0との互換性のないJDBCドライバを使う場合は、利用するJDBCドライバの互換バージョンを意識したTypeHandlerを作成する必要がある。

例えば、PostgreSQL用のJDBCドライバ(postgresql-42.2.9.jar)では、JDBC 4.0から追加されたメソッドの一部が、未実装の状態である。


6.2.3.2.1. Joda-Time用のTypeHandlerの実装

MyBatis3では、Joda-Timeのクラス(org.joda.time.DateTimeorg.joda.time.LocalDateTimeorg.joda.time.LocalDateなど)はサポートされていない。
そのため、EntityクラスのフィールドにJoda-Timeのクラスを使用する場合は、Joda-Time用のTypeHandlerを用意する必要がある。

org.joda.time.DateTimejava.sql.TimestampをマッピングするためのTypeHandlerの実装例を、以下に示す。

Note

Jada-Timeから提供されている他のクラス(LocalDateTimeLocalDateLocalTimeなど)も同じ要領で実装すればよい。

package com.example.infra.mybatis.typehandler;

import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;

import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.joda.time.DateTime;

// (1)
public class DateTimeTypeHandler extends BaseTypeHandler<DateTime> {

    // (2)
    @Override
    public void setNonNullParameter(PreparedStatement ps, int i,
            DateTime parameter, JdbcType jdbcType) throws SQLException {
        ps.setTimestamp(i, new Timestamp(parameter.getMillis()));
    }

    // (3)
    @Override
    public DateTime getNullableResult(ResultSet rs, String columnName)
            throws SQLException {
        return toDateTime(rs.getTimestamp(columnName));
    }

    // (3)
    @Override
    public DateTime getNullableResult(ResultSet rs, int columnIndex)
            throws SQLException {
        return toDateTime(rs.getTimestamp(columnIndex));
    }

    // (3)
    @Override
    public DateTime getNullableResult(CallableStatement cs, int columnIndex)
            throws SQLException {
        return toDateTime(cs.getTimestamp(columnIndex));
    }

    private DateTime toDateTime(Timestamp timestamp) {
        // (4)
        if (timestamp == null) {
            return null;
        } else {
            return new DateTime(timestamp.getTime());
        }
    }

}

項番

説明

MyBatis3から提供されているBaseTypeHandlerを親クラスに指定する。

その際、BaseTypeHandlerのジェネリック型には、DateTimeを指定する。

DateTimeTimestampに変換し、PreparedStatementに設定する処理を実装する。

ResultSet又はCallableStatementから取得したTimestampDateTimeに変換し、返り値として返却する。

nullを許可するカラムの場合、Timestampnullになる可能性があるため、nullチェックを行ってからDateTimeに変換する必要がある。

上記実装例では、3つのメソッドで同じ処理が必要になるため、privateメソッドを作成している。


6.2.3.3. ResultHandlerの実装

MyBatis3では、検索結果を1件単位で処理する仕組みを提供している。

この仕組みを利用すると、

  • DBより取得した値をJavaの処理で加工する

  • DBより取得した値などをJavaの処理として集計する

といった処理を行う際に、同時に消費するメモリの容量を最小限に抑える事ができる。

例えば、検索結果をCSV形式のデータとしてダウンロードするような処理を実装する場合は、検索結果を1件単位で処理する仕組みを使用するとよい。

Note

検索結果が大量になる可能性があり、且つJavaの処理で検索結果を1件ずつ処理する必要がある場合は、この仕組みを使用することを強く推奨する。

検索結果を1件単位で処理する仕組みを使用しない場合、検索結果の全データ「1データのサイズ * 検索結果件数」をメモリ上に同時に確保することになり、全てのデータに対して処理が終了するまでGC候補になることはない。

一方、検索結果を1件単位で処理する仕組みを使用した場合、基本的には「1データのサイズ」をメモリ上に確保するだけであり、1データの処理を終えた時点でGC候補となる。

例えば「1データのサイズ」が2KBで「検索結果件数」が10,000件だった場合、

  • まとめて処理を行う場合は、20MBのメモリ

  • 1件単位で処理を行う場合は、2KBのメモリ

が同時に消費される。シングルスレッドで動くアプリケーションであれば問題になる事はないが、Webアプリケーションの様なマルチスレッドで動くアプリケーションの場合は、問題になる事がある。

仮に100スレッドで同時に処理を行った場合、

  • まとめて処理を行う場合は、2GBのメモリ

  • 1件単位で処理を行う場合は、200KBのメモリ

が同時に消費される。

結果として、

  • まとめて処理を行う場合は、ヒープの最大サイズの指定によっては、メモリ枯渇によるシステムダウンやフルGCの頻発による性能劣化などが起こる可能性が高まる。

  • 1件単位で処理を行う場合は、メモリ枯渇やコストの高いGC処理が発生する可能性を抑える事ができる。

上記に挙げた数字は目安であり、実際の計測値ではないという点を補足しておく。


以下に、検索結果をCSV形式のデータとしてダウンロードする処理の実装例を示す。

  • Repositoryインタフェースにメソッドを定義する。

    public interface TodoRepository {
    
        // (1) (2)
        void collectAllByCriteria(TodoCriteria criteria, ResultHandler<Todo> resultHandler);
    
    }
    

    項番

    説明

    メソッドの引数として、 org.apache.ibatis.session.ResultHandlerを指定する。

    メソッドの返り値は、void型を指定する。

    void以外を指定すると、ResultHandlerが呼び出されなくなるので、注意すること。


  • マッピングファイルにSQLを定義する。

    <!-- (3) -->
    <select id="collectAllByCriteria" parameterType="TodoCriteria" resultType="Todo">
        SELECT
            todo_id,
            todo_title,
            finished,
            created_at,
            version
        FROM
            t_todo
        <where>
            <if test="title != null">
                <bind name="titleContainingCondition"
                      value="@org.terasoluna.gfw.common.query.QueryEscapeUtils@toContainingCondition(title)" />
                todo_title LIKE #{titleContainingCondition} ESCAPE '~'
            </if>
            <if test="createdAt != null">
                <![CDATA[
                AND created_at < #{createdAt}
                ]]>
            </if>
        </where>
    </select>
    

    項番

    説明

    マッピングファイルの実装は、通常の検索処理と同じである。

    Warning

    fetchSize属性の指定について

    大量のデータを返すようなクエリを記述する場合には、fetchSize属性に適切な値を設定すること。fetchSizeは、JDBCドライバとデータベース間の1回の通信で取得するデータの件数を設定するパラメータである。

    なお、MyBatis 3.3.0以降のバージョンでは、MyBatis設定ファイルに「デフォルトのfetchSize」を指定することができる。

    fetchSizeの詳細は「fetchSizeの設定」を参照されたい。


  • ServiceクラスにRepositoryをDIし、Repositoryインターフェースのメソッドを呼び出す。

    public class TodoServiceImpl implements TodoService {
    
        private static final DateTimeFormatter DATE_FORMATTER =
            DateTimeFormat.forPattern("yyyy/MM/dd");
    
        @Inject
        TodoRepository todoRepository;
    
        public void downloadTodos(TodoCriteria criteria,
            final BufferedWriter downloadWriter) {
    
            // (4)
            ResultHandler<Todo> handler = new ResultHandler<Todo>() {
                @Override
                public void handleResult(ResultContext<? extends Todo> context) {
                    Todo todo = context.getResultObject();
                    StringBuilder sb = new StringBuilder();
                    try {
                        sb.append(todo.getTodoId());
                        sb.append(",");
                        sb.append(todo.getTodoTitle());
                        sb.append(",");
                        sb.append(todo.isFinished());
                        sb.append(",");
                        sb.append(DATE_FORMATTER.print(todo.getCreatedAt().getTime()));
                        downloadWriter.write(sb.toString());
                        downloadWriter.newLine();
                    } catch (IOException e) {
                        throw new SystemException("e.xx.fw.9001", e);
                    }
                }
            };
    
            // (5)
            todoRepository.collectAllByCriteria(criteria, handler);
    
        }
    
    }
    

    項番

    説明

    ResultHandlerのインスタンスを生成する。

    ResultHandlerhandleResultメソッドの中に、1件毎に行う処理を実装する。

    上記例では、ResultHandlerの実装クラスは作らず、無名オブジェクトとしてResultHandlerの実装を行っている。
    実装クラスを作成してもよいが、複数の処理で共有する必要がない場合は、無理に実装クラスを作成する必要はない。

    Repositoryインタフェースのメソッドを呼び出す。

    メソッドを呼び出す際に、(4)で生成したResultHandlerのインスタンスを引数に指定する。

    ResultHandlerを使用した場合、MyBatisは以下の処理を検索結果の件数分繰り返す。

    • 検索結果からレコードを取得し、JavaBeanにマッピングを行う。

    • ResultHandlerインスタンスのhandleResult(ResultContext)メソッドを呼び出す。

    Warning

    ResultHandler使用時の注意点

    ResultHandlerを使用する場合、以下の2点に注意すること。

    • MyBatis3では、検索処理の性能向上させる仕組みとして、検索結果をローカルキャッシュ及びグローバルな2次キャッシュに保存する仕組みを提供しているが、ResultHandlerを引数に取るメソッドから返されるデータはキャッシュされない。

    • 手動マッピングを使用して複数行のデータを一つのJavaオブジェクトにマッピングするステートメントに対してResultHandlerを使用した場合、不完全な状態(関連Entityのオブジェクトがマッピングされる前の状態)のオブジェクトが渡されるケースがある。

    Tip

    ResultContextのメソッドについて

    ResultHandler#handleResultメソッドの引数であるResultContextには、以下のメソッドが用意されている。

    項番

    メソッド

    説明

    getResultObject

    検索結果がマッピングされたオブジェクトを取得するためのメソッド。

    getResultCount

    ResultHandler#handleResultメソッドの呼び出し回数を取得するためのメソッド。

    stop

    以降のレコードに対する処理を中止するようにMyBatis側に通知するためのメソッド。 このメソッドは、以降のレコードを全て破棄したい場合に使用するとよい。

    ResultContextにはisStoppedというメソッドもあるが、これはMyBatis側が使用するメソッドなので、説明は割愛する。


6.2.3.4. SQL実行モードの利用

MyBatis3では、SQLを実行するモードとして以下の3種類を用意しており、デフォルトはSIMPLEである。

ここでは、

  • 実行モードの使用方法

  • バッチモードのRepository利用時の注意点

について説明を行う。
実行モードの説明については、「SQL実行モードの設定」を参照されたい。

6.2.3.4.1. PreparedStatement再利用モードの利用

実行モードをSIMPLEからREUSEに変更した場合、MyBatis内部のPreparedStatementの扱い方は変わるが、MyBatisの動作(使い方)は変わらない。

実行モードをデフォルト(SIMPLE)からREUSEに変更する方法を、以下に示す。

  • projectName-domain/src/main/xxx/yyy/zzz/config/app/mybatis/MybatisConfig.java

    public static Configuration configuration() throws IOException {
        Configuration configuration = new Configuration();
    
        // omitted
    
        setSettings(configuration);
    
        return configuration;
    }
    
    private static void setSettings(Configuration configuration) {
    
        // omitted
    
        configuration.setDefaultExecutorType(ExecutorType.REUSE); // (1)
    }
    

    項番

    説明

    defaultExecutorTypeREUSEに変更する。

    上記設定を行うと、デフォルト動作がPreparedStatement再利用モードになる。


6.2.3.4.2. バッチモードの利用

Mapperインタフェースの更新系メソッドの呼び出しを、全てバッチモードで実行する場合は、「PreparedStatement再利用モードの利用」と同じ方法で、実行モードをBATCHモードに変更すればよい。

ただし、バッチモードはいくつかの制約事項があるため、実際のアプリケーション開発ではSIMPLE又はREUSEモードと共存して使用するケースが想定される。

例えば、

  • 大量のデータ更新を伴い性能要件を充たす事が最優先される処理では、バッチモードを使用する。

  • 楽観ロックの制御などデータの一貫性を保つために更新結果の判定が必要な処理では、SIMPLE又はREUSEモードを使用する。

等の使い分けを行う場合である。

Warning

実行モードを共存して使用する際の注意点

アプリケーション内で複数の実行モードを使用する場合は、同一トランザクション内で実行モードを切り替える事が出来ないという点に注意すること。

仮に同一トランザクション内で複数の実行モードを使用した場合は、MyBatisが矛盾を検知しエラーとなる。

これは、同一トランザクション内の処理において、

  • XxxRepositoryのメソッド呼び出しはBATCHモードで実行する

  • YyyRepositoryのメソッド呼び出しはREUSEモードで実行する

といった事が出来ないという事を意味する。

本ガイドラインをベースに作成するアプリケーションのトランザクション境界は、Service又はRepositoryとなる。そのため、アプリケーション内で複数の実行モードを使用する場合は、ServiceやRepositoryの設計を行う際に、実行モードを意識する必要がある。

トランザクションを分離させたい場合は、ServiceやRepositoryのメソッドアノテーションとして、@Transactional(propagation = Propagation.REQUIRES_NEW)を指定する事で実現する事ができる。

トランザクション管理の詳細については、「トランザクション管理について」を参照されたい。


以降では、

  • 複数の実行モードを共存させるための設定方法

  • アプリケーションの実装例

について説明を行う。


6.2.3.4.2.1. 個別にバッチモードのRepositoryを作成するための設定

特定のRepositoryに対してバッチモードのRepositoryを作成したい場合は、MyBatis-Springから提供されているorg.mybatis.spring.mapper.MapperFactoryBeanを使用して、RepositoryのBean定義を行えばよい。

下記の設定例では、

  • 通常使用するRepositoryとしてREUSEモードのRepository

  • 特定のRepositoryに対してBATCHモードのRepository

をBean登録している。

  • projectName-domain/src/main/xxx/yyy/zzz/config/app/ProjectNameInfraConfig.java

    @Configuration
    @MapperScan(basePackages = "com.example.domain.repository", sqlSessionFactoryRef = "sqlSessionFactory") // (2)
    public class ProjectNameInfraConfig {
    
        // (1)
        @Bean("sqlSessionFactory")
        public SqlSessionFactoryBean sqlSessionFactory(
                @Qualifier("dataSource") DataSource dataSource,
                @Qualifier("databaseIdProvider") VendorDatabaseIdProvider databaseIdProvider) throws IOException {
            SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
            bean.setDataSource(dataSource);
            bean.setDatabaseIdProvider(databaseIdProvider);
            bean.setConfiguration(MybatisConfig.configuration());
            return bean;
        }
    
        // (3)
        @Bean("batchSqlSessionTemplate")
        public SqlSessionTemplate batchSqlSessionTemplate(
                @Qualifier("sqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
            return new SqlSessionTemplate(sqlSessionFactory, ExecutorType.BATCH);
        }
    
        // (4)
        @Bean("todoBatchRepository")
        public MapperFactoryBean<TodoBatchRepository> todoBatchRepository(
                @Qualifier("batchSqlSessionTemplate") SqlSessionTemplate batchSqlSessionTemplate) {
            MapperFactoryBean<TodoBatchRepository> bean = new MapperFactoryBean<TodoBatchRepository>();
            // (5)
            bean.setMapperInterface(TodoBatchRepository.class);
            // (6)
            bean.setSqlSessionTemplate(batchSqlSessionTemplate);
            return bean;
        }
    

    項番

    説明

    通常使用するRepositoryで利用するためのSqlSessionTemplateをBean定義する。

    通常使用するRepositoryをスキャンしBean登録する。

    template-ref属性に、(1)で定義したSqlSessionTemplateを指定する。

    バッチモードのRepositoryで利用するためのSqlSessionTemplateをBean定義する。

    バッチモード用のRepositoryをBean定義する。

    id属性には、(2)でスキャンしたRepositoryのBean名と重複しない値を指定する。
    (2)でスキャンされたRepositoryのBean名は、インタフェース名を「lowerCamelCase」にした値にとなる。

    上記例では、バッチモード用のTodoRepositorytodoBatchRepositoryという名前のBeanでBean登録される。

    mapperInterfaceプロパティには、 バッチモードを利用するRepositoryのインタフェース名(FQCN)を指定する。

    sqlSessionTemplateプロパティには、 (3)で定義したバッチモード用のSqlSessionTemplateを指定する。


6.2.3.4.2.2. 一括でバッチモードのRepositoryを作成するための設定

一括でバッチモードのRepositoryを作成したい場合は、MyBatis-Springから提供されているスキャン機能(mybatis:scan要素)を使用して、RepositoryのBean定義を行えばよい。

下記の設定例では、全てのRepositoryに対して、REUSEモードとBATCHモードのRepositoryをBean登録している。

  • BeanNameGeneratorを作成する。

    package com.example.domain.repository;
    
    import org.springframework.beans.factory.config.BeanDefinition;
    import org.springframework.beans.factory.support.BeanDefinitionRegistry;
    import org.springframework.beans.factory.support.BeanNameGenerator;
    import org.springframework.util.ClassUtils;
    
    import java.beans.Introspector;
    
    // (1)
    public class BachRepositoryBeanNameGenerator implements BeanNameGenerator {
        // (2)
        @Override
        public String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry registry) {
            String defaultBeanName = Introspector.decapitalize(ClassUtils.getShortName(definition
                    .getBeanClassName()));
            return defaultBeanName.replaceAll("Repository", "BatchRepository");
        }
    }
    

    項番

    説明

    SpringのApplicationContextに登録するBean名を生成するクラスを作成する。

    このクラスは、通常使用するREUSEモードのRepositoryのBean名と、BATCHモードのBean名が重複しないようにするために必要なクラスである。

    Bean名を生成するためのメソッドを実装する。

    上記例では、Bean名のsuffixをBatchRepositoryとする事で、通常使用されるREUSEモードのRepositoryのBean名と重複しないようにしている。


  • projectName-domain/src/main/xxx/yyy/zzz/config/app/ProjectNameInfraConfig.java

    @Configuration
    @MapperScan(basePackages = "com.example.domain.repository",
        sqlSessionTemplateRef = "batchSqlSessionTemplate",
        nameGenerator = BatchRepositoryBeanNameGenerator.class) // (3)
    public class ProjectNameInfraConfig {
    
        @Bean("sqlSessionFactory")
        public SqlSessionFactoryBean sqlSessionFactory(
                @Qualifier("dataSource") DataSource dataSource,
                @Qualifier("databaseIdProvider") VendorDatabaseIdProvider databaseIdProvider) throws IOException {
            SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
            bean.setDataSource(dataSource);
            bean.setDatabaseIdProvider(databaseIdProvider);
            bean.setConfiguration(MybatisConfig.configuration());
            return bean;
        }
    
        @Bean("batchSqlSessionTemplate")
        public SqlSessionTemplate batchSqlSessionTemplate(
                @Qualifier("sqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
            return new SqlSessionTemplate(sqlSessionFactory, ExecutorType.BATCH);
        }
    
        // omitted
    

    項番

    属性

    説明

    -

    @MapperScanアノテーションを使用して、バッチモードのRepositoryをBean登録する。

    basePackages

    Repositoryをスキャンするベースパッケージを指定する。

    指定パッケージの配下に存在するRepositoryインタフェースがスキャンされ、SpringのApplicationContextにBean登録される。

    sqlSessionTemplateRef

    バッチモード用のSqlSessionTemplateのBeanを指定する。

    nameGenerator

    スキャンしたRepositoryのBean名を生成するためのクラスを指定する。

    具体的には、(1)で作成したクラスのクラス名(FQCN)を指定する。

    この指定を省略した場合、Bean名が重複するため、バッチモードのRepositoryはSpringのApplicationContextに登録されない。


6.2.3.4.2.3. バッチモードのRepositoryの使用例

以下に、バッチモードのRepositoryを使用してデータベースにアクセスするための実装例を示す。

@Transactional
@Service
public class TodoServiceImpl implements TodoService {

    // (1)
    @Inject
    @Named("todoBatchRepository")
    TodoRepository todoBatchRepository;

    @Override
    public void updateTodos(List<Todo> todos) {
        for (Todo todo : todos) {
            // (2)
            todoBatchRepository.update(todo);
        }
    }

}

項番

説明

バッチモードのRepositoryをインジェクションする。

バッチモードのRepositoryのメソッドを呼び出し、Entityの更新を行う。

バッチモードのRepositoryの場合は、メソッドを呼び出したタイミングでSQLが実行されないため、メソッドから返却される更新結果は無視する必要がある。

Entityを更新するためのSQLは、トランザクションがコミットされる直前にバッチ実行され、エラーがなければコミットされる。

Note

バッチ実行のタイミングについて

SQLがバッチ実行されるタイミングは、基本的には以下の場合である。

  • トランザクションがコミットされる直前

  • クエリ(SELECT)を実行する直前

Repositoryのメソッドの呼び出し順番に関する注意点は、「Repositoryのメソッドの呼び出し順番」を参照されたい。


6.2.3.4.3. バッチモードのRepository利用時の注意点

バッチモードのRepositoryを利用する場合、Serviceクラスの実装として、以下の点に注意する必要がある。


6.2.3.4.3.1. 更新結果の判定

バッチモードのRepositoryを使用した場合、更新結果の妥当性をチェックする事ができない。

バッチモードを使用する場合、Mapperインタフェースのメソッドから返却される更新結果は、

  • 返り値が数値(intlong)の場合は、固定値(org.apache.ibatis.executor.BatchExecutor#BATCH_UPDATE_RETURN_VALUE)

  • 返り値がbooleanの場合は、false

が返却される。

これは、Mapperインタフェースのメソッドを呼び出したタイミングではSQLが発行されず、バッチ実行用にキューイング(java.sql.Statement#addBatch())される仕組みになっているためである。

これは、以下の様な実装が出来ないことを意味している。

@Transactional
@Service
public class TodoServiceImpl implements TodoService {

    @Inject
    @Named("todoBatchRepository")
    TodoRepository todoBatchRepository;

    @Override
    public void updateTodos(List<Todo> todos) {
        for (Todo todo : todos) {
            boolean updateSuccess = todoBatchRepository.update(todo);
            // (1)
            if (!updateSuccess) {
                // ...
            }
        }
    }

}

項番

説明

上記例のように実装した場合、更新結果は常にfalseになるため、必ず更新失敗時の処理が実行されてしまう。

アプリケーションの要件によっては、バッチ実行した更新結果の妥当性をチェックすることが求められるケースも考えられる。
そのようなケースでは、Mapperインタフェースに「バッチ実行用にキューイングされているSQLを実行するためのメソッド」を用意すればよい。

MyBatis 3.2系では、org.apache.ibatis.session.SqlSessionインタフェースのflushStatementsメソッドを直接呼び出す必要があったが、MyBatis 3.3.0以降のバージョンでは、Mapperインタフェースに@org.apache.ibatis.annotations.Flushアノテーションを付与したメソッドを作成する方法がサポートされている。

Warning

バッチモード使用時のJDBCドライバが返却する更新結果について

@Flushアノテーションを付与したメソッド(及びSqlSessionインタフェースのflushStatementsメソッド)を使用するとバッチ実行時の更新結果を受け取る事ができると前述したが、JDBCドライバから返却される更新結果が「処理したレコード数」になる保証はない。

これは、使用するJDBCドライバの実装に依存する部分なので、使用するJDBCドライバの仕様を確認しておく必要がある。

以下に、@Flushアノテーションを付与したメソッドの作成例と呼び出し例を示す。

public interface TodoRepository {
    // ...
    @Flush // (1)
    List<BatchResult> flush();
}
@Transactional
@Service
public class TodoServiceImpl implements TodoService {

    @Inject
    @Named("todoBatchRepository")
    TodoRepository todoBatchRepository;

    @Override
    public void updateTodos(List<Todo> todos) {

        for (Todo todo : todos) {
            todoBatchRepository.update(todo);
        }

        List<BatchResult> updateResults = todoBatchRepository.flush(); // (2)

        // Validate update results
        // ...

    }

}

項番

説明

@Flushアノテーションを付与したメソッド(以降「@Flushメソッド」と呼ぶ)を作成する。

更新結果の判定が必要な場合は、返り値としてorg.apache.ibatis.executor.BatchResultのリスト型を指定する。
更新結果の判定が不要な場合(一意制約違反などのデータベースエラーのみをハンドリングしたい場合)は、返り値はvoidでよい。
バッチ実行用にキューイングされているSQLを実行したいタイミングで、@Flushメソッドを呼び出す。
@Flushメソッドを呼び出すと、Mapperインタフェースに紐づくSqlSessionオブジェクトのflushStatementsメソッドが呼び出されて、バッチ実行用にキューイングされているSQLが実行される。

更新結果の判定が必要な場合は、@Flushメソッドから返却される更新結果の妥当性チェックを行う。


6.2.3.4.3.2. 一意制約違反の検知方法

バッチモードのRepositoryを使用した場合、一意制約違反などのデータベースエラーをServiceの処理として検知する事が出来ないケースがある。

これは、Mapperインタフェースのメソッドを呼び出したタイミングではSQLが発行されず、バッチ実行用にキューイング(java.sql.Statement#addBatch())される仕組みになっているためであり、以下の様な実装が出来ないことを意味している。

@Transactional
@Service
public class TodoServiceImpl implements TodoService {

    @Inject
    @Named("todoBatchRepository")
    TodoRepository todoBatchRepository;

    @Override
    public void storeTodos(List<Todo> todos) {
        for (Todo todo : todos) {
            try {
                todoBatchRepository.create(todo);
            // (1)
            } catch (DuplicateKeyException e) {
                // ....
            }
        }
    }

}

項番

説明

上記例のように実装した場合、 このタイミングでorg.springframework.dao.DuplicateKeyExceptionが発生することはないため、DuplicateKeyException補足後の処理が実行される事はない。

これは、SQLがバッチ実行されるタイミングが、Serviceの処理が終わった後(トランザクションがコミットされる直前)に行われるためである。

アプリケーションの要件によっては、バッチ実行時の一意制約違反を検知することが求められるケースも考えられる。
そのようなケースでは、Mapperインタフェースに「バッチ実行用にキューイングされているSQLを実行するためのメソッド(@Flushメソッド)」を用意すればよい。
@Flushメソッドの詳細は、前述の「更新結果の判定」を参照されたい。

6.2.3.4.3.3. Repositoryのメソッドの呼び出し順番

バッチモードを使用する目的は更新処理の性能向上であるが、Repositoryのメソッドの呼び出し順番を間違えると、性能向上につながらないケースがある。

バッチモードを使用して性能向上させるためには、以下のMyBatisの仕様を理解しておく必要がある。

  • クエリ(SELECT)を実行すると、それまでキューイングされていたSQLがバッチ実行される。

  • 連続して呼び出された更新処理(Repositoryのメソッド)毎にPreparedStatementが生成され、SQLをキューイングする。

これは、以下の様な実装をすると、バッチモードを利用するメリットがない事を意味している。

  • 例1

    @Transactional
    @Service
    public class TodoServiceImpl implements TodoService {
    
        @Inject
        @Named("todoBatchRepository")
        TodoRepository todoBatchRepository;
    
        @Override
        public void storeTodos(List<Todo> todos) {
            for (Todo todo : todos) {
                // (1)
                Todo currentTodo = todoBatchRepository.findByTodoId(todo.getTodoId());
                if (currentTodo == null) {
                    todoBatchRepository.create(todo);
                } else{
                    todoBatchRepository.update(todo);
                }
            }
        }
    
    }
    

    項番

    説明

    上記例のように実装した場合、繰り返し処理の先頭にクエリを発行しているため、1件毎にSQLがバッチ実行される事になってしまう。
    これはほぼ、シンプルモード(SIMPLE)で実行しているのと同義である。

    上記のような処理が必要な場合は、PreparedStatement再利用モード(REUSE)のRepositoryを使用した方が効率的である。

  • 例2

    @Transactional
    @Service
    public class TodoServiceImpl implements TodoService {
    
        @Inject
        @Named("todoBatchRepository")
        TodoRepository todoBatchRepository;
    
        @Override
        public void storeTodos(List<Todo> todos) {
            for (Todo todo : todos) {
                // (2)
                todoBatchRepository.create(todo);
                todoBatchRepository.createHistory(todo);
            }
        }
    
    }
    

    項番

    説明

    上記のような処理が必要な場合は、Repositoryのメソッドが交互に呼び出されているため、1件毎にPreparedStatementが生成されてしまう。
    これはほぼ、シンプルモード(SIMPLE)で実行しているのと同義である。

    上記のような処理が必要な場合は、PreparedStatement再利用モード(REUSE)のRepositoryを使用した方が効率的である。


6.2.3.5. ストアドプロシージャの実装

データベースに登録されているストアドプロシージャやファンクションを、MyBatis3から呼び出す方法について説明を行う。

以下で説明する実装例では、PostgreSQLに登録されているファンクションを呼び出している。

  • ストアドプロシージャ(ファンクション)を登録する。

    /* (1) */
    CREATE FUNCTION findTodo(pTodoId CHAR)
    RETURNS TABLE(
        todo_id CHAR,
        todo_title VARCHAR,
        finished BOOLEAN,
        created_at TIMESTAMP,
        version BIGINT
    ) AS $$ BEGIN RETURN QUERY
    SELECT
        t.todo_id,
        t.todo_title,
        t.finished,
        t.created_at,
        t.version
    FROM
        t_todo t
    WHERE
        t.todo_id = pTodoId;
    END;
    $$ LANGUAGE plpgsql;
    

    項番

    説明

    このファンクションは、指定されたIDのレコードを取得するファンクションである。


  • Repositoryインタフェースにメソッドを定義する。

    // (2)
    public interface TodoRepository extends Repository {
        Todo findByTodoId(String todoId);
    }
    

    項番

    説明

    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="com.example.domain.repository.todo.TodoRepository">
    
        <!-- (3) -->
        <select id="findByTodoId" parameterType="string" resultType="Todo"
                statementType="CALLABLE">
            <!-- (4) -->
            {call findTodo(#{todoId})}
        </select>
    
    </mapper>
    

    項番

    説明

    ストアドプロシージャを呼び出すステートメントを実装する。

    ストアドプロシージャを呼び出す場合は、statementType属性にCALLABLEを指定する。
    CALLABLEを指定すると、java.sql.CallableStatementを使用してストアドプロシージャが呼び出される。

    OUTパラメータをJavaBeanにマッピングするために、resultType属性又はresultMap属性を指定する。

    ストアドプロシージャを呼び出す。

    ストアドプロシージャ(ファンクション)を呼び出す場合は、

    • {call Procedure or Function名(INパラメータ...)}

    形式で指定する。

    上記例では、findTodoという名前のファンクションに対して、INパラメータにIDを指定して呼び出している。


6.2.4. Appendix

6.2.4.1. Mapperインタフェースの仕組みについて

Mapperインタフェースを使用する場合、開発者はMapperインタフェースとマッピングファイルを作成するだけで、SQLを実行する事ができる。
Mapperインタフェースの実装クラスは、MyBatis3がJDKのProxy機能を使用してアプリケーション実行時に生成されるため、開発者がMapperインタフェースの実装クラスを作成する必要はない。
Mapperインタフェースは、MyBatis3から提供されているインタフェースの継承やアノテーションなどの定義は不要であり、単にJavaのインタフェースとして作成すればよい。
以下に、Mapperインタフェースとマッピングファイルの作成例、及びアプリケーション(Service)での利用例を示す。
ここでは、開発者が作成する成果物をイメージしてもらう事が目的なので、コードに対する説明はポイントとなる点に絞って行っている。
  • Mapperインタフェースの作成例

    本ガイドラインでは、MyBatis3のMapperインタフェースをRepositoryインタフェースとして使用することを前提としているため、 インタフェース名は、「Entity名」 + Repositoryというネーミングにしている。

    package com.example.domain.repository.todo;
    
    import com.example.domain.model.Todo;
    
    public interface TodoRepository {
        Todo findByTodoId(String todoId);
    }
    
  • マッピングファイルの作成例

    マッピングファイルでは、ネームスペースとしてMapperインタフェースのFQCN(Fully Qualified Class Name)を指定し、Mapperインタフェースに定義したメソッドの呼び出し時に実行するSQLとの紐づけは、各種ステートメントタグ(insert/update/delete/selectタグ)のid属性に、メソッド名を指定する事で行う事ができる。

    <?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="com.example.domain.repository.todo.TodoRepository">
    
        <resultMap id="todoResultMap" type="Todo">
            <result column="todo_id" property="todoId" />
            <result column="title" property="title" />
            <result column="finished" property="finished" />
        </resultMap>
    
        <select id="findByTodoId" parameterType="String" resultMap="todoResultMap">
          SELECT
            todo_id,
            title,
            finished
          FROM
            t_todo
          WHERE
            todo_id = #{todoId}
        </select>
    
    </mapper>
    
  • アプリケーション(Service)でのMapperインタフェースの使用例

    アプリケーション(Service)からMapperインタフェースのメソッドを呼び出す場合は、Spring(DIコンテナ)によって注入されたMapperオブジェクトのメソッドを呼び出す。

    アプリケーション(Service)は、Mapperオブジェクトのメソッドを呼び出すことで、透過的にSQLが実行され、SQLの実行結果を得ることができる。

    package com.example.domain.service.todo;
    
    import com.example.domain.model.Todo;
    import com.example.domain.repository.todo.TodoRepository;
    
    public class TodoServiceImpl implements TodoService {
    
        @Inject
        TodoRepository todoRepository;
    
        public Todo getTodo(String todoId){
            Todo todo = todoRepository.findByTodoId(todoId);
            if(todo == null){
                throw new ResourceNotFoundException(
                    ResultMessages.error().add("e.ex.td.5001" ,todoId));
            }
            return todo;
        }
    
    }
    

以下に、Mapperインタフェースのメソッドを呼び出した際に、SQLが実行されるまでの処理フローについて説明を行う。

Mapper mechanism

Picture - Mapper mechanism

項番

説明

アプリケーションは、Mapperインタフェースに定義されているメソッドを呼び出す。

Mapperインタフェースの実装クラス(MapperインタフェースのProxyオブジェクト)は、アプリケーション起動時にMyBatis3のコンポーネントによって生成される。

MapperインタフェースのProxyオブジェクトは、MapperProxyのinvokeメソッドを呼び出す。

MapperProxyは、Mapperインタフェースのメソッド呼び出しをハンドリングする役割をもつ。

MapperProxyは、呼び出されたMapperインタフェースのメソッドに対応する MapperMethodを生成し、executeメソッドを呼び出す。

MapperMethodは、 呼び出されたMapperインタフェースのメソッドに対応するSqlSessionのメソッドを呼び出す役割をもつ。

MapperMethodは、 SqlSessionのメソッドを呼び出す。

SqlSessionのメソッドを呼び出す際は、実行するSQLステートメントを特定するためのキー(以降、「ステートメントID」と呼ぶ)を引き渡している。

SqlSessionは、指定されたステートメントIDをキーに、マッピングファイルよりSQLステートメントを取得する。

SqlSessionは、マッピングファイルより取得したSQLステートメントに指定されているバインド変数に値を設定し、SQLを実行する。

Mapperインタフェース(SqlSession)は、SQLの実行結果をJavaBeanなどに変換して、アプリケーションに返却する。

件数のカウントや、更新件数などを取得する場合は、プリミティブ型やプリミティブラッパ型などが返却値となるケースもある。

Tip

ステートメントIDとは

ステートメントIDは、実行するSQLステートメントを特定するためのキーであり、「MapperインタフェースのFQCN + “.” + 呼び出されたMapperインタフェースのメソッド名」 というルールで生成される。

MapperMethodによって生成されたステートメントIDに対応するSQLステートメントをマッピングファイルに定義するためには、マッピングファイルのネームスペースに「MapperインタフェースのFQCN」、

各種ステートメントタグのid属性に「Mapperインタフェースのメソッド名」を指定する必要がある。


6.2.4.2. データベースによるSQL切り替えについて

MyBatis3では、JDBCドライバから接続しているデータベースのベンダー情報を取得して、使用するSQLを切り替える仕組み(org.apache.ibatis.mapping.VendorDatabaseIdProvider)を提供している。

この仕組みは、動作環境として複数のデータベースをサポートするようなアプリケーションを構築する際に有効である。

Note

本ガイドラインでは、環境依存するコンポーネントや設定ファイルについては、[projectName]-envというサブプロジェクトで管理し、ビルド時に実行環境にあったコンポーネントや設定ファイル作成を選択するスタイルを推奨している。

[projectName]-envは、

  • 開発環境(ローカルのPC環境)

  • 各種試験環境

  • 商用環境

上記それぞれの差分を吸収するためのサブプロジェクトであり、複数のデータベースをサポートするアプリケーションの開発でも利用する事ができる。

基本的には、環境依存するコンポーネントや設定ファイルは、[projectName]-envというサブプロジェクトで管理する事を推奨するが、SQLのちょっとした違いを吸収したい場合は、本仕組みを使用してもよい。

アーキテクトは、データベースの違いによるSQLの環境依存をどのように実装するかの指針を明確に示すことで、アプリケーション全体として統一された実装となるように心がけてほしい。


  • projectName-domain/src/main/xxx/yyy/zzz/config/app/ProjectNameInfraConfig.java

    @Configuration
    @MapperScan(basePackages = "com.example.domain.repository", sqlSessionFactoryRef = "sqlSessionFactory")
    @Import({ ProjectNameEnvConfig.class })
    public class ProjectNameInfraConfig {
    
        // (1)
        @Bean("databaseIdProvider")
        public VendorDatabaseIdProvider databaseIdProvider() {
            VendorDatabaseIdProvider bean = new VendorDatabaseIdProvider();
            Properties properties = new Properties();
            properties.setProperty("Oracle", "oracle"); // (2)
            properties.setProperty("PostgreSQL", "postgres"); // (2)
            properties.setProperty("H2", "h2"); // (2)
            bean.setProperties(properties);
            return bean;
        }
    
        @Bean("sqlSessionFactory")
        public SqlSessionFactoryBean sqlSessionFactory(
                @Qualifier("dataSource") DataSource dataSource) throws IOException {
            SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
            bean.setDataSource(dataSource);
            bean.setDatabaseIdProvider(databaseIdProvider()); // (3)
            bean.setConfiguration(MybatisConfig.configuration());
            return bean;
        }
    

    項番

    説明

    MyBatis3から提供されているVendorDatabaseIdProviderをBean定義する。

    VendorDatabaseIdProviderは、JDBCドライバから取得したデータベースのプロダクト名(java.sql.DatabaseMetaData#getDatabaseProductName())をデータベースIDとして扱うためのクラスである。

    propertiesプロパティには、JDBCドライバから取得したデータベースのプロダクト名とデータベースIDのマッピングを指定する。

    マッピング仕様については、「MyBatis3 REFERENCE DOCUMENTATION(Configuration-databaseIdProvider-)」を参照されたい。

    データベースIDを使用するSqlSessionFactoryBeandatabaseIdProviderプロパティ対して、(1)で定義したDatabaseIdProviderを指定する。

    この指定を行うと、マッピングファイルからデータベースIDを参照する事が可能となる。

Note

本ガイドラインでは、propertiesプロパティを指定して、データベースのプロダクト名とデータベースIDをマッピングする方式を推奨する。

理由は、JDBCドライバから取得できるデータベースのプロダクト名は、JDBCドライバのバージョンによって変わる可能性があるためである。

propertiesプロパティを使用すると、使用するJDBCドライバのバージョンによるプロダクト名の違いを、一箇所で管理する事ができる。


  • マッピングファイルの実装を行う。

    <insert id="create" parameterType="Todo">
        <!-- (1) -->
        <selectKey keyProperty="todoId" resultType="string" order="BEFORE"
                   databaseId="h2">
            SELECT RANDOM_UUID()
        </selectKey>
        <selectKey keyProperty="todoId" resultType="string" order="BEFORE"
                   databaseId="postgresql">
            SELECT UUID_GENERATE_V4()
        </selectKey>
    
        INSERT INTO
          t_todo
        (
            todo_id
            ,todo_title
            ,finished
            ,created_at
            ,version
        )
        VALUES
        (
            #{todoId}
            ,#{todoTitle}
            ,#{finished}
            ,#{createdAt}
            ,#{version}
        )
    </insert>
    

    項番

    説明

    ステートメント要素(select要素、update要素、sql要素など)をデータベース毎に切り替えたい場合は、各要素のdatabaseId属性にデータベースIDを指定する。

    databaseId属性を指定すると、データベースIDが一致するステートメント要素が使用される。

    上記例では、データベース固有のUUID生成関数を呼び出して、IDを生成している。

    Tip

    上記例では、PostgreSQLのUUID生成関数としてUUID_GENERATE_V4()を呼び出しているが、この関数は、uuid-osspと呼ばれるサブモジュールの関数である。

    この関数を使用したい場合は、uuid-osspモジュールを有効にする必要がある。

    Tip

    データベースIDは、OGNLベースの式(Expression言語)内でも参照する事ができる。

    これは、データベースIDを動的SQLの条件として使用できる事を意味している。 以下に実装例を紹介する。

    <select id="findAllByCreatedAtBefore" parameterType="_int" resultType="Todo">
        SELECT
            todo_id,
            todo_title,
            finished,
            created_at,
            version
        FROM
            t_todo
        WHERE
            <choose>
                <!-- (2) -->
                <when test="_databaseId == 'h2'">
                    <bind name="criteriaDate"
                          value="'DATEADD(\ 'DAY\ ',#{days} * -1,#{currentDate})'"/>
                </when>
                <when test="_databaseId == 'postgresql'">
                    <bind name="criteriaDate"
                          value="'#{currentDate}::DATE - (#{days} * INTERVAL \ '1 DAY\ ')'"/>
                </when>
            </choose>
            <![CDATA[
                created_at < ${criteriaDate}
            ]]>
    </select>
    

    項番

    説明

    OGNLベースの式(Expression言語)内では、_databaseIdという特別な変数にデータベースIDが格納されている。

    上記例では、「システム日付 - 指定日」より前に作成されたレコードを抽出するための条件を、データベースの関数を利用して指定している。


6.2.4.3. 関連Entityを1回のSQLで取得する方法について

主Entityと関連Entityを1回のSQLでまとめて取得する方法について説明する。

主Entityと関連Entityをまとめて取得する仕組みを使用すると、ServiceクラスでEntity(JavaBean)の組み立て処理を行う必要がなくなり、Serviceクラスは業務ロジック(ビジネスルール)の実装に集中する事ができる。

また、この方法は、N+1問題を回避する手段としても使用される。
N+1問題については、「N+1問題の対策方法」を参照されたい。

Warning

主Entityと関連Entityをまとめて取得する場合は、以下の点に注意して使用すること。

  • 以下の説明では全ての関連Entityを1回のSQLでまとめて取得しているが、実際のプロジェクトで使用する場合は、処理で必要となる関連Entityのみ取得するようにした方がよいケースがある。使用しない関連Entityを同時に取得すると、無駄なオブジェクト生成やマッピング処理が行われるため性能劣化の要因となる事がある。特に、一覧検索を行うSQLでは、必要な関連Entityのみ取得するようにした方がよいケースが多い。

  • 使用頻度の低い関連Entityについては、まとめて取得せず必要なときに個別に取得する方法を採用した方がよいケースがある。使用頻度の低い関連Entityを同時に取得すると、無駄なオブジェクト生成やマッピング処理が行われるため性能劣化の要因となる事がある。

  • 1:Nの関係となる関連Entityが複数含まれる場合、主Entityと関連Entityを別々に取得する方法を採用した方がよいケースがある。1:Nの関係となる関連Entityが複数ある場合、無駄なデータをDBから取得する必要があるため、性能劣化の要因となる事がある。主Entityと関連Entityを別々に取得する方法の一例については、「N+1問題の対策方法」を参照されたい。

Tip

使用頻度の低い関連Entityを必要になった時に個別に取得する方法としては、

  • Serviceクラスの処理で関連Entityを取得するメソッド(SQL)を呼び出して取得する。

  • 関連Entityを”Lazy Load”対象にし、Getterメソッドが呼び出された際にSQLを透過的に実行して取得する。

方法がある。

“Lazy Load”の仕組みを使用すると、ServiceクラスでEntity(JavaBean)の組み立て処理を行う必要がなくなり、Serviceクラスは業務ロジック(ビジネスルール)の実装に集中する事ができる。

一覧検索を行うSQLで”Lazy Load”を使用するとN+1問題を引き起こすので、使用する際は注意すること。

“Lazy Load”の使用方法については、「関連EntityをLazy Loadするための設定」を参照されたい。


ここからは、ショッピングサイトで扱う注文データを、1回のSQLでまとめて取得し、主Entity及び関連Entityにマッピングする実装例について説明を行う。

ここで説明する実装方法は、あくまで一例である。
MyBatis3では、本節で説明していない機能も多く提供しており、より高度なマッピングを行う事も可能である。

MyBatis3のマッピング機能の詳細については、「MyBatis3 REFERENCE DOCUMENTATION(Mapper XML Files-Result Maps-) 」を参照されたい。


6.2.4.3.1. テーブルレイアウトとデータ

説明で使用するテーブルは、以下の通り。

ER diagram

Picture - ER diagram

項番

カテゴリ

テーブル名

説明

トランザクション系

t_order

注文データを保持するテーブル。

1つの注文に対して、1レコードが格納される。

t_order_item

1つの注文で購入された商品データを保持するテーブル。

1つの注文で複数の商品が購入された場合は、商品数分レコードが格納される。

t_order_coupon

1つの注文で使用されたクーポンのデータを保持するテーブル。

1つの注文で、複数のクーポンが使用された場合は、クーポン数分レコードが格納される。
クーポンを使用しなかった場合は、レコードは格納されない。

マスタ系

m_item

商品を定義するマスタテーブル。

m_category

商品のカテゴリを定義するマスタテーブル。

m_item_category

商品が所属するカテゴリを定義するマスタテーブル。

商品とカテゴリのマッピングを保持している。
1つの商品は、複数のカテゴリに属すことができるモデルとなっている。

m_coupon

クーポンを定義するマスタテーブル。

コード系

c_order_status

注文ステータスを定義するコードテーブル。


説明で使用するテーブルレイアウトと格納データを作成するためのSQL(DDLとDML)を以下に示す。
(SQLはH2 Database用である)
  • マスタ系テーブル作成用のDDL

    CREATE TABLE m_item (
        code CHAR(10),
        name NVARCHAR(256),
        price INTEGER,
        CONSTRAINT m_item_pk PRIMARY KEY(code)
    );
    
    CREATE TABLE m_category (
        code CHAR(10),
        name NVARCHAR(256),
        CONSTRAINT m_category_pk PRIMARY KEY(code)
    );
    
    CREATE TABLE m_item_category (
        item_code CHAR(10),
        category_code CHAR(10),
        CONSTRAINT m_item_category_pk PRIMARY KEY(item_code, category_code),
        CONSTRAINT m_item_category_fk1 FOREIGN KEY(item_code) REFERENCES m_item(code),
        CONSTRAINT m_item_category_fk2 FOREIGN KEY(category_code) REFERENCES m_category(code)
    );
    
    CREATE TABLE m_coupon (
        code CHAR(10),
        name NVARCHAR(256),
        price INTEGER,
        CONSTRAINT m_coupon_pk PRIMARY KEY(code)
    );
    
  • コード系テーブル作成用のDDL

    CREATE TABLE c_order_status (
        code VARCHAR(10),
        name NVARCHAR(256),
        CONSTRAINT c_order_status_pk PRIMARY KEY(code)
    );
    
  • トランザクション系テーブル作成用のDDL

    CREATE TABLE t_order (
        id INTEGER,
        status_code VARCHAR(10),
        CONSTRAINT t_order_pk PRIMARY KEY(id),
        CONSTRAINT t_order_fk FOREIGN KEY(status_code) REFERENCES c_order_status(code)
    );
    
    CREATE TABLE t_order_item (
        order_id INTEGER,
        item_code CHAR(10),
        quantity INTEGER,
        CONSTRAINT t_order_item_pk PRIMARY KEY(order_id, item_code),
        CONSTRAINT t_order_item_fk1 FOREIGN KEY(order_id) REFERENCES t_order(id),
        CONSTRAINT t_order_item_fk2 FOREIGN KEY(item_code) REFERENCES m_item(code)
    );
    
    CREATE TABLE t_order_coupon (
        order_id INTEGER,
        coupon_code CHAR(10),
        CONSTRAINT t_order_coupon_pk PRIMARY KEY(order_id, coupon_code),
        CONSTRAINT t_order_coupon_fk1 FOREIGN KEY(order_id) REFERENCES t_order(id),
        CONSTRAINT t_order_coupon_fk2 FOREIGN KEY(coupon_code) REFERENCES m_coupon(code)
    );
    
  • データ投入用のDML

    -- Setup master tables
    INSERT INTO m_item VALUES ('ITM0000001','Orange juice',100);
    INSERT INTO m_item VALUES ('ITM0000002','NotePC',100000);
    
    INSERT INTO m_category VALUES ('CTG0000001','Drink');
    INSERT INTO m_category VALUES ('CTG0000002','PC');
    INSERT INTO m_category VALUES ('CTG0000003','Hot selling');
    
    INSERT INTO m_item_category VALUES ('ITM0000001','CTG0000001');
    INSERT INTO m_item_category VALUES ('ITM0000002','CTG0000002');
    INSERT INTO m_item_category VALUES ('ITM0000002','CTG0000003');
    
    INSERT INTO m_coupon VALUES ('CPN0000001','Join coupon',3000);
    INSERT INTO m_coupon VALUES ('CPN0000002','PC coupon',30000);
    
    -- Setup code tables
    INSERT  INTO  c_order_status VALUES ('accepted','Order accepted');
    INSERT  INTO  c_order_status VALUES ('checking','Stock checking');
    INSERT  INTO  c_order_status VALUES ('shipped','Item Shipped');
    
    -- Setup transaction tables
    INSERT INTO t_order VALUES (1,'accepted');
    INSERT INTO t_order VALUES (2,'checking');
    
    INSERT INTO t_order_item VALUES (1,'ITM0000001',1);
    INSERT INTO t_order_item VALUES (1,'ITM0000002',2);
    INSERT INTO t_order_item VALUES (2,'ITM0000001',3);
    INSERT INTO t_order_item VALUES (2,'ITM0000002',4);
    
    INSERT INTO t_order_coupon VALUES (1,'CPN0000001');
    INSERT INTO t_order_coupon VALUES (1,'CPN0000002');
    
    COMMIT;
    

6.2.4.3.2. Entityのクラス図

実装例では、上記テーブルに格納されているレコードを、以下のEntity(JavaBean)にマッピングする。

Class(JavaBean) diagram

Picture - Class(JavaBean) diagram

項番

クラス名

説明

Order

t_orderテーブルの1レコードを表現するJavaBean。

関連Entityとして、OrderStatusを1件、OrderItemおよびOrderCouponを複数保持する。

public class Order implements Serializable {
    private static final long serialVersionUID = 1L;
    private int id;
    private OrderStatus orderStatus;
    List<OrderItem> orderItems;
    List<OrderCoupon> orderCoupons;
    // omitted
}

OrderItem

t_order_itemテーブルの1レコードを表現するJavaBean。

関連Entityとして、Itemを保持する。

public class OrderItem implements Serializable {
    private static final long serialVersionUID = 1L;
    private int orderId;
    private Item item;
    private int quantity;
    // omitted
}

OrderCoupon

t_order_couponテーブルの1コードを表現するJavaBean。

関連Entityとして、Couponを保持する。

public class OrderCoupon implements Serializable {
    private static final long serialVersionUID = 1L;
    private int orderId;
    private Coupon coupon;
    // omitted
}

Item

m_itemテーブルの1コードを表現するJavaBean。

関連オブジェクトとして、所属しているCategoryを複数保持する。
Categoryとの紐づけは、m_item_categoryテーブルによって行われる。
public class Item implements Serializable {
    private static final long serialVersionUID = 1L;
    private String code;
    private String name;
    private int price;
    private List<Category> categories;
    // omitted
}

Category

m_categoryテーブルの1レコードを表現するJavaBean。

public class Category implements Serializable {
    private static final long serialVersionUID = 1L;
    private String code;
    private String name;
    // omitted
}

Coupon

m_couponテーブルの1レコードを表現するJavaBean。

public class Coupon implements Serializable {
    private static final long serialVersionUID = 1L;
    private String code;
    private String name;
    private int price;
    // omitted
}

OrderStatus

c_order_statusテーブルの1レコードを表現するJavaBean。

public class OrderStatus implements Serializable {
    private static final long serialVersionUID = 1L;
    private String code;
    private String name;
    // omitted
}

6.2.4.3.3. Repositoryインタフェースの実装

実装例では、

  • Orderオブジェクトを1件取得するメソッド(findById)

  • 該当ページのOrderオブジェクトを取得するメソッド(findPageByPageable)

を実装する。

package com.example.domain.repository.order;

import com.example.domain.model.Order;

import java.util.List;

public interface OrderRepository {

    Order findById(int id);

    List<Order> findPageByPageable(@Param("pageable") Pageable pageable);

}

6.2.4.3.4. SQLの実装

関連Entityを1回のSQLでまとめて取得する場合は、取得対象のテーブルをJOINしてマッピングに必要な全てのレコードを取得する。

<?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="com.example.domain.repository.order.OrderRepository">

    <!-- (1) -->
    <sql id="selectFromJoin">
        SELECT
            /* (2) */
            o.id,
            /* (3) */
            o.status_code,
            os.name AS status_name,
            /* (4) */
            oi.quantity,
            i.code AS item_code,
            i.name AS item_name,
            i.price AS item_price,
            /* (5) */
            ct.code AS category_code,
            ct.name AS category_name,
            /* (6) */
            cp.code AS coupon_code,
            cp.name AS coupon_name,
            cp.price AS coupon_price
        FROM
            ${orderTable} o
        /* (7) */
        INNER JOIN c_order_status os ON os.code = o.status_code
        INNER JOIN t_order_item oi ON oi.order_id = o.id
        INNER JOIN m_item i ON i.code = oi.item_code
        INNER JOIN m_item_category ic ON ic.item_code = i.code
        INNER JOIN m_category ct ON ct.code = ic.category_code
        /* (8) */
        LEFT JOIN t_order_coupon oc ON oc.order_id = o.id
        LEFT JOIN m_coupon cp ON cp.code = oc.coupon_code
    </sql>

    <!-- (9) -->
    <select id="findById" parameterType="_int" resultMap="orderResultMap">
        <bind name="orderTable" value="'t_order'" />
        <include refid="selectFromJoin"/>
        WHERE
            o.id = #{id}
        ORDER BY
            item_code ASC,
            category_code ASC,
            coupon_code ASC
    </select>

    <!-- (10) -->
    <select id="findPageByPageable" resultMap="orderResultMap">
        <bind name="orderTable" value="
            '(
              SELECT
                  *
              FROM
                  t_order
              ORDER BY
                  id DESC
              LIMIT #{pageable.pageSize}
              OFFSET #{pageable.offset}
              )'" />
        <include refid="selectFromJoin"/>
        ORDER BY
            id DESC,
            item_code ASC,
            category_code ASC,
            coupon_code ASC
    </select>

    <!-- omitted -->

</mapper>

項番

説明

findByIdメソッドとfindPageByPageableメソッド用のSELECT句、FROM句、JOIN句を実装する。

上記例では、findByIdメソッドとfindPageByPageableメソッドの共通箇所を共通化している。

Orderオブジェクトを生成するために必要なデータを取得する。

OrderStatusオブジェクトを生成するために必要なデータを取得する。

取得するカラム名は重複しないようにする必要がある。
上記例では、nameカラムが重複するため、AS句を使用して別名(status_プレフィックス)を指定している。

OrderItemオブジェクトとItemオブジェクトを生成するために必要なデータを取得する。

取得するカラム名は重複しないようにする必要がある。
上記例では、code,name, priceが重複するため、AS句を使用して別名(item_プレフィックス)を指定している。

Categoryオブジェクトを生成するために必要なデータを取得する。

取得するカラム名は重複しないようにする必要がある。
上記例では、code,nameが重複するため、AS句を使用して別名(category_プレフィックス)を指定している。

OrderCouponオブジェクトとCouponオブジェクトを生成するために必要なデータを取得する。

取得するカラム名は重複しないようにする必要がある。
上記例では、code,name, priceが重複するため、AS句を使用して別名(coupon_プレフィックス)を指定している。

関連オブジェクトを生成するために必要なデータが格納されているテーブルを結合する。

レコードが格納されない可能性のあるテーブルについては、外部結合とする。
クーポンを使用しない場合、t_order_couponにレコードが格納されないので外部結合にする必要がある。
t_order_couponと結合するt_couponも同様である。

findByIdメソッド用のSQLを実装する。

ORDER BY句には、1:Nの関連をもつEntityの並び順を指定する。
上記例では、PKの昇順で並べ替えている。

findPageByPageableメソッド用のSQLを実装する。

ORDER BY句には、Orderと1:Nの関連をもつEntityの並び順を指定する。
上記例では、OrderはPKの降順(新しい順)、関連EntityはPKの昇順で並べ替えている。

Tip

1:Nの関連を持つ関連Entityを1回のSQLでまとめて取得する際にページネーション検索が必要な場合は、MyBatis3から提供されているRowBoundsを使用することが出来ない。

代替案としては、

  • まず主Entityのみを検索するメソッドを呼び出し、関連Entityは別途のメソッドを呼び出して取得する

  • SQLでページ範囲内の主Entityのみ格納されている仮想テーブルを作成し、仮想テーブルのレコードとJOINする事で、マッピングに必要な全てのレコードを取得する(上記例の findPageByPageableは、このパターンで実装している)

等の方法が考えられる。


上記SQL(findPage)を実行すると以下のレコードが取得される。
注文レコードとしては2件だが、レコードが複数件格納される関連テーブルと結合しているため、
合計で9レコードが取得される。

内訳は、

  • 1~3行目は、注文IDが”2“のOrderオブジェクトを生成するためのレコード

  • 4~9行目は、注文IDが”1“のOrderオブジェクトを生成するためレコード

となる。

以降の説明では、注文IDが”1“のレコードを例に、どのように検索結果(ResultSet)をJavaBeanにマッピングするかを説明していく。

Result Set of findPageByPageable

Picture - Result Set of findPageByPageable


6.2.4.3.5. マッピングの実装

上記レコードを、Orderオブジェクトと関連Entityにマッピングするための定義を以下に示す。

<?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="com.example.domain.repository.order.OrderRepository">

    <!-- omitted -->

    <!-- (1) -->
    <resultMap id="orderResultMap" type="Order">
        <id property="id" column="id"/>
        <!-- (2) -->
        <result property="orderStatus.code" column="status_code" />
        <result property="orderStatus.name" column="status_name" />
        <!-- (3) -->
        <collection property="orderItems" ofType="OrderItem">
            <id property="orderId" column="id"/>
            <id property="item.code" column="item_code"/>
            <result property="quantity" column="quantity"/>
            <association property="item" resultMap="itemResultMap"/>
        </collection>
        <!-- (4) -->
        <collection property="orderCoupons" ofType="OrderCoupon"
                    notNullColumn="coupon_code">
            <id property="orderId" column="id"/>
            <!-- (5) -->
            <id property="coupon.code" column="coupon_code"/>
            <result property="coupon.name" column="coupon_name"/>
            <result property="coupon.price" column="coupon_price"/>
        </collection>
    </resultMap>

    <!-- (6) -->
    <resultMap id="itemResultMap" type="Item">
        <id property="code" column="item_code"/>
        <result property="name" column="item_name"/>
        <result property="price" column="item_price"/>
        <!-- (7) -->
        <collection property="categories" ofType="Category">
            <id property="code" column="category_code"/>
            <result property="name" column="category_name"/>
        </collection>
    </resultMap>

</mapper>

項番

説明

取得したレコードをOrderオブジェクトにマッピングするための定義。
関連Entity(OrderStatus, OrderItem,OrderCoupon)のマッピングを行う。

取得したレコードをOrderStatusオブジェクトにマッピングするための定義。

取得したレコードをOrderItemオブジェクトにマッピングするための定義。 関連Entity(Item)へのマッピングは、別のresultMap(6)に委譲している。

取得したレコードをOrderCouponオブジェクトにマッピングするための定義。

取得したレコードをCouponオブジェクトにマッピングするための定義。

取得したレコードをItemオブジェクトにマッピングするための定義。

取得したレコードをCategoryオブジェクトにマッピングするための定義。


6.2.4.3.5.1. Orderオブジェクトへのマッピングの実装

Orderオブジェクトへのマッピングを行う。

<!-- (1) -->
<resultMap id="orderResultMap" type="Order">
    <!-- (2) -->
    <id property="id" column="id"/>
    <result property="orderStatus.code" column="status_code" />
    <result property="orderStatus.name" column="status_name" />
    <collection property="orderItems" ofType="OrderItem">
        <id property="orderId" column="id"/>
        <id property="item.code" column="item_code"/>
        <result property="quantity" column="quantity"/>
        <association property="item" resultMap="itemResultMap"/>
    </collection>
    <collection property="orderCoupons" ofType="OrderCoupon"
                notNullColumn="coupon_code">
        <id property="orderId" column="id"/>
        <id property="coupon.code" column="coupon_code"/>
        <result property="coupon.name" column="coupon_name"/>
        <result property="coupon.price" column="coupon_price"/>
    </collection>
</resultMap>
ResultMap for Order

Picture - ResultMap for Order

項番

説明

検索結果をOrderオブジェクトにマッピングする。

type属性にマッピングするクラスを指定する。

取得したレコードのidカラムの値を、Order#idプロパティに設定する。

idカラムはPKなので、id要素を使用してマッピングを行う。
id要素を使用すると、指定したプロパティの値でレコードがグループ化される。
具体的には、id=1id=2の2つにグループ化され、2つのOrderオブジェクトが生成される。

6.2.4.3.5.2. OrderStatusオブジェクトへのマッピングの実装

OrderStatusオブジェクトへのマッピングを行う。

Note

Entityの実装」のEntityクラスの作成方針では、「コード系テーブルは、Entityとして扱うのではなく、java.lang.Stringなどの基本型で扱う。」としている。

これは、コード系テーブルで保持しているデータは、「コードリスト」などの別の仕組みを使用するケースが多いためである。

本節では、関連Entity(JavaBean)へのマッピング方法を説明する事が目的なので、コード系テーブルもEntityとして扱っている点を補足しておく。

実際のプロジェクトでは、Entityクラスの作成方針を参考にEntityを作成することを推奨する。

<resultMap id="orderResultMap" type="Order">
    <id property="id" column="id"/>
    <!-- (1) -->
    <result property="orderStatus.code" column="status_code" />
    <!-- (2) -->
    <result property="orderStatus.name" column="status_name" />
    <collection property="orderItems" ofType="OrderItem">
        <id property="orderId" column="id"/>
        <id property="item.code" column="item_code"/>
        <result property="quantity" column="quantity"/>
        <association property="item" resultMap="itemResultMap"/>
    </collection>
    <collection property="orderCoupons" ofType="OrderCoupon"
                notNullColumn="coupon_code">
        <id property="orderId" column="id"/>
        <id property="coupon.code" column="coupon_code"/>
        <result property="coupon.name" column="coupon_name"/>
        <result property="coupon.price" column="coupon_price"/>
    </collection>
</resultMap>
ResultMap for OrderStatus

Picture - ResultMap for OrderStatus

項番

説明

取得したレコードのstatus_codeカラムの値を、OrderStatus#codeプロパティに設定する。

取得したレコードのstatus_nameカラムの値を、OrderStatus#nameプロパティに設定する。

Note

OrderStatusオブジェクトには、idカラムでグループ化されたレコードの値が設定される。


6.2.4.3.5.3. OrderItemオブジェクトへのマッピングの実装

OrderItemオブジェクトへのマッピングを行う。

<resultMap id="orderResultMap" type="Order">
    <id property="id" column="id"/>
    <result property="orderStatus.code" column="status_code" />
    <result property="orderStatus.name" column="status_name" />
    <!-- (1) -->
    <collection property="orderItems" ofType="OrderItem">
        <!-- (2) -->
        <id property="orderId" column="id"/>
        <!-- (3) -->
        <id property="item.code" column="item_code"/>
        <!-- (4) -->
        <result property="quantity" column="quantity"/>
        <!-- (5) -->
        <association property="item" resultMap="itemResultMap"/>
    </collection>
    <collection property="orderCoupons" ofType="OrderCoupon"
                notNullColumn="coupon_code">
        <id property="orderId" column="id"/>
        <id property="coupon.code" column="coupon_code"/>
        <result property="coupon.name" column="coupon_name"/>
        <result property="coupon.price" column="coupon_price"/>
    </collection>
</resultMap>
ResultMap for OrderItem

Picture - ResultMap for OrderItem

項番

説明

検索結果をOrderItemオブジェクトにマッピングし、Order#orderItemsプロパティに追加する。

1:Nの関係の関連Entityにマッピングする場合は、collection要素を使用する。
collection要素の詳細は、「MyBatis3 REFERENCE DOCUMENTATION(Mapper XML Files-collection-)」を参照されたい。

取得したレコードのidカラムの値を、OrderItem#orderIdプロパティに設定する。

idカラムはPKなので、id要素を使用してマッピングを行う。

取得したレコードのitem_codeカラムの値を、Item#codeプロパティに設定する。

item_codeカラムはPKなので、id要素を使用してマッピングを行う。
id要素を使用すると、指定したプロパティの値でレコードがグループ化される。
具体的には、Item#code=ITM0000001Item#code=ITM0000002の2つにグループ化され、2つのOrderItemオブジェクトが生成される。

取得したレコードのquantityカラムの値を、OrderItem#quantityプロパティに設定する。

Itemオブジェクトの生成を、別のresultMapに委譲し、生成されたオブジェクトをOrderItem#itemプロパティに設定する。
実際のマッピングは、「Itemオブジェクトへのマッピングの実装」を参照されたい。
1:1の関係の関連Entityにマッピングする場合は、association要素を使用する。
association要素の詳細は、「MyBatis3 REFERENCE DOCUMENTATION(Mapper XML Files-association-)」を参照されたい。

Note

OrderItemオブジェクトには、idカラムとitem_codeカラムでグループ化されたレコードの値が設定される。


6.2.4.3.5.4. Itemオブジェクトへのマッピングの実装

Itemオブジェクトへのマッピングを行う。

<!-- (1) -->
<resultMap id="itemResultMap" type="Item">
    <!-- (2) -->
    <id property="code" column="item_code"/>
    <!-- (3) -->
    <result property="name" column="item_name"/>
    <!-- (4) -->
    <result property="price" column="item_price"/>
    <collection property="categories" ofType="Category">
        <id property="code" column="category_code"/>
        <result property="name" column="category_name"/>
    </collection>
</resultMap>
ResultMap for Item

Picture - ResultMap for Item

項番

説明

検索結果をItemオブジェクトにマッピングする。

type属性にマッピングするクラスを指定する。

取得したレコードのitem_codeカラムの値を、Item#codeに設定する。

item_codeカラムはPKなので、id要素を使用してマッピングを行う。

取得したレコードのitem_nameカラムの値を、Item#nameに設定する。

取得したレコードのitem_priceカラムの値を、Item#priceに設定する。

Note

Itemオブジェクトには、idカラムとitem_codeカラムでグループ化されたレコードの値が設定される。


6.2.4.3.5.5. Categoryオブジェクトへのマッピングの実装

Categoryオブジェクトへのマッピングを行う。

<resultMap id="itemResultMap" type="Item">
    <id property="code" column="item_code"/>
    <result property="name" column="item_name"/>
    <result property="price" column="item_price"/>
    <!-- (1) -->
    <collection property="categories" ofType="Category">
        <!-- (2) -->
        <id property="code" column="category_code"/>
        <!-- (3) -->
        <result property="name" column="category_name"/>
    </collection>
</resultMap>
ResultMap for Category

Picture - ResultMap for Category

項番

説明

検索結果をCategoryオブジェクトにマッピングし、Item#categoriesプロパティに追加する。

1:Nの関係の関連Entityにマッピングする場合は、collection要素を使用する。
collection要素の詳細は、「MyBatis3 REFERENCE DOCUMENTATION(Mapper XML Files-collection-)」を参照されたい。

取得したレコードのcategory_codeカラムの値を、Category#codeに設定する。

category_codeカラムはPKなので、id要素を使用してマッピングを行う。
id要素を使用すると、指定したプロパティの値でレコードがグループ化される。

具体的には、

  • Item#code=ITM0000001のカテゴリとして、Category#code=CTG0000001Categoryオブジェクト

  • Item#code=ITM0000002のカテゴリとして、Category#code=CTG0000002Category#code=CTG0000003の2つのCategoryオブジェクト

が生成される。

取得したレコードのcategory_nameカラムの値を、Category#nameに設定する。

Note

Categoryオブジェクトには、idカラムとitem_codeカラムとcategory_codeカラムでグループ化されたレコードの値が設定される。


6.2.4.3.5.6. OrderCouponオブジェクトへのマッピングの実装

OrderCouponオブジェクトへのマッピングを行う。

<resultMap id="orderResultMap" type="Order">
    <id property="id" column="id"/>
    <result property="orderStatus.code" column="status_code" />
    <result property="orderStatus.name" column="status_name" />
    <collection property="orderItems" ofType="OrderItem">
        <id property="orderId" column="id"/>
        <id property="item.code" column="item_code"/>
        <result property="quantity" column="quantity"/>
        <association property="item" resultMap="itemResultMap"/>
    </collection>
    <!-- (1) -->
    <collection property="orderCoupons" ofType="OrderCoupon" notNullColumn="coupon_code">
        <!-- (2) -->
        <id property="orderId" column="id"/>
        <!-- (3) -->
        <id property="coupon.code" column="coupon_code"/>
        <result property="coupon.name" column="coupon_name"/>
        <result property="coupon.price" column="coupon_price"/>
    </collection>
</resultMap>
ResultMap for OrderCoupon

Picture - ResultMap for OrderCoupon

項番

説明

検索結果をOrderCouponオブジェクトにマッピングし、Order#orderCouponsプロパティに追加する。

1:Nの関係の関連Entityにマッピングする場合は、collection要素を使用する。
collection要素の詳細は、「MyBatis3 REFERENCE DOCUMENTATION(Mapper XML Files-collection-)」を参照されたい。

上記例にて、notNullColumn属性を指定している点に注目してほしい。

これはt_couponテーブルにレコードが存在しない時に、OrderCouponオブジェクトを生成させないための設定である。
本実装例では、idカラムが”2“のデータにはt_couponテーブルのレコードを格納していないため、検索結果をみると、coupon_codecoupon_namecoupon_priceの値がnullになっているのがわかる。
OrderCouponオブジェクトにマッピングするカラムがこの3つだけであれば、notNullColumn属性を指定する必要はないが、実装例ではidカラムの値をOrderCoupon#orderIdプロパティにマッピングする設定を行っているため、notNullColumn属性の指定が必要となる。
これは、マッピング対象のカラムの中にnullでない値がセットされていた場合に、MyBatisがオブジェクトを生成するためである。
上記例のように、notNullColumn属性にcoupon_codeカラムを指定しておくと、coupon_codeカラムがnullでない場合(つまり、レコードが存在する場合)にのみ、オブジェクトが生成される。
notNullColumn属性には、複数のカラムを指定する事もできる。

取得したレコードのidカラムの値を、OrderCoupon#orderIdプロパティに設定する。

orderIdはPKなので、id要素を使用する。

取得したレコードのcoupon_codeカラムの値をCoupon#codeに設定する。

coupon_codeカラムはPKなので、id要素を使用してマッピングを行う。
id要素を使用すると、指定したプロパティの値でレコードがグループ化される。

具体的には、Coupon#code=CPN0000001Coupon#code=CPN0000002の2つにグループ化され、2つのOrderCouponオブジェクトが生成される。


6.2.4.3.5.7. Couponオブジェクトへのマッピングの実装

Couponオブジェクトへのマッピングを行う。

<resultMap id="orderResultMap" type="Order">
    <id property="id" column="id"/>
    <result property="orderStatus.code" column="status_code" />
    <result property="orderStatus.name" column="status_name" />
    <collection property="orderItems" ofType="OrderItem">
        <id property="orderId" column="id"/>
        <id property="item.code" column="item_code"/>
        <result property="quantity" column="quantity"/>
        <association property="item" resultMap="itemResultMap"/>
    </collection>
    <collection property="orderCoupons" ofType="OrderCoupon" notNullColumn="coupon_code">
        <id property="orderId" column="id"/>
        <!-- (1) -->
        <id property="coupon.code" column="coupon_code"/>
        <!-- (2) -->
        <result property="coupon.name" column="coupon_name"/>
        <!-- (3) -->
        <result property="coupon.price" column="coupon_price"/>
    </collection>
</resultMap>
ResultMap for Coupon

Picture - ResultMap for Coupon

項番

説明

取得したレコードのcoupon_codeカラムの値を、Coupon#codeに設定する。

取得したレコードのcoupon_nameカラムの値を、Coupon#nameに設定する。

取得したレコードのcoupon_priceカラムの値を、Coupon#priceに設定する。

Note

Couponオブジェクトには、idカラムとcoupon_codeカラムでグループ化されたレコードの値が設定される。


6.2.4.3.5.8. マッピング後のオブジェクト図

実際にマッピングされたOrderオブジェクトおよび関連Entityの状態は、以下の通りである。

Mapped object diagram

Picture - Mapped object diagram

Orderオブジェクトにマッピングされたレコードとカラムは、以下の通りである。
グレーアウトしている部分は、グループ化によって、グレーアウトされていない部分にマージされる。
Valid Result Set

Picture - Valid Result Set

Warning

1:Nの関連をもつレコードをJOINしてマッピングする場合、グレーアウトされている部分のデータの取得が無駄になる点を、意識しておくこと。

Nの部分のデータを使用しない処理で、同じSQLを使用した場合、さらに無駄なデータの取得となってしまうので、Nの部分を取得するSQLと、取得しないSQLを、別々に用意しておくなどの工夫を行うこと。


6.2.4.4. 関連EntityをネストしたSQLを使用して取得する方法について

MyBatis3では、マッピング時に別のSQL(ネストしたSQL)を使用して関連Entityを取得する方法を提供している。

ネストしたSQLを使用して関連Entityを取得する仕組みを使用すると、

  • 個々のSQL定義

  • resultMap要素のマッピング定義

をシンプルにする事ができる。

Warning

各種定義がシンプルになる一方で、ネストしたSQLを多用すると、N+1問題を引き起こす要因になるという事を意識する必要がある。

ネストしたSQLを使用する場合のMyBatisのデフォルトの動作は、”Eager Load”となる。

これは、関連Entityの使用有無に関係なくSQLが発行される事を意味しており、

  • 無駄なSQLの実行とデータの取得

  • N+1問題

などが発生する危険性が高まる。

Tip

MyBatis3では、ネストしたSQLを使用して関連Entityを取得する際の動作を、”Lazy Load”に変更するためのオプションを提供している。

“Lazy Load”の使用方法については、「関連EntityをLazy Loadするための設定」を参照されたい。


6.2.4.4.1. 関連EntityをネストしたSQLを使用して取得する実装例

ネストしたSQLを使用して関連Entityを取得する際の実装例を以下に示す。

<resultMap id="itemResultMap" type="Item">
    <id property="code" column="item_code"/>
    <result property="name" column="item_name"/>
    <result property="price" column="item_price"/>
    <!-- (1) -->
    <collection property="categories" column="item_code"
        select="findAllCategoryByItemCode" />
</resultMap>

<select id="findAllCategoryByItemCode"
    parameterType="string" resultType="Category">
    SELECT
        ct.code,
        ct.name
    FROM
        m_item_category ic
    INNER JOIN m_category ct ON ct.code = ic.category_code
    WHERE
        ic.item_code = #{itemCode}
    ORDER BY
        code
</select>

項番

説明

association要素又はcollection要素のselect属性に、呼び出すSQLのステートメントIDを指定する。

column属性には、SQLに渡すパラメータ値が格納されているカラム名を指定する。
上記例では、
findAllCategoryByItemCodeのパラメータとしてitem_codeカラムの値を渡している。

指定可能な属性の詳細は、「MyBatis3 REFERENCE DOCUMENTATION(Mapper XML Files-Nested Select for Association-)」を参照されたい。

Note

上記例では、fetchType属性を指定していないため、”Lazy Load”と”Eager Load”のどちらで実行されるかは、アプリケーション全体の設定に依存する。

アプリケーション全体の設定については、「Lazy Loadを使用するためのMyBatisの設定」を参照されたい。


6.2.4.4.2. 関連EntityをLazy Loadするための設定

ネストしたSQLを使用して関連Entityを取得する際のMyBatis3のデフォルト動作は、”Eager Load”であるが、”Lazy Load”を使用する事も可能である。

以下に、”Lazy Load”を使用するために最低限必要な設定及び使用方法について説明を行う。

説明していない設定値については、「MyBatis3 REFERENCE DOCUMENTATION(Mapper XML Files-settings-)」を参照されたい。


6.2.4.4.2.1. バイトコード操作ライブラリの追加

“Lazy Load”を使用する場合は、”Lazy Load”を実現するためのProxyオブジェクトを生成するために、

  • JAVASSIST

  • CGLIB

のいずれか一方のライブラリが必要となる。

MyBatis 3.2系まではCGLIBがデフォルトで使用されるライブラリであったが、MyBatis 3.3.0以降のバージョンではJAVASSISTがデフォルトで使用される。
さらに、MyBatis 3.3.0からJAVASSISTがMyBatis本体に内包されているため、ライブラリを追加しなくても”Lazy Load”を使用する事ができる。

Note

MyBatis 3.3.0以降のバージョンでCGLIBを使用する場合は、

  • pom.xmlにCGLIBのアーティファクトを追加

  • MyBatis設定ファイル(projectName-domain/src/main/resources/META-INF/mybatis/mybatis-config.xml)に「proxyFactory=CGLIB」を追加

すればよい。

CGLIBのアーティファクト情報については、「MyBatis3 PROJECT DOCUMENTATION(Project Dependencies-compile-)」を参照されたい。


6.2.4.4.2.2. Lazy Loadを使用するためのMyBatisの設定

MyBatis3では、”Lazy Load”の使用有無を、

  • アプリケーションの全体設定(MyBatis設定ファイル)

  • 個別設定(マッピングファイル)

の2箇所で指定する事ができる。

  • アプリケーションの全体設定は、 MyBatis設定ファイル(projectName-domain/src/main/resources/META-INF/mybatis/mybatis-config.xml)に指定する。

    <?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>
            <!-- (1) -->
            <setting name="lazyLoadingEnabled" value="true"/>
        </settings>
    </configuration>
    

    項番

    説明

    アプリケーションのデフォルト動作をlazyLoadingEnabledに指定する。

    • true: “Lazy Load”

    • false: “Eager Load” (デフォルト)

    association要素とcollection要素のfetchType属性を指定した場合は、fetchType属性の指定値が優先される。

    Warning

    false: “Eager Load”」の状態でassociation要素又はcollection要素のselect属性を使用すると、マッピング時にSQLが実行されるので、注意が必要である。

    特に理由がない場合は、lazyLoadingEnabledtrueにする事を推奨する。


  • 個別設定は、マッピングファイルのassociation要素とcollection要素のfetchType属性で指定する。

    <resultMap id="itemResultMap" type="Item">
        <id property="code" column="item_code"/>
        <result property="name" column="item_name"/>
        <result property="price" column="item_price"/>
        <!-- (2) -->
        <collection property="categories" column="item_code"
            fetchType="lazy"
            select="findAllCategoryByItemCode" />
    </resultMap>
    
    <select id="findAllCategoryByItemCode"
        parameterType="string" resultType="Category">
        SELECT
            ct.code,
            ct.name
        FROM
            m_item_category ic
        INNER JOIN m_category ct ON ct.code = ic.category_code
        WHERE
            ic.item_code = #{itemCode}
        ORDER BY
            code
    </select>
    

    項番

    説明

    association要素又はcollection要素のfetchType属性に、lazy又はeagerを指定する。

    fetchType属性を指定すると、アプリケーション全体の設定を上書きする事ができる。


6.2.4.4.2.3. Lazy Loadの実行タイミングを制御するための設定

MyBatis3では、”Lazy Load”を実行するタイミングを制御するためのオプション(aggressiveLazyLoading)を提供している[1]

このオプションのデフォルト値はMybatis 3.4.2以降からfalseであり、”Lazy Load”対象となっているプロパティのgetterメソッドが呼び出されたタイミングで実行する。

Warning

aggressiveLazyLoadingが「true」の場合、”Lazy Load”対象となっているプロパティを保持するオブジェクトのgetterメソッドが呼び出されたタイミングで”Lazy Load”が実行される。 このため、実際にはデータの取得が必要ないにもかかわらずSQLが実行されてしまう可能性があることに注意が必要である。

具体的には、以下のようなマッピングを行い、”Lazy Load”対象になっていないプロパティだけにアクセスするケースである。「true」の場合、”Lazy Load”対象のプロパティに対して直接アクセスしなくても、”Lazy Load”が実行されてしまう。

特に理由がない場合は、aggressiveLazyLoadingは「false」(デフォルト)のまま変更しないことを推奨する。

  • Entity

    public class Item implements Serializable {
        private static final long serialVersionUID = 1L;
        private String code;
        private String name;
        private int price;
        private List<Category> categories;
        // omitted
    }
    
  • マッピングファイル

    <resultMap id="itemResultMap" type="Item">
        <id property="code" column="item_code"/>
        <result property="name" column="item_name"/>
        <result property="price" column="item_price"/>
        <collection property="categories" column="item_code"
            fetchType="lazy" select="findByItemCode" />
    </resultMap>
    
  • アプリケーションコード(Service)

        Item item = itemRepository.findByItemCode(itemCode);
        // (1)
        String code = item.getCode();
        String name = item.getName();
        String price = item.getPrice();
        // omitted
    }
    

    項番

    説明

    上記例では、”Lazy Load”対象のプロパティであるcategoriesプロパティにアクセスしていないが、Item#codeプロパティにアクセスした際に、”Lazy Load”が実行される。

    false」(デフォルト)の場合、上記のケースでは”Lazy Load”は実行されない。