6.2. データベースアクセス(MyBatis3編)¶
目次
6.2.1. Overview¶
本節では、MyBatis3を使用してデータベースにアクセスする方法について説明する。
6.2.1.1. MyBatis3について¶
6.2.1.1.1. MyBatis3のコンポーネント構成について¶
項番 コンポーネント/設定ファイル 説明
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ファイル。
アプリケーションの起動時に行う処理。下記(1)~(3)の処理が、これに該当する。
クライアントからのリクエスト毎に行う処理。下記(4)~(10)の処理が、これに該当する。
項番 説明
アプリケーションは、 SqlSessionFactoryBuilder
に対してSqlSessionFactory
の構築を依頼する。
SqlSessionFactoryBuilder
は、SqlSessionFactory
を生成するためにMyBatis設定ファイルを読込む。
SqlSessionFactoryBuilder
は、MyBatis設定ファイルの定義に基づきSqlSessionFactory
を生成する。
項番 説明
クライアントは、アプリケーションに対して処理を依頼する。
アプリケーションは、 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の連携について¶
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のコンポーネント構成について¶
項番 コンポーネント/設定ファイル 説明
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)の処理が、これに該当する。
アプリケーションの起動時に行う処理は、以下の流れで実行される。
項番 説明
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の設定¶
pom.xml
にterasoluna-gfw-mybatis3-dependenciesへの依存関係を追加する。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. トランザクション管理の設定¶
PlatformTransactionManager
を使用する必要がある。ローカルトランザクションを使用する場合は、JDBCのAPIを呼び出してトランザクション制御を行うDataSourceTransactionManager
を使用する。
MyBatis3用のブランクプロジェクト からプロジェクトを生成した場合は、DataSourceTransactionManager
が設定済みの状態である。
設定例は以下の通り。
projectName-env/src/main/resources/META-INF/spring/projectName-env.xml
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:jee="http://www.springframework.org/schema/jee" xmlns:jdbc="http://www.springframework.org/schema/jdbc" xsi:schemaLocation="http://www.springframework.org/schema/jdbc https://www.springframework.org/schema/jdbc/spring-jdbc.xsd http://www.springframework.org/schema/jee https://www.springframework.org/schema/jee/spring-jee.xsd http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd"> <!-- omitted --> <!-- (1) --> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <!-- (2) --> <property name="dataSource" ref="dataSource" /> <!-- (3) --> <property name="rollbackOnCommitFailure" value="true" /> </bean> <!-- omitted --> </beans>
項番 説明 PlatformTransactionManager
として、org.springframework.jdbc.datasource.DataSourceTransactionManager
を指定する。dataSource
プロパティに、設定済みのデータソースのbeanを指定する。トランザクション内でSQLを実行する際は、ここで指定したデータソースからコネクションが取得される。
コミット時にエラーが発生した場合にロールバック処理が呼び出される様にする。
この設定を追加することで、「未確定状態の操作を持つコネクションがコネクションプールに戻ることで発生する意図しないコミット(コネクション再利用時のコミット、コネクションクローズ時の暗黙コミットなど)」が発生するリスクを下げることができる。ただし、ロールバック処理時にエラーが発生する可能性もあるため、意図しないコミットが発生するリスクがなくなるわけではない点に留意されたい。
Note
PlatformTransactionManagerのbean IDについて
id属性には、
transactionManager
を指定することを推奨する。transactionManager
以外の値を指定すると、<tx:annotation-driven>
タグのtransaction-manager属性に同じ値を設定する必要がある。
アプリケーションサーバから提供されているトランザクションマネージャを使用する場合は、JTAのAPIを呼び出してトランザクション制御を行うorg.springframework.transaction.jta.JtaTransactionManager
を使用する。
設定例は以下の通り。
projectName-env/src/main/resources/META-INF/spring/projectName-env.xml
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:jee="http://www.springframework.org/schema/jee" xmlns:jdbc="http://www.springframework.org/schema/jdbc" xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation="http://www.springframework.org/schema/jdbc https://www.springframework.org/schema/jdbc/spring-jdbc.xsd http://www.springframework.org/schema/jee https://www.springframework.org/schema/jee/spring-jee.xsd http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/tx https://www.springframework.org/schema/tx/spring-tx.xsd"> <!-- omitted --> <!-- (1) --> <tx:jta-transaction-manager /> <!-- omitted --> </beans>
項番 説明 <tx:jta-transaction-manager />
を指定すると、アプリケーションサーバに対して最適なJtaTransactionManager
がbean定義される。
6.2.2.2.3. MyBatis-Springの設定¶
MyBatis3とSpringを連携する場合、MyBatis-Springのコンポーネントを使用して、
- MyBatis3とSpringを連携するために必要となる処理がカスタマイズされた
SqlSessionFactory
の生成 - スレッドセーフなMapperオブジェクト(MapperインタフェースのProxyオブジェクト)の生成
を行う必要がある。
MyBatis3用のブランクプロジェクト からプロジェクトを生成した場合は、MyBatis3とSpringを連携するための設定は、設定済みの状態である。
設定例は以下の通り。
projectName-domain/src/main/resources/META-INF/spring/projectName-infra.xml
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mybatis="http://mybatis.org/schema/mybatis-spring" xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd http://mybatis.org/schema/mybatis-spring http://mybatis.org/schema/mybatis-spring.xsd"> <import resource="classpath:/META-INF/spring/projectName-env.xml" /> <!-- (1) --> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <!-- (2) --> <property name="dataSource" ref="dataSource" /> <!-- (3) --> <property name="configLocation" value="classpath:/META-INF/mybatis/mybatis-config.xml" /> </bean> <!-- (4) --> <mybatis:scan base-package="com.example.domain.repository" /> </beans>
項番 説明 SqlSessionFactory
を生成するためのコンポーネントとして、SqlSessionFactoryBean
をbean定義する。dataSource
プロパティに、設定済みのデータソースのbeanを指定する。MyBatis3の処理の中でSQLを発行する際は、ここで指定したデータソースからコネクションが取得される。
configLocation
プロパティに、MyBatis設定ファイルのパスを指定する。ここで指定したファイルが
SqlSessionFactory
を生成する時に読み込まれる。Mapperインタフェースをスキャンするために
<mybatis:scan>
を定義し、base-package
属性には、Mapperインタフェースが格納されている基底パッケージを指定する。指定されたパッケージ配下に格納されている Mapperインタフェースがスキャンされ、スレッドセーフなMapperオブジェクト(MapperインタフェースのProxyオブジェクト)が自動的に生成される。
【指定するパッケージは、各プロジェクトで決められたパッケージにすること】
Note
MyBatis3の設定方法について
SqlSessionFactoryBean
を使用する場合、MyBatis3の設定は、MyBatis設定ファイルではなくbeanのプロパティに直接指定することもできるが、本ガイドラインでは、MyBatis3自体の設定はMyBatis標準の設定ファイルに指定する方法を推奨する。
6.2.2.3. MyBatis3の設定¶
Note
MyBatis設定ファイルの格納場所について
本ガイドラインでは、MyBatis設定ファイルは、
projectName-domain/src/main/resources/META-INF/mybatis/mybatis-config.xml
に格納することを推奨している。MyBatis3用のブランクプロジェクト からプロジェクトを生成した場合は、上記ファイルは格納済みの状態である。
6.2.2.3.1. fetchSize
の設定¶
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/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="defaultFetchSize" value="100" /> </settings> </configuration>
項番 説明 defaultFetchSize
に、1回の通信で取得するデータの件数を指定する。
Note
「クエリ単位のfetchSize」の指定方法
fetchSize
をクエリ単位に指定する必要がある場合は、検索用のSQLを記述するためのXML要素(<select>
要素)のfetchSize
属性に値を指定すればよい。
なお、大量のデータを返すようなクエリを記述する場合は、「ResultHandlerの実装」の利用も検討すること。
6.2.2.3.2. SQL実行モードの設定¶
MyBatis3では、SQLを実行するモードとして以下の3種類を用意している。
項番 モード 説明
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. TypeAliasの設定¶
TypeAliasを使用すると、マッピングファイルで指定するJavaクラスに対して、エイリアス名(短縮名)を割り当てる事ができる。
TypeAliasを使用しない場合、マッピングファイルで指定するtype
属性、parameterType
属性、resultType
属性などには、Javaクラスの完全修飾クラス名(FQCN)を指定する必要があるため、マッピングファイルの記述効率の低下、記述ミスの増加などが懸念される。
本ガイドラインでは、記述効率の向上、記述ミスの削減、マッピングファイルの可読性向上などを目的として、TypeAliasを使用することを推奨する。
${projectPackage}.domain.model
)配下に格納されるクラスがTypeAliasの対象となっている。TypeAliasの設定方法は以下の通り。
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> <typeAliases> <!-- (1) --> <package name="com.example.domain.model" /> </typeAliases> </configuration>
項番 説明 package
要素のname
属性に、エイリアスを設定するクラスが格納されているパッケージ名を指定する。指定したパッケージ配下に格納されているクラスは、パッケージの部分が除去された部分が、エイリアス名となる。上記例だと、com.example.domain.model.Account
クラスのエイリアス名は、Account
となる。【指定するパッケージは、各プロジェクトで決められたパッケージにすること】
Tip
クラス単位にType Aliasを設定する方法について
Type Aliasの設定には、クラス単位に設定する方法やエイリアス名を明示的に指定する方法が用意されている。
詳細は、Appendixの「TypeAliasの設定」を参照されたい。
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.4. NULL値とJDBC型のマッピング設定¶
null
値の設定と認識できるJDBC型を指定する事で、解決する事ができる。null
値を設定した際に、以下の様なスタックトレースを伴うエラーが発生した場合は、null
値とJDBC型のマッピングが必要となる。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/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="jdbcTypeForNull" value="NULL" /> </settings> </configuration>
項番 説明 jdbcTypeForNullに、JDBC型を指定する。
上記例では、
null
値のJDBC型としてNULL
型を指定している。Tip
項目単位で解決する方法について
別の解決方法として、
null
値が設定される可能性があるプロパティのインラインパラメータに、Java型に対応する適切なJDBC型を個別に指定する方法もある。ただし、インラインパラメータで個別に指定した場合、マッピングファイルの記述量及び指定ミスが発生する可能性が増えることが予想されるため、本ガイドラインとしては、全体の設定でエラーを解決することを推奨している。
全体の設定を変更してもエラーが解決しない場合は、エラーが発生するプロパティについてのみ、個別に設定を行えばよい。
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に適用する方法について説明を行う。
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> <typeHandlers> <!-- (1) --> <package name="com.example.infra.mybatis.typehandler" /> </typeHandlers> </configuration>
項番 説明 MyBatis設定ファイルに
TypeHandler
の設定を行う。package
要素のname属性に、作成したTypeHandler
が格納されているパッケージ名を指定する。指定したパッケージ配下に格納されているTypeHandler
が、MyBatisによって自動検出される。Tip
上記例では、指定したパッケージ配下に格納されている
TypeHandler
をMyBatisによって自動検出させているが、クラス単位に設定する事もできる。クラス単位に
TypeHandler
を設定する場合は、typeHandler
要素を使用する。projectName-domain/src/main/resources/META-INF/mybatis/mybatis-config.xml
<typeHandlers> <typeHandler handler="xxx.yyy.zzz.CustomTypeHandler" /> <package name="com.example.infra.mybatis.typehandler" /> </typeHandlers>
更に、
TypeHandler
の中でDIコンテナが管理しているbeanを使用したい場合は、bean定義ファイル内でTypeHandler
を指定すればよい。projectName-domain/src/main/resources/META-INF/spring/projectName-infra.xml
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:mybatis="http://mybatis.org/schema/mybatis-spring" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/tx https://www.springframework.org/schema/tx/spring-tx.xsd http://mybatis.org/schema/mybatis-spring http://mybatis.org/schema/mybatis-spring.xsd"> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="oracleDataSource" /> <property name="configLocation" value="classpath:/META-INF/mybatis/mybatis-config.xml" /> <property name="typeHandlers"> <list> <bean class="xxx.yyy.zzz.CustomTypeHandler" /> </list> </property> </bean> </beans>
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-)」を参照されたい。
Tip
上記の設定例は、いずれもアプリケーション全体に適用するための設定方法であったが、フィールド毎に個別の
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" /> <!-- (2) --> <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} /* (3) */ ,#{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処理として、以下の処理の実装方法について説明を行う。
-
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のマッピング方法について説明を行う。
ResultSet
)をJavaBean(Entity)にマッピングする方法として、自動マッピング と手動マッピングの2つの方法が用意されている。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/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> <!-- (3) --> <setting name="mapUnderscoreToCamelCase" value="true" /> </settings> </configuration>
項番 説明 mapUnderscoreToCamelCase
をtrue
にする設定を追加する。設定を
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のプロパティをマッピングする際に力を発揮するマッピング方法である。また、自動マッピングに比べて効率的にマッピングを行う事ができる。
処理の効率性を優先するアプリケーションの場合は、自動マッピングの代わりに手動マッピングを使用した方がよい。
実践的なマッピングの実装例については、
- 「MyBatis 3 REFERENCE DOCUMENTATION(Mapper XML Files-Advanced Result Maps-) 」
- 「関連Entityを1回のSQLで取得する方法について」
- 「関連EntityをネストしたSQLを使用して取得する方法について」
を参照されたい。
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/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> <setting name="mapUnderscoreToCamelCase" value="true" /> </settings> </configuration>
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の取得¶
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として、
orderId
とhistoryId
を引数に定義している。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
で受け取る場合は、Map
のkey
にはPKの値Map
のvalue
には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
属性には、Map
のkey
として扱うプロパティ名を指定する。上記例では、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_title
とcreated_at
が指定した条件に一致するTodoレコードを取得する実装している。ソート条件を指定する。
複数件のレコードを取得する場合は、ソート条件を指定する。特に画面に表示するレコードを取得するSQLでは、ソート条件の指定は必須である。Tip
CDATAセクションの活用方法について
SQL内にXMLのエスケープが必要な文字(“
<
”や”>
”など)を指定する場合は、CDATAセクションを使用すると、SQLの可読性を保つことができる。CDATAセクションを使用しない場合は、
<
や>
といったエンティティ参照文字を指定する必要があり、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); }
項番 説明 件数を取得ためのメソッドの返り値は、数値型(
int
やlong
など)を指定する。上記例では、
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
)を作成し返却する。- offsetに”
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>
項番 説明 countByCriteria
とfindPageByCriteria
メソッドの引数に@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()
を呼び出した結果をキーとして扱う方法
<?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を生成する関数を呼び出して、キーを生成している。キー生成の代表的な実装としては、シーケンスオブジェクトから取得した値を文字列にフォーマットする実装があげられる。
Statement#getGeneratedKeys()
を呼び出した結果をキーとして扱う方法について説明する。<?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のバッチ更新機能を使用する方法については、「バッチモードの利用」を参照されたい。
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
を指定した場合は、@Param
のvalue
属性に指定した値を指定する。item リストの中の1要素を保持するローカル変数名を指定する。
foreach
要素内のSQLからは、#{ローカル変数名.プロパティ名}の形式でJavaBeanのプロパティにアクセスする事ができる。separator リスト内の要素間を区切るための文字列を指定する。
上記例では、”
,
”を指定することで、要素毎のVALUE句を”,
”で区切っている。
以下のような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の更新処理¶
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句に複数レコードを更新するための削除条件を指定する。
上記例では、
- 完了済み(
finished
がTRUE
) - 基準日より前に作成された(
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 (?,?)
-- (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/resources/META-INF/spring/projectName-codelist.xml
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:util="http://www.springframework.org/schema/util" xsi:schemaLocation=" http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/util https://www.springframework.org/schema/util/spring-util.xsd "> <bean id="CL_DIRECTION" class="org.terasoluna.gfw.common.codelist.SimpleMapCodeList"> <property name="map"> <util:map> <entry key="1" value="ASC" /> <entry key="2" value="DESC" /> </util:map> </property> </bean> </beans>
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.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の実装¶
org.joda.time.DateTime
、org.joda.time.LocalDateTime
、org.joda.time.LocalDate
など)はサポートされていない。TypeHandler
を用意する必要がある。org.joda.time.DateTime
とjava.sql.Timestamp
をマッピングするためのTypeHandler
の実装例を、以下に示す。
Note
Jada-Timeから提供されている他のクラス(LocalDateTime
、LocalDate
、LocalTime
など)も同じ要領で実装すればよい。
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
を指定する。
DateTime
をTimestamp
に変換し、PreparedStatement
に設定する処理を実装する。
ResultSet
又はCallableStatement
から取得したTimestamp
をDateTime
に変換し、返り値として返却する。
null
を許可するカラムの場合、Timestamp
がnull
になる可能性があるため、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
のインスタンスを生成する。ResultHandler
のhandleResult
メソッドの中に、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利用時の注意点
6.2.3.4.1. PreparedStatement再利用モードの利用¶
実行モードをSIMPLE
からREUSE
に変更した場合、MyBatis内部のPreparedStatement
の扱い方は変わるが、MyBatisの動作(使い方)は変わらない。
実行モードをデフォルト(SIMPLE
)からREUSE
に変更する方法を、以下に示す。
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="defaultExecutorType" value="REUSE"/> </settings> </configuration>
項番 説明 defaultExecutorType
をREUSE
に変更する。上記設定を行うと、デフォルト動作が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/resources/META-INF/spring/projectName-infra.xml
にBean定義を追加する。<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:mybatis="http://mybatis.org/schema/mybatis-spring" xsi:schemaLocation=" http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://mybatis.org/schema/mybatis-spring http://mybatis.org/schema/mybatis-spring.xsd"> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="configLocation" value="classpath:META-INF/mybatis/mybatis-config.xml"/> </bean> <!-- (1) --> <bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate"> <constructor-arg index="0" ref="sqlSessionFactory"/> <constructor-arg index="1" value="REUSE"/> </bean> <mybatis:scan base-package="com.example.domain.repository" template-ref="sqlSessionTemplate"/> <!-- (2) --> <!-- (3) --> <bean id="batchSqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate"> <constructor-arg index="0" ref="sqlSessionFactory"/> <constructor-arg index="1" value="BATCH"/> </bean> <!-- (4) --> <bean id="todoBatchRepository" class="org.mybatis.spring.mapper.MapperFactoryBean"> <!-- (5) --> <property name="mapperInterface" value="com.example.domain.repository.todo.TodoRepository"/> <!-- (6) --> <property name="sqlSessionTemplate" ref="batchSqlSessionTemplate"/> </bean> </beans>
項番 説明 通常使用するRepositoryで利用するための SqlSessionTemplate
をBean定義する。通常使用するRepositoryをスキャンしBean登録する。
template-ref
属性に、(1)で定義したSqlSessionTemplate
を指定する。バッチモードのRepositoryで利用するための SqlSessionTemplate
をBean定義する。バッチモード用のRepositoryをBean定義する。
id
属性には、(2)でスキャンしたRepositoryのBean名と重複しない値を指定する。(2)でスキャンされたRepositoryのBean名は、インタフェース名を「lowerCamelCase」にした値にとなる。上記例では、バッチモード用の
TodoRepository
がtodoBatchRepository
という名前の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/resources/META-INF/spring/projectName-infra.xml
にBean定義を追加する。<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:mybatis="http://mybatis.org/schema/mybatis-spring" xsi:schemaLocation=" http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://mybatis.org/schema/mybatis-spring http://mybatis.org/schema/mybatis-spring.xsd"> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="configLocation" value="classpath:META-INF/mybatis/mybatis-config.xml"/> </bean> <!-- ... --> <bean id="batchSqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate"> <constructor-arg index="0" ref="sqlSessionFactory"/> <constructor-arg index="1" value="BATCH"/> </bean> <!-- (3) --> <mybatis:scan base-package="com.example.domain.repository" template-ref="batchSqlSessionTemplate" name-generator="com.example.domain.repository.BatchRepositoryBeanNameGenerator"/> </beans>
項番 属性 説明 - mybatis:scan
要素を使用して、バッチモードのRepositoryをBean登録する。base-package Repositoryをスキャンするベースパッケージを指定する。
指定パッケージの配下に存在するRepositoryインタフェースがスキャンされ、SpringのApplicationContextにBean登録される。
template-ref バッチモード用の SqlSessionTemplate
のBeanを指定する。name-generator スキャンした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インタフェースのメソッドから返却される更新結果は、
- 返り値が数値(
int
やlong
)の場合は、固定値(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
になるため、必ず更新失敗時の処理が実行されてしまう。
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の処理が終わった後(トランザクションがコミットされる直前)に行われるためである。
@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インタフェースの作成例
本ガイドラインでは、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インタフェースに定義されているメソッドを呼び出す。
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. TypeAliasの設定¶
TypeAliasの設定は、基本的にはpackage
要素を使用してパッケージ単位で設定すればよいが、
- クラス単位でエイリアス名を設定する方法
- デフォルトで付与されるエイリアス名を上書きする方法(任意のエイリアス名を指定する方法)
も用意されている。
6.2.4.2.1. TypeAliasをクラス単位に設定¶
TypeAliasの設定は、クラス単位で設定する事もできる。
projectName-domain/src/main/resources/META-INF/mybatis/mybatis-config.xml
<typeAliases> <!-- (1) --> <typeAlias type="com.example.domain.repository.account.AccountSearchCriteria" /> <package name="com.example.domain.model" /> </typeAliases>
項番 説明 typeAlias
要素のtype
属性に、エイリアスを設定するクラスの完全修飾クラス名(FQCN)を指定する。上記例だと、
com.example.domain.repository.account.AccountSearchCriteria
クラスのエイリアス名は、AccountSearchCriteria
(パッケージの部分が除去された部分)となる。エイリアス名に任意の値を指定したい場合は、
typeAlias
要素のalias
属性に任意のエイリアス名を指定することができる。
6.2.4.2.2. デフォルトで付与されるエイリアス名の上書き¶
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
となる。異なるパッケージの中に同じクラス名のクラスが格納されている場合は、この方法を使用することで、それぞれ異なるエイリアス名を設定する事ができる。ただし、本ガイドラインでは、クラス名は重複しないように設計する事を推奨する。上記例であれば、クラス名自体をBookAuthor
とArticleAuthor
にすることを検討して頂きたい。Tip
TypeAliasの エイリアス名は、
typeAlias
要素のalias
属性の指定値@Alias
アノテーションのvalue
属性の指定値- デフォルトで付与されるエイリアス名(完全修飾クラス名からパッケージの部分が除去された部分)
の優先順で適用される。
6.2.4.3. データベースによるSQL切り替えについて¶
MyBatis3では、JDBCドライバから接続しているデータベースのベンダー情報を取得して、使用するSQLを切り替える仕組み(org.apache.ibatis.mapping.VendorDatabaseIdProvider
)を提供している。
この仕組みは、動作環境として複数のデータベースをサポートするようなアプリケーションを構築する際に有効である。
Note
本ガイドラインでは、環境依存するコンポーネントや設定ファイルについては、[projectName]-envというサブプロジェクトで管理し、ビルド時に実行環境にあったコンポーネントや設定ファイル作成を選択するスタイルを推奨している。
[projectName]-envは、
- 開発環境(ローカルのPC環境)
- 各種試験環境
- 商用環境
上記それぞれの差分を吸収するためのサブプロジェクトであり、複数のデータベースをサポートするアプリケーションの開発でも利用する事ができる。
基本的には、環境依存するコンポーネントや設定ファイルは、[projectName]-envというサブプロジェクトで管理する事を推奨するが、SQLのちょっとした違いを吸収したい場合は、本仕組みを使用してもよい。
アーキテクトは、データベースの違いによるSQLの環境依存をどのように実装するかの指針を明確に示すことで、アプリケーション全体として統一された実装となるように心がけてほしい。
projectName-domain/src/main/resources/META-INF/spring/projectName-infra.xml
にBean定義を追加する。<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mybatis="http://mybatis.org/schema/mybatis-spring" xsi:schemaLocation=" http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd http://mybatis.org/schema/mybatis-spring http://mybatis.org/schema/mybatis-spring.xsd "> <import resource="classpath:/META-INF/spring/projectName-env.xml" /> <!-- (1) --> <bean id="databaseIdProvider" class="org.apache.ibatis.mapping.VendorDatabaseIdProvider"> <!-- (2) --> <property name="properties"> <props> <prop key="H2">h2</prop> <prop key="PostgreSQL">postgresql</prop> </props> </property> </bean> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource" /> <!-- (3) --> <property name="databaseIdProvider" ref="databaseIdProvider"/> <property name="configLocation" value="classpath:/META-INF/mybatis/mybatis-config.xml" /> </bean> <mybatis:scan base-package="com.example.domain.repository" /> </beans>
項番 説明 MyBatis3から提供されている
VendorDatabaseIdProvider
をBean定義する。VendorDatabaseIdProvider
は、JDBCドライバから取得したデータベースのプロダクト名(java.sql.DatabaseMetaData#getDatabaseProductName()
)をデータベースIDとして扱うためのクラスである。properties
プロパティには、JDBCドライバから取得したデータベースのプロダクト名とデータベースIDのマッピングを指定する。マッピング仕様については、「MyBatis3 REFERENCE DOCUMENTATION(Configuration-databaseIdProvider-)」を参照されたい。
データベースIDを使用する
SqlSessionFactoryBean
のdatabaseIdProvider
プロパティ対して、(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.4. 関連Entityを1回のSQLで取得する方法について¶
主Entityと関連Entityを1回のSQLでまとめて取得する方法について説明する。
主Entityと関連Entityをまとめて取得する仕組みを使用すると、ServiceクラスでEntity(JavaBean)の組み立て処理を行う必要がなくなり、Serviceクラスは業務ロジック(ビジネスルール)の実装に集中する事ができる。
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 REFERENCE DOCUMENTATION(Mapper XML Files-Result Maps-) 」を参照されたい。
6.2.4.5. 関連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.5.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.5.2. 関連EntityをLazy Loadするための設定¶
ネストしたSQLを使用して関連Entityを取得する際のMyBatis3のデフォルト動作は、”Eager Load”であるが、”Lazy Load”を使用する事も可能である。
以下に、”Lazy Load”を使用するために最低限必要な設定及び使用方法について説明を行う。
説明していない設定値については、「MyBatis3 REFERENCE DOCUMENTATION(Mapper XML Files-settings-)」を参照されたい。
6.2.4.5.2.1. バイトコード操作ライブラリの追加¶
“Lazy Load”を使用する場合は、”Lazy Load”を実現するためのProxyオブジェクトを生成するために、
- JAVASSIST
- CGLIB
のいずれか一方のライブラリが必要となる。
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.5.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が実行されるので、注意が必要である。特に理由がない場合は、
lazyLoadingEnabled
はtrue
にする事を推奨する。
個別設定は、マッピングファイルの
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.5.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”は実行されない。
[1] | 設定方法は、MyBatisのリファレンスを参照されたい。 |