6.2. データベースアクセス(MyBatis3編)¶
6.2.1. Overview¶
本節では、MyBatis3を使用してデータベースにアクセスする方法について説明する。
![]()
Picture - Scope of description¶
6.2.1.1. MyBatis3について¶
6.2.1.1.1. MyBatis3のコンポーネント構成について¶
項番
コンポーネント/設定ファイル
説明
MyBatis設定ファイル
MyBatis3の動作設定を記載するXMLファイル。
データベースの接続先、マッピングファイルのパス、MyBatisの動作設定などを記載するファイルである。 Springと連携して使用する場合は、データベースの接続先やマッピングファイルのパスの設定を本設定ファイルに指定する必要がないため、 MyBatis3のデフォルトの動作を変更又は拡張する際に、設定を行う事になる。
org.apache.ibatis.session.SqlSessionFactoryBuilderMyBatis設定ファイルを読込み、
SqlSessionFactoryを生成するためのコンポーネント。Springと連携して使用する場合は、アプリケーションのクラスから本コンポーネントを直接扱うことはない。
org.apache.ibatis.session.SqlSessionFactory
SqlSessionを生成するためのコンポーネント。Springと連携して使用する場合は、アプリケーションのクラスから本コンポーネントを直接扱うことはない。
org.apache.ibatis.session.SqlSessionSQLの発行やトランザクション制御のAPIを提供するコンポーネント。
MyBatis3を使ってデータベースにアクセスする際に、もっとも重要な役割を果たすコンポーネントである。
Springと連携して使用する場合は、アプリケーションのクラスから本コンポーネントを直接扱うことは、基本的にはない。
Mapperインタフェース
マッピングファイルに定義したSQLをタイプセーフに呼び出すためのインタフェース。
Mapperインターフェースに対する実装クラスは、MyBatis3が自動で生成するため、開発者はインターフェースのみ作成すればよい。
マッピングファイル
SQLとO/Rマッピングの設定を記載するXMLファイル。
アプリケーションの起動時に行う処理。下記(1)~(3)の処理が、これに該当する。
クライアントからのリクエスト毎に行う処理。下記(4)~(10)の処理が、これに該当する。
Picture - Relationship of MyBatis3 components¶
項番
説明
アプリケーションは、
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)の処理が、これに該当する。
Picture - Relationship of MyBatis-Spring components¶
アプリケーションの起動時に行う処理は、以下の流れで実行される。
項番
説明
SqlSessionFactoryBeanは、SqlSessionFactoryBuilderに対してSqlSessionFactoryの構築を依頼する。
SqlSessionFactoryBuilderは、SqlSessionFactoryを生成するためにMyBatis設定ファイルを読込む。
SqlSessionFactoryBuilderは、MyBatis設定ファイルの定義に基づきSqlSessionFactoryを生成する。生成された
SqlSessionFactoryは、SpringのDIコンテナによって管理される。
MapperFactoryBeanは、スレッドセーフなSqlSession(SqlSessionTemplate)と、スレッドセーフなMapperオブジェクト(MapperインタフェースのProxyオブジェクト)を生成する。生成されたMapperオブジェクトは、SpringのDIコンテナによって管理され、ServiceクラスなどにDIされる。Mapperオブジェクトは、スレッドセーフなSqlSession(SqlSessionTemplate)を利用することで、スレッドセーフな実装を提供している。
クライアントからのリクエスト毎に行う処理は、以下の流れで実行される。
項番
説明
クライアントは、アプリケーションに対して処理を依頼する。
アプリケーション(Service)は、 DIコンテナによって注入されたMapperオブジェクト(Mapperインターフェースを実装したProxyオブジェクト)のメソッドを呼び出す。
Mapperインタフェースの仕組みについては、 「Mapperインタフェースの仕組みについて」を参照されたい。
Mapperオブジェクトは、呼び出されたメソッドに対応する
SqlSession(SqlSessionTemplate)のメソッドを呼び出す。
SqlSession(SqlSessionTemplate)は、Proxy化されたスレッドセーフなSqlSessionのメソッドを呼び出す。
Proxy化されたスレッドセーフな
SqlSessionは、トランザクションに割り当てられているMyBatis3標準のSqlSessionを使用する。トランザクションに割り当てられている
SqlSessionが存在しない場合は、MyBatis3標準のSqlSessionを取得するために、SqlSessionFactoryのメソッドを呼び出す。
SqlSessionFactoryは、MyBatis3標準のSqlSessionを返却する。返却されたMyBatis3標準の
SqlSessionはトランザクションに割り当てられるため、同一トランザクション内であれば、新たに生成されることはなく、同じSqlSessionが使用される仕組みになっている。
MyBatis3標準の
SqlSessionは、マッピングファイルから実行するSQLを取得し、SQLを実行する。Tip
トランザクション制御について
上記フローには記載していないが、トランザクションのコミット及びロールバックは、Springのトランザクション管理機能が行う。
Springのトランザクション管理機能を使用したトランザクション管理方法については、「トランザクション管理について」を参照されたい。
6.2.2. How to use¶
ここからは、実際にMyBatis3を使用して、データベースにアクセスするための設定及び実装方法について、説明する。
以降の説明は、大きく以下に分類する事ができる。
項番
分類
説明
アプリケーション全体の設定
MyBatis3をアプリケーションで使用するための設定方法や、MyBatis3の動作を変更するための設定方法について記載している。
ここに記載している内容は、プロジェクト立ち上げ時にアプリケーションアーキテクトが設定を行う時に必要となる。そのため、基本的にはアプリケーション開発者が個々に意識する必要はない部分である。以下のセクションが、この分類に該当する。
MyBatis3用のブランクプロジェクト からプロジェクトを生成した場合は、上記で説明している設定の多くが既に設定済みの状態となっているため、アプリケーションアーキテクトは、プロジェクト特性を判断し、必要に応じて設定の追加及び変更を行うことになる。
データアクセス処理の実装方法
MyBatis3を使った基本的なデータアクセス処理の実装方法について記載している。
ここに記載している内容は、アプリケーション開発者が実装時に必要となる。
以下のセクションが、この分類に該当する。
6.2.2.1. pom.xmlの設定¶
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/xxx/yyy/zzz/config/app/ProjectNameEnvConfig.java@Bean("transactionManager") public TransactionManager transactionManager( @Qualifier("dataSource") DataSource dataSource) { DataSourceTransactionManager bean = new DataSourceTransactionManager(); // (1) bean.setDataSource(dataSource); // (2) bean.setRollbackOnCommitFailure(true); // (3) return bean; }
項番
説明
PlatformTransactionManagerとして、org.springframework.jdbc.datasource.DataSourceTransactionManagerを指定する。dataSourceプロパティに、設定済みのデータソースのbeanを指定する。トランザクション内でSQLを実行する際は、ここで指定したデータソースからコネクションが取得される。
コミット時にエラーが発生した場合にロールバック処理が呼び出される様にする。
この設定を追加することで、「未確定状態の操作を持つコネクションがコネクションプールに戻ることで発生する意図しないコミット(コネクション再利用時のコミット、コネクションクローズ時の暗黙コミットなど)」が発生するリスクを下げることができる。ただし、ロールバック処理時にエラーが発生する可能性もあるため、意図しないコミットが発生するリスクがなくなるわけではない点に留意されたい。
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属性に同じ値を設定する必要がある。
6.2.2.2.3. MyBatis-Springの設定¶
MyBatis3とSpringを連携する場合、MyBatis-Springのコンポーネントを使用して、
MyBatis3とSpringを連携するために必要となる処理がカスタマイズされた
SqlSessionFactoryの生成スレッドセーフなMapperオブジェクト(MapperインタフェースのProxyオブジェクト)の生成
を行う必要がある。
MyBatis3用のブランクプロジェクト からプロジェクトを生成した場合は、MyBatis3とSpringを連携するための設定は、設定済みの状態である。
設定例は以下の通り。
projectName-domain/src/main/xxx/yyy/zzz/config/app/ProjectNameInfraConfig.java@Configuration @MapperScan(basePackages = "com.example.domain.repository", sqlSessionFactoryRef = "sqlSessionFactory") // (4) @Import({ ProjectNameEnvConfig.class }) public class ProjectNameInfraConfig { // omitted @Bean("sqlSessionFactory") public SqlSessionFactoryBean sqlSessionFactory( @Qualifier("dataSource") DataSource dataSource) throws IOException { SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); // (1) bean.setDataSource(dataSource); // (2) bean.setConfiguration(MybatisConfig.configuration()); // (3) return bean; }
項番
説明
SqlSessionFactoryを生成するためのコンポーネントとして、SqlSessionFactoryBeanをbean定義する。dataSourceプロパティに、設定済みのデータソースのbeanを指定する。MyBatis3の処理の中でSQLを発行する際は、ここで指定したデータソースからコネクションが取得される。
configurationプロパティに、MyBatisのConfigrationを指定する。上記設定例では後述するMyBatis設定ファイルから作成されるorg.apache.ibatis.session.Configurationを指定している。Mapperインタフェースをスキャンするために
@MapperScanアノテーションを定義し、base-package属性には、Mapperインタフェースが格納されている基底パッケージを指定する。指定されたパッケージ配下に格納されている Mapperインタフェースがスキャンされ、スレッドセーフなMapperオブジェクト(MapperインタフェースのProxyオブジェクト)が自動的に生成される。
【指定するパッケージは、各プロジェクトで決められたパッケージにすること】
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オブジェクト)が自動的に生成される。
【指定するパッケージは、各プロジェクトで決められたパッケージにすること】
6.2.2.3. 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/xxx/yyy/zzz/config/app/mybatis/MybatisConfig.javapublic static Configuration configuration() throws IOException { Configuration configuration = new Configuration(); // omitted setSettings(configuration); return configuration; } private static void setSettings(Configuration configuration) { // omitted configuration.setDefaultFetchSize(100); // (1) }
項番
説明
defaultFetchSizeに、1回の通信で取得するデータの件数を指定する。
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. 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/xxx/yyy/zzz/config/app/mybatis/MybatisConfig.javapublic static Configuration configuration() throws IOException { Configuration configuration = new Configuration(); // omitted setSettings(configuration); return configuration; } private static void setSettings(Configuration configuration) { // omitted configuration.setJdbcTypeForNull("NULL"); // (1) }
項番
説明
jdbcTypeForNullに、JDBC型を指定する。
上記例では、
null値のJDBC型としてNULL型を指定している。
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.4. TypeAliasの設定¶
TypeAliasを使用すると、マッピングファイルで指定するJavaクラスに対して、エイリアス名(短縮名)を割り当てる事ができる。
TypeAliasを使用しない場合、マッピングファイルで指定するtype属性、parameterType属性、resultType属性などには、Javaクラスの完全修飾クラス名(FQCN)を指定する必要があるため、マッピングファイルの記述効率の低下、記述ミスの増加などが懸念される。
本ガイドラインでは、記述効率の向上、記述ミスの削減、マッピングファイルの可読性向上などを目的として、TypeAliasを使用することを推奨する。
${projectPackage}.domain.model)配下に格納されるクラスがTypeAliasの対象となっている。6.2.2.3.4.1. パッケージ名を指定してTypeAliasを設定する¶
パッケージ名を指定することで、指定したパッケージ配下に格納されているクラスは、パッケージの部分が除去された部分がエイリアス名となる。
TypeAliasの設定方法は以下の通り。
projectName-domain/src/main/xxx/yyy/zzz/config/app/mybatis/MybatisConfig.javapublic static Configuration configuration() throws IOException { Configuration configuration = new Configuration(); // omitted setTypeAliases(configuration.getTypeAliasRegistry()); return configuration; } private static void setTypeAliases(TypeAliasRegistry typeAliasRegistry) { typeAliasRegistry.registerAliases("com.example.domain.model"); // (1) }
項番
説明
TypeAliasRegistryのregisterAliasesに、エイリアスを設定するクラスが格納されているパッケージ名を指定する。指定したパッケージ配下に格納されているクラスは、パッケージの部分が除去された部分がエイリアス名となる。上記例だと、com.example.domain.model.Accountクラスのエイリアス名は、Accountとなる。【指定するパッケージは、各プロジェクトで決められたパッケージにすること】
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となる。【指定するパッケージは、各プロジェクトで決められたパッケージにすること】
6.2.2.3.4.2. クラスを指定してTypeAliasを設定する¶
TypeAliasの設定は、クラス単位で設定する事もできる。
projectName-domain/src/main/xxx/yyy/zzz/config/app/mybatis/MybatisConfig.javapublic static Configuration configuration() throws IOException { Configuration configuration = new Configuration(); // omitted setTypeAliases(configuration.getTypeAliasRegistry()); return configuration; } private static void setTypeAliases(TypeAliasRegistry typeAliasRegistry) { typeAliasRegistry.registerAlias(AccountSearchCriteria.class); // (1) }
項番
説明
TypeAliasRegistryのregisterAliasに、エイリアスを設定するクラスの完全修飾クラス名(FQCN)を指定する。上記例だと、
com.example.domain.repository.account.AccountSearchCriteriaクラスのエイリアス名は、AccountSearchCriteria(パッケージの部分が除去された部分)となる。エイリアス名に任意の値を指定したい場合は、第一引数に任意のエイリアス名を指定することができる。
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.2.3.4.3. デフォルトで付与されるエイリアス名の上書き¶
package要素を使用してエイリアスを設定した場合や、typeAlias要素のalias属性を省略してエイリアスを設定した場合は、TypeAliasのエイリアス名は、完全修飾クラス名(FQCN)からパッケージの部分が除去された部分となる。
デフォルトで付与されるエイリアス名ではなく、任意のエイリアス名にしたい場合は、TypeAliasを設定したいクラスに@org.apache.ibatis.type.Aliasアノテーションを指定する事で、
任意のエイリアス名を指定する事ができる。
エイリアス設定対象のJavaクラス
package com.example.domain.model.book; @Alias("BookAuthor") // (1) public class Author { // omitted }
package com.example.domain.model.article; @Alias("ArticleAuthor") // (1) public class Author { // omitted }
項番
説明
@Aliasアノテーションのvalue属性に、エイリアス名を指定する。上記例だと、
com.example.domain.model.book.Authorクラスのエイリアス名は、BookAuthorとなる。異なるパッケージの中に同じクラス名のクラスが格納されている場合は、この方法を使用することで、それぞれ異なるエイリアス名を設定する事ができる。ただし、本ガイドラインでは、クラス名は重複しないように設計する事を推奨する。上記例であれば、クラス名自体をBookAuthorとArticleAuthorにすることを検討して頂きたい。Tip
TypeAliasの エイリアス名は、
typeAlias要素のalias属性の指定値@Aliasアノテーションのvalue属性の指定値デフォルトで付与されるエイリアス名(完全修飾クラス名からパッケージの部分が除去された部分)
の優先順で適用される。
TypeAliasを使用した際の、マッピングファイルの記述例は以下の通り。
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.example.domain.repository.account.AccountRepository"> <resultMap id="accountResultMap" type="Account"> <!-- omitted --> </resultMap> <select id="findByUsername" parameterType="string" resultMap="accountResultMap"> <!-- omitted --> </select> <select id="findByCriteria" parameterType="AccountSearchCriteria" resultMap="accountResultMap"> <!-- omitted --> </select> </mapper>Tip
MyBatis3標準のエイリアス名について
プリミティブ型やプリミティブラッパ型などの一般的なJavaクラスについては、予めエイリアス名が設定されている。
予め設定されるエイリアス名については、「MyBatis 3 REFERENCE DOCUMENTATION(Configuration XML-typeAliases-) 」を参照されたい。
6.2.2.3.5. TypeHandlerの設定¶
TypeHandlerは、JavaクラスとJDBC型をマッピングする時に使用される。
具体的には以下を実施時に使用される。
SQLを発行する際に、Javaクラスのオブジェクトを
java.sql.PreparedStatementのバインドパラメータとして設定するSQLの発行結果として取得した
java.sql.ResultSetから値を取得する
TypeHandlerが提供されており、特別な設定を行う必要はない。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でサポートしていないオブジェクトとJDBC型をマッピングする場合である。TypeHandlerの作成例については、「TypeHandlerの実装」を参照されたい。ここでは、作成したTypeHandlerをMyBatisに適用する方法について説明を行う。
6.2.2.3.5.1. パッケージ名を指定してTypeHandlerを設定する¶
パッケージ名を指定することで、指定したパッケージ配下に格納されているTypeHandlerを抽出できる。
projectName-domain/src/main/xxx/yyy/zzz/config/app/mybatis/MybatisConfig.javapublic static Configuration configuration() throws IOException { Configuration configuration = new Configuration(); // omitted setTypeHandlers(configuration.getTypeHandlerRegistry()); return configuration; } private static void setTypeHandlers( TypeHandlerRegistry typeHandlerRegistry) { typeHandlerRegistry.register( "com.example.infra.mybatis.typehandler"); // (1) }
項番
説明
MyBatis設定ファイルに
TypeHandlerの設定を行う。TypeAliasRegistryのtypeHandlerRegistryに、作成した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によって自動検出される。
6.2.2.3.5.2. クラスを指定してTypeHandlerを設定する¶
クラスを指定することで、クラス単位にTypeHandlerを設定することが出来る。
projectName-domain/src/main/xxx/yyy/zzz/config/app/mybatis/MybatisConfig.javapublic static Configuration configuration() throws IOException { Configuration configuration = new Configuration(); // omitted setTypeHandlers(configuration.getTypeHandlerRegistry()); return configuration; } private static void setTypeHandlers( TypeHandlerRegistry typeHandlerRegistry) { typeHandlerRegistry.register(CustomTypeHandler.class); }
DIコンテナが管理しているbeanを使用したい場合は、MyBatis設定ファイルではなくbean定義ファイル内でTypeHandlerを指定すればよい。
projectName-domain/src/main/xxx/yyy/zzz/config/app/ProjectNameInfraConfig.java@Bean("sqlSessionFactory") public SqlSessionFactoryBean sqlSessionFactory( DataSource dataSource) throws IOException { SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); bean.setDataSource(dataSource); bean.setConfiguration(MybatisConfig.configuration()); bean.setTypeHandlers(customTypeHandler()); return bean; } @Bean public CustomTypeHandler customTypeHandler(){ new CustomTypeHandler(); }
projectName-domain/src/main/resources/META-INF/mybatis/mybatis-config.xml<typeHandlers> <typeHandler handler="xxx.yyy.zzz.CustomTypeHandler" /> </typeHandlers>
DIコンテナが管理しているbeanを使用したい場合は、MyBatis設定ファイルではなく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-)」を参照されたい。
6.2.2.3.5.3. フィールド毎にTypeHandlerを設定する¶
アプリケーション全体に適用するのではなく、フィールド毎に個別のTypeHandlerを指定する事も可能である。これは、アプリケーション全体に適用しているTypeHandlerを上書きする際に使用する。
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.example.domain.repository.dam3.CustomTypeHandlerRepository"> <resultMap id="SampleObjResultMap" type="com.example.domain.model.SampleObj"> <id property="id" column="id"/> <!-- (1) --> <result property="handlerObj" column="handler_obj" typeHandler="CustomTypeHandler"/> </resultMap> <select id="findAllHandlerObj" resultMap="SampleObjResultMap"> SELECT id, handler_obj FROM t_custom_type_handler </select> <select id="findOneById" parameterType="string" resultMap="SampleObjResultMap"> SELECT id, handler_obj FROM t_custom_type_handler WHERE id = #{id} </select> <insert id="insert" parameterType="com.example.domain.model.SampleObj"> INSERT INTO t_custom_type_handler (id, handler_obj) <!-- (2) --> VALUES (#{id}, #{handlerObj, typeHandler=CustomTypeHandler}) </insert> <update id="update" parameterType="com.example.domain.model.SampleObj"> UPDATE t_custom_type_handler <!-- (2) --> SET handler_obj = #{handlerObj, typeHandler=CustomTypeHandler} WHERE id = #{id} </update> <delete id="delete" parameterType="string"> DELETE FROM t_custom_type_handler WHERE id = #{id} </delete> </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/xxx/yyy/zzz/config/app/mybatis/MybatisConfig.javapublic static Configuration configuration() throws IOException { Configuration configuration = new Configuration(); // omitted setSettings(configuration); return configuration; } private static void setSettings(Configuration configuration) { // omitted configuration.setMapUnderscoreToCamelCase(true); // (3) }
項番
説明
mapUnderscoreToCamelCaseをtrueにする設定を追加する。設定をtrueにすると、アンダースコア区切りのカラム名がキャメルケース形式に自動変換される。具体例としては、カラム名がtodo_idの場合、todoIdに変換されてマッピングが行われる。
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のプロパティをマッピングする際に力を発揮するマッピング方法である。また、自動マッピングに比べて効率的にマッピングを行う事ができる。
処理の効率性を優先するアプリケーションの場合は、自動マッピングの代わりに手動マッピングを使用した方がよい。
実践的なマッピングの実装例については、
を参照されたい。
projectName-domain/src/main/resources/com/example/domain/repository/todo/TodoRepository.xml<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.example.domain.repository.todo.TodoRepository"> <!-- (1) --> <resultMap id="todoResultMap" type="Todo"> <!-- (2) --> <id column="todo_id" property="todoId" /> <!-- (3) --> <result column="todo_title" property="todoTitle" /> <result column="finished" property="finished" /> <result column="created_at" property="createdAt" /> <result column="version" property="version" /> </resultMap> <!-- (4) --> <select id="findByTodoId" parameterType="string" resultMap="todoResultMap"> SELECT todo_id, todo_title, finished, created_at, version FROM t_todo WHERE todo_id = #{todoId} </select> </mapper>
項番
説明
<resultMap>要素に、検索結果(ResultSet)とJavaBeanのマッピング定義を行う。id属性にマッピングを識別するためのIDを、type属性にマッピングするJavaBeanのクラス名(又はエイリアス名)を指定する。<resultMap>要素の詳細は、「MyBatis 3 REFERENCE DOCUMENTATION(Mapper XML Files-resultMap-) 」を参照されたい。検索結果(
ResultSet)のID(PK)のカラムとJavaBeanのプロパティのマッピングを行う。ID(PK)のマッピングは、
<id>要素を使って指定する。column属性には検索結果(ResultSet)のカラム名、property属性にはJavaBeanのプロパティ名を指定する。<id>要素の詳細は、「MyBatis 3 REFERENCE DOCUMENTATION(Mapper XML Files-id & result-) 」を参照されたい。検索結果(
ResultSet)のID(PK)以外のカラムとJavaBeanのプロパティのマッピングを行う。ID(PK)以外のマッピングは、
<result>要素を使って指定する。column属性には検索結果(ResultSet)のカラム名、property属性にはJavaBeanのプロパティ名を指定する。<result>要素の詳細は、「MyBatis 3 REFERENCE DOCUMENTATION(Mapper XML Files-id & result-) 」を参照されたい。<select>要素のresultMap属性に、適用するマッピング定義のIDを指定する。Note
id要素とresult要素の使い分けについて
<id>要素と<result>要素は、どちらも検索結果(ResultSet)のカラムとJavaBeanのプロパティをマッピングするための要素であるが、ID(PK)カラムに対してマッピングは、<id>要素を使うことを推奨する。理由は、ID(PK)カラムに対して
<id>要素を使用してマッピングを行うと、MyBatis3が提供しているオブジェクトのキャッシュ制御の処理や、関連オブジェクトへのマッピングの処理のパフォーマンスを、全体的に向上させることが出来るためである。
6.2.2.6. Entityの検索処理¶
Entityの検索処理の実装方法について、目的別に説明を行う。
Entityの検索処理の実装方法に対する説明を読む前に、「検索結果とJavaBeanのマッピング方法」を一読して頂きたい。
以降の説明では、アンダースコア区切りのカラム名をキャメルケース形式のプロパティ名に自動でマッピングする設定を有効にした場合の実装例となる。
projectName-domain/src/main/xxx/yyy/zzz/config/app/mybatis/MybatisConfig.javapublic static Configuration configuration() throws IOException { Configuration configuration = new Configuration(); // omitted setSettings(configuration); return configuration; } private static void setSettings(Configuration configuration) { // omitted configuration.setMapUnderscoreToCamelCase(true); }
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.xx.yy.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)を作成し返却する。
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コレクションや配列に対して繰り返し処理を行うための要素
bindOGNL式の結果を変数に格納するための要素。
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/xxx/yyy/zzz/config/app/ProjectNameCodelistConfig.java@Bean("CL_DIRECTION") public SimpleMapCodeList simpleMapCodeList() { Map<String, String> map = new LinkedHashMap<>(); map.put("1", "ASC"); map.put("2", "DESC"); SimpleMapCodeList bean = new SimpleMapCodeList(); bean.setMap(map); return bean; }
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の標準でサポートされていないオブジェクトとのマッピングが必要な場合、独自のTypeHandlerの作成が必要となる。
本ガイドラインでは「独自TypeHandlerの実装」を例に、TypeHandlerの実装方法について説明する。
作成したTypeHandlerをアプリケーションに適用する方法については、「TypeHandlerの設定」を参照されたい。
6.2.3.2.1. 独自TypeHandlerの実装¶
public class HandlerObj implements Serializable {
private static final long serialVersionUID = 1L;
private String value1;
private String value2;
private Integer value3;
// omitted
}
public class SampleObj implements Serializable {
private static final long serialVersionUID = 1L;
private String id;
private HandlerObj handlerObj;
// omitted
}
com.example.domain.model.HandlerObjとVARCHARをマッピングするためのTypeHandlerの実装例を、以下に示す。value1、value2、value3を文字列”value1/value2/value3“に変換しVARCHARに格納している。また逆に、VARCHARに格納された文字列を分解しvalue1~value3にマッピングしている。package com.example.infra.mybatis.typehandler; import java.sql.CallableStatement; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import org.apache.ibatis.type.BaseTypeHandler; import org.apache.ibatis.type.JdbcType; import org.springframework.util.StringUtils; import com.example.domain.model.HandlerObj; // (1) public class CustomTypeHandler extends BaseTypeHandler<HandlerObj> { // (2) @Override public HandlerObj getNullableResult(ResultSet rs, String columnName) throws SQLException { return getObj(rs.getString(columnName)); } // (2) @Override public HandlerObj getNullableResult(ResultSet rs, int columnIndex) throws SQLException { return getObj(rs.getString(columnIndex)); } // (2) @Override public HandlerObj getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { return getObj(cs.getString(columnIndex)); } // (3) @Override public void setNonNullParameter(PreparedStatement ps, int i, HandlerObj parameter, JdbcType jdbcType) throws SQLException { ps.setString(i, objToString(parameter)); } private HandlerObj getObj(String value) { // (4) if (StringUtils.hasText(value)) { String[] parts = value.split("/"); if (parts.length == 3) { return new HandlerObj(parts[0], parts[1], Integer.valueOf(parts[2])); } } return null; } private String objToString(HandlerObj handlerObj) { return handlerObj.getValue1() + "/" + handlerObj.getValue2() + "/" + handlerObj.getValue3(); } }
項番
説明
MyBatis3から提供されている
BaseTypeHandlerを親クラスに指定する。その際、
BaseTypeHandlerのジェネリック型にはマッピング対象となるクラス(ここではHandlerObj)を指定する。
ResultSet又はCallableStatementから取得したVARCHARをHandlerObjに変換し、返り値として返却する。
HandlerObjを文字列に変換し、PreparedStatementに設定する処理を実装する。
nullやを許可するカラムの場合、valueがnullになる可能性があるため、nullチェックを行ってから変換する必要がある。
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/xxx/yyy/zzz/config/app/mybatis/MybatisConfig.javapublic static Configuration configuration() throws IOException { Configuration configuration = new Configuration(); // omitted setSettings(configuration); return configuration; } private static void setSettings(Configuration configuration) { // omitted configuration.setDefaultExecutorType(ExecutorType.REUSE); // (1) }
項番
説明
defaultExecutorTypeをREUSEに変更する。上記設定を行うと、デフォルト動作がPreparedStatement再利用モードになる。
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/xxx/yyy/zzz/config/app/ProjectNameInfraConfig.java@Configuration @MapperScan(basePackages = "com.example.domain.repository", sqlSessionFactoryRef = "sqlSessionFactory") // (2) public class ProjectNameInfraConfig { // (1) @Bean("sqlSessionFactory") public SqlSessionFactoryBean sqlSessionFactory( @Qualifier("dataSource") DataSource dataSource, @Qualifier("databaseIdProvider") VendorDatabaseIdProvider databaseIdProvider) throws IOException { SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); bean.setDataSource(dataSource); bean.setDatabaseIdProvider(databaseIdProvider); bean.setConfiguration(MybatisConfig.configuration()); return bean; } // (3) @Bean("batchSqlSessionTemplate") public SqlSessionTemplate batchSqlSessionTemplate( @Qualifier("sqlSessionFactory") SqlSessionFactory sqlSessionFactory) { return new SqlSessionTemplate(sqlSessionFactory, ExecutorType.BATCH); } // (4) @Bean("todoBatchRepository") public MapperFactoryBean<TodoBatchRepository> todoBatchRepository( @Qualifier("batchSqlSessionTemplate") SqlSessionTemplate batchSqlSessionTemplate) { MapperFactoryBean<TodoBatchRepository> bean = new MapperFactoryBean<TodoBatchRepository>(); // (5) bean.setMapperInterface(TodoBatchRepository.class); // (6) bean.setSqlSessionTemplate(batchSqlSessionTemplate); return bean; }
項番
説明
通常使用するRepositoryで利用するための
SqlSessionTemplateをBean定義する。通常使用するRepositoryをスキャンしBean登録する。
template-ref属性に、(1)で定義したSqlSessionTemplateを指定する。バッチモードのRepositoryで利用するための
SqlSessionTemplateをBean定義する。バッチモード用のRepositoryをBean定義する。
id属性には、(2)でスキャンしたRepositoryのBean名と重複しない値を指定する。(2)でスキャンされたRepositoryのBean名は、インタフェース名を「lowerCamelCase」にした値にとなる。上記例では、バッチモード用の
TodoRepositoryがtodoBatchRepositoryという名前のBeanでBean登録される。mapperInterfaceプロパティには、 バッチモードを利用するRepositoryのインタフェース名(FQCN)を指定する。sqlSessionTemplateプロパティには、 (3)で定義したバッチモード用のSqlSessionTemplateを指定する。
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: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/xxx/yyy/zzz/config/app/ProjectNameInfraConfig.java@Configuration @MapperScan(basePackages = "com.example.domain.repository", sqlSessionTemplateRef = "batchSqlSessionTemplate", nameGenerator = BatchRepositoryBeanNameGenerator.class) // (3) public class ProjectNameInfraConfig { @Bean("sqlSessionFactory") public SqlSessionFactoryBean sqlSessionFactory( @Qualifier("dataSource") DataSource dataSource, @Qualifier("databaseIdProvider") VendorDatabaseIdProvider databaseIdProvider) throws IOException { SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); bean.setDataSource(dataSource); bean.setDatabaseIdProvider(databaseIdProvider); bean.setConfiguration(MybatisConfig.configuration()); return bean; } @Bean("batchSqlSessionTemplate") public SqlSessionTemplate batchSqlSessionTemplate( @Qualifier("sqlSessionFactory") SqlSessionFactory sqlSessionFactory) { return new SqlSessionTemplate(sqlSessionFactory, ExecutorType.BATCH); } // omitted
項番
属性
説明
-
@MapperScanアノテーションを使用して、バッチモードのRepositoryをBean登録する。basePackages
Repositoryをスキャンするベースパッケージを指定する。
指定パッケージの配下に存在するRepositoryインタフェースがスキャンされ、SpringのApplicationContextにBean登録される。
sqlSessionTemplateRef
バッチモード用の
SqlSessionTemplateのBeanを指定する。nameGenerator
スキャンしたRepositoryのBean名を生成するためのクラスを指定する。
具体的には、(1)で作成したクラスのクラス名(FQCN)を指定する。
この指定を省略した場合、Bean名が重複するため、バッチモードのRepositoryはSpringのApplicationContextに登録されない。
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: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.xx.yy.5001" ,todoId)); } return todo; } }
以下に、Mapperインタフェースのメソッドを呼び出した際に、SQLが実行されるまでの処理フローについて説明を行う。
![]()
Picture - Mapper mechanism¶
項番
説明
アプリケーションは、Mapperインタフェースに定義されているメソッドを呼び出す。
Mapperインタフェースの実装クラス(MapperインタフェースのProxyオブジェクト)は、アプリケーション起動時にMyBatis3のコンポーネントによって生成される。
MapperインタフェースのProxyオブジェクトは、
MapperProxyのinvokeメソッドを呼び出す。
MapperProxyは、Mapperインタフェースのメソッド呼び出しをハンドリングする役割をもつ。
MapperProxyは、呼び出されたMapperインタフェースのメソッドに対応するMapperMethodを生成し、executeメソッドを呼び出す。
MapperMethodは、 呼び出されたMapperインタフェースのメソッドに対応するSqlSessionのメソッドを呼び出す役割をもつ。
MapperMethodは、SqlSessionのメソッドを呼び出す。
SqlSessionのメソッドを呼び出す際は、実行するSQLステートメントを特定するためのキー(以降、「ステートメントID」と呼ぶ)を引き渡している。
SqlSessionは、指定されたステートメントIDをキーに、マッピングファイルよりSQLステートメントを取得する。
SqlSessionは、マッピングファイルより取得したSQLステートメントに指定されているバインド変数に値を設定し、SQLを実行する。
Mapperインタフェース(
SqlSession)は、SQLの実行結果をJavaBeanなどに変換して、アプリケーションに返却する。件数のカウントや、更新件数などを取得する場合は、プリミティブ型やプリミティブラッパ型などが返却値となるケースもある。
Tip
ステートメントIDとは
ステートメントIDは、実行するSQLステートメントを特定するためのキーであり、「MapperインタフェースのFQCN + “.” + 呼び出されたMapperインタフェースのメソッド名」 というルールで生成される。
MapperMethodによって生成されたステートメントIDに対応するSQLステートメントをマッピングファイルに定義するためには、マッピングファイルのネームスペースに「MapperインタフェースのFQCN」、各種ステートメントタグのid属性に「Mapperインタフェースのメソッド名」を指定する必要がある。
6.2.4.2. データベースによるSQL切り替えについて¶
MyBatis3では、JDBCドライバから接続しているデータベースのベンダー情報を取得して、使用するSQLを切り替える仕組み(org.apache.ibatis.mapping.VendorDatabaseIdProvider)を提供している。
この仕組みは、動作環境として複数のデータベースをサポートするようなアプリケーションを構築する際に有効である。
Note
本ガイドラインでは、環境依存するコンポーネントや設定ファイルについては、[projectName]-envというサブプロジェクトで管理し、ビルド時に実行環境にあったコンポーネントや設定ファイル作成を選択するスタイルを推奨している。
[projectName]-envは、
開発環境(ローカルのPC環境)
各種試験環境
商用環境
上記それぞれの差分を吸収するためのサブプロジェクトであり、複数のデータベースをサポートするアプリケーションの開発でも利用する事ができる。
基本的には、環境依存するコンポーネントや設定ファイルは、[projectName]-envというサブプロジェクトで管理する事を推奨するが、SQLのちょっとした違いを吸収したい場合は、本仕組みを使用してもよい。
アーキテクトは、データベースの違いによるSQLの環境依存をどのように実装するかの指針を明確に示すことで、アプリケーション全体として統一された実装となるように心がけてほしい。
projectName-domain/src/main/xxx/yyy/zzz/config/app/ProjectNameInfraConfig.java@Configuration @MapperScan(basePackages = "com.example.domain.repository", sqlSessionFactoryRef = "sqlSessionFactory") @Import({ ProjectNameEnvConfig.class }) public class ProjectNameInfraConfig { // (1) @Bean("databaseIdProvider") public VendorDatabaseIdProvider databaseIdProvider() { VendorDatabaseIdProvider bean = new VendorDatabaseIdProvider(); Properties properties = new Properties(); properties.setProperty("Oracle", "oracle"); // (2) properties.setProperty("PostgreSQL", "postgres"); // (2) properties.setProperty("H2", "h2"); // (2) bean.setProperties(properties); return bean; } @Bean("sqlSessionFactory") public SqlSessionFactoryBean sqlSessionFactory( @Qualifier("dataSource") DataSource dataSource) throws IOException { SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); bean.setDataSource(dataSource); bean.setDatabaseIdProvider(databaseIdProvider()); // (3) bean.setConfiguration(MybatisConfig.configuration()); return bean; }
項番
説明
MyBatis3から提供されている
VendorDatabaseIdProviderをBean定義する。VendorDatabaseIdProviderは、JDBCドライバから取得したデータベースのプロダクト名(java.sql.DatabaseMetaData#getDatabaseProductName())をデータベースIDとして扱うためのクラスである。propertiesプロパティには、JDBCドライバから取得したデータベースのプロダクト名とデータベースIDのマッピングを指定する。マッピング仕様については、「MyBatis3 REFERENCE DOCUMENTATION(Configuration-databaseIdProvider-)」を参照されたい。
データベースIDを使用する
SqlSessionFactoryBeanのdatabaseIdProviderプロパティ対して、(1)で定義したDatabaseIdProviderを指定する。この指定を行うと、マッピングファイルからデータベースIDを参照する事が可能となる。
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="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.3. 関連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.4. 関連EntityをネストしたSQLを使用して取得する方法について¶
MyBatis3では、マッピング時に別のSQL(ネストしたSQL)を使用して関連Entityを取得する方法を提供している。
ネストしたSQLを使用して関連Entityを取得する仕組みを使用すると、
個々のSQL定義
resultMap要素のマッピング定義
をシンプルにする事ができる。
Warning
各種定義がシンプルになる一方で、ネストしたSQLを多用すると、N+1問題を引き起こす要因になるという事を意識する必要がある。
ネストしたSQLを使用する場合のMyBatisのデフォルトの動作は、”Eager Load”となる。
これは、関連Entityの使用有無に関係なくSQLが発行される事を意味しており、
無駄なSQLの実行とデータの取得
N+1問題
などが発生する危険性が高まる。
Tip
MyBatis3では、ネストしたSQLを使用して関連Entityを取得する際の動作を、”Lazy Load”に変更するためのオプションを提供している。
“Lazy Load”の使用方法については、「関連EntityをLazy Loadするための設定」を参照されたい。
6.2.4.4.1. 関連EntityをネストしたSQLを使用して取得する実装例¶
ネストしたSQLを使用して関連Entityを取得する際の実装例を以下に示す。
<resultMap id="itemResultMap" type="Item"> <id property="code" column="item_code"/> <result property="name" column="item_name"/> <result property="price" column="item_price"/> <!-- (1) --> <collection property="categories" column="item_code" select="findAllCategoryByItemCode" /> </resultMap> <select id="findAllCategoryByItemCode" parameterType="string" resultType="Category"> SELECT ct.code, ct.name FROM m_item_category ic INNER JOIN m_category ct ON ct.code = ic.category_code WHERE ic.item_code = #{itemCode} ORDER BY code </select>
項番
説明
association要素又はcollection要素のselect属性に、呼び出すSQLのステートメントIDを指定する。column属性には、SQLに渡すパラメータ値が格納されているカラム名を指定する。上記例では、findAllCategoryByItemCodeのパラメータとしてitem_codeカラムの値を渡している。指定可能な属性の詳細は、「MyBatis3 REFERENCE DOCUMENTATION(Mapper XML Files-Nested Select for Association-)」を参照されたい。
Note
上記例では、
fetchType属性を指定していないため、”Lazy Load”と”Eager Load”のどちらで実行されるかは、アプリケーション全体の設定に依存する。アプリケーション全体の設定については、「Lazy Loadを使用するためのMyBatisの設定」を参照されたい。
6.2.4.4.2. 関連EntityをLazy Loadするための設定¶
ネストしたSQLを使用して関連Entityを取得する際のMyBatis3のデフォルト動作は、”Eager Load”であるが、”Lazy Load”を使用する事も可能である。
以下に、”Lazy Load”を使用するために最低限必要な設定及び使用方法について説明を行う。
説明していない設定値については、「MyBatis3 REFERENCE DOCUMENTATION(Mapper XML Files-settings-)」を参照されたい。
6.2.4.4.2.1. バイトコード操作ライブラリの追加¶
“Lazy Load”を使用する場合は、”Lazy Load”を実現するためのProxyオブジェクトを生成するために、
JAVASSIST
CGLIB
のいずれか一方のライブラリが必要となる。
Note
MyBatis 3.3.0以降のバージョンでCGLIBを使用する場合は、
pom.xmlにCGLIBのアーティファクトを追加MyBatis設定ファイル(
projectName-domain/src/main/resources/META-INF/mybatis/mybatis-config.xml)に「proxyFactory=CGLIB」を追加
すればよい。
CGLIBのアーティファクト情報については、「MyBatis3 PROJECT DOCUMENTATION(Project Dependencies-compile-)」を参照されたい。
6.2.4.4.2.2. Lazy Loadを使用するためのMyBatisの設定¶
MyBatis3では、”Lazy Load”の使用有無を、
アプリケーションの全体設定(MyBatis設定ファイル)
個別設定(マッピングファイル)
の2箇所で指定する事ができる。
アプリケーションの全体設定は、 MyBatis設定ファイル(
projectName-domain/src/main/resources/META-INF/mybatis/mybatis-config.xml)に指定する。<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <settings> <!-- (1) --> <setting name="lazyLoadingEnabled" value="true"/> </settings> </configuration>
項番
説明
アプリケーションのデフォルト動作を
lazyLoadingEnabledに指定する。true: “Lazy Load”false: “Eager Load” (デフォルト)
association要素とcollection要素のfetchType属性を指定した場合は、fetchType属性の指定値が優先される。Warning
「
false: “Eager Load”」の状態でassociation要素又はcollection要素のselect属性を使用すると、マッピング時にSQLが実行されるので、注意が必要である。特に理由がない場合は、
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.4.2.3. Lazy Loadの実行タイミングを制御するための設定¶
MyBatis3では、”Lazy Load”を実行するタイミングを制御するためのオプション(aggressiveLazyLoading)を提供している[1]。
このオプションのデフォルト値はMybatis 3.4.2以降からfalseであり、”Lazy Load”対象となっているプロパティのgetterメソッドが呼び出されたタイミングで実行する。
Warning
aggressiveLazyLoadingが「true」の場合、”Lazy Load”対象となっているプロパティを保持するオブジェクトのgetterメソッドが呼び出されたタイミングで”Lazy Load”が実行される。
このため、実際にはデータの取得が必要ないにもかかわらずSQLが実行されてしまう可能性があることに注意が必要である。
具体的には、以下のようなマッピングを行い、”Lazy Load”対象になっていないプロパティだけにアクセスするケースである。「true」の場合、”Lazy Load”対象のプロパティに対して直接アクセスしなくても、”Lazy Load”が実行されてしまう。
特に理由がない場合は、aggressiveLazyLoadingは「false」(デフォルト)のまま変更しないことを推奨する。
Entity
public class Item implements Serializable { private static final long serialVersionUID = 1L; private String code; private String name; private int price; private List<Category> categories; // omitted }
マッピングファイル
<resultMap id="itemResultMap" type="Item"> <id property="code" column="item_code"/> <result property="name" column="item_name"/> <result property="price" column="item_price"/> <collection property="categories" column="item_code" fetchType="lazy" select="findByItemCode" /> </resultMap>
アプリケーションコード(Service)
Item item = itemRepository.findByItemCode(itemCode); // (1) String code = item.getCode(); String name = item.getName(); String price = item.getPrice(); // omitted }
項番
説明
上記例では、”Lazy Load”対象のプロパティである
categoriesプロパティにアクセスしていないが、Item#codeプロパティにアクセスした際に、”Lazy Load”が実行される。「
false」(デフォルト)の場合、上記のケースでは”Lazy Load”は実行されない。













