1. MyBatisの概要
1.1 MyBatisとは
MyBatisは、データベースアクセスを簡素化する永続化層フレームワークです。カスタムSQL、ストアドプロシージャ、高度なマッピングをサポートし、JDBCコードやパラメータ設定、結果セット取得の手間を削減します。XMLまたはアノテーションを用いて、Javaオブジェクトとデータベースレコードのマッピングを設定できます。
元々はApacheのiBatisプロジェクトでしたが、2010年にGoogle Codeへ移行し、MyBatisと改名されました。2013年11月にGitHubへ移行しています。
入手方法
- Mavenリポジトリ:
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.13</version>
</dependency>
- GitHub: Search · Mybatis · GitHub
- 公式ドキュメント: mybatis – MyBatis 3 | 概要
1.2 永続化層
永続化とは、プログラムのデータを永続状態と一時状態の間で変換するプロセスです。メモリ上のデータは電源断で消失するため、データベースやファイルシステムに保存する必要があります。DAO層、Service層、Controller層のうち、データベースとのやり取りを担当するのが永続化層です。
1.3 MyBatisの利点
- 学習曲線が緩やかで導入が容易
- SQLとJavaコードの分離により保守性が向上
- ORMフィールドマッピングのためのマッピングタグを提供
- オブジェクト関係マッピングタグによる関連管理
- 動的SQL構築のためのXMLタグをサポート
- 広く利用されており、コミュニティが活発
2. 最初のMyBatisプログラム
2.1 環境構築
データベースを作成し、Mavenプロジェクトに以下の依存関係を追加します。
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.27</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.13</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>
2.2 モジュール作成
MyBatis設定ファイル (mybatis-config.xml)
<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/mybatis?useSSL=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
</environments>
</configuration>
ユーティリティクラス (MybatisUtils.java)
public class MybatisUtils {
private static SqlSessionFactory sqlSessionFactory;
static {
try {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
} catch (IOException e) {
e.printStackTrace();
}
}
public static SqlSession getSqlSession() {
return sqlSessionFactory.openSession();
}
}
2.3 コード記述
エンティティクラス (User.java)
package com.example.pojo;
public class User {
private int id;
private String name;
private String pwd;
// コンストラクタ、getter/setter、toString() は省略
}
DAOインターフェース (UserDao.java)
package com.example.dao;
import com.example.pojo.User;
import java.util.List;
public interface UserDao {
List<User> getUserList();
}
Mapper XML (UserMapper.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.dao.UserDao">
<select id="getUserList" resultType="com.example.pojo.User">
SELECT * FROM mybatis.user
</select>
</mapper>
2.4 テスト
注意: 設定ファイルでMapperを登録する必要があります。
<mappers>
<mapper resource="com/example/dao/UserMapper.xml"/>
</mappers>
また、Mavenのリソースフィルタリング問題を解決するため、pom.xmlに以下を追加します。
<build>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
<filtering>true</filtering>
</resource>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
<filtering>true</filtering>
</resource>
</resources>
</build>
JUnitテスト
public class UserDaoTest {
@Test
public void test() {
SqlSession sqlSession = MybatisUtils.getSqlSession();
UserDao userDao = sqlSession.getMapper(UserDao.class);
List<User> userList = userDao.getUserList();
for (User user : userList) {
System.out.println(user);
}
sqlSession.close();
}
}
よくある問題:
- Mapperが登録されていない (BindingException)
- インターフェースのバインディング誤り
- メソッド名の不一致
- 戻り値の型の不一致
- Mavenのリソースエクスポート問題
3. CRUD操作
3.1 namespace
Mapper XMLのnamespaceは、対応するDAO/Mapperインターフェースの完全修飾名と一致させる必要があります。
3.2 SELECT
// インターフェース
User getUserById(int id);
// Mapper XML
<select id="getUserById" parameterType="int" resultType="com.example.pojo.User">
SELECT * FROM mybatis.user WHERE id = #{id}
</select>
3.3 INSERT
<insert id="addUser" parameterType="com.example.pojo.User">
INSERT INTO mybatis.user (id, name, pwd)
VALUES (#{id}, #{name}, #{pwd})
</insert>
3.4 UPDATE
<update id="updateUser" parameterType="com.example.pojo.User">
UPDATE mybatis.user
SET name = #{name}, pwd = #{pwd}
WHERE id = #{id}
</update>
3.5 DELETE
<delete id="deleteUser" parameterType="int">
DELETE FROM mybatis.user WHERE id = #{id}
</delete>
注意: 挿入、更新、削除の操作後はトランザクションのコミットが必要です。
sqlSession.commit();
3.6 エラー分析
- タグの不一致
- resourceのパス誤り
- 設定ファイルの不備
- NullPointerException(リソース未登録)
- XMLファイルの文字化け
- Mavenのリソースパッケージング問題
3.7 汎用Mapの使用
フィールドが多い場合やパラメータが動的な場合、Mapを使用すると便利です。
// インターフェース
int addUser2(Map<String, Object> map);
// Mapper XML
<insert id="addUser2" parameterType="map">
INSERT INTO mybatis.user (id, name, pwd)
VALUES (#{userid}, #{userName}, #{passWord})
</insert>
// テスト
@Test
public void addUser2() {
SqlSession sqlSession = MybatisUtils.getSqlSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
Map<String, Object> map = new HashMap<>();
map.put("userid", 5);
map.put("userName", "Hello");
map.put("passWord", "12345516");
mapper.addUser2(map);
sqlSession.commit();
sqlSession.close();
}
3.8 あいまい検索
2つの方法があります。
方法1: Javaコードでワイルドカードを渡す
List<User> userList = mapper.getUserLike("%李%");
方法2: SQLで連結
<select id="getUserLike" resultType="com.example.pojo.User">
SELECT * FROM mybatis.user WHERE name LIKE #{value}
</select>
4. 設定ファイルの解析
4.1 コア設定ファイル (mybatis-config.xml)
設定ファイルの構成要素:
- properties (プロパティ)
- settings (設定)
- typeAliases (型エイリアス)
- typeHandlers (型ハンドラ)
- objectFactory (オブジェクトファクトリ)
- plugins (プラグイン)
- environments (環境設定)
- mappers (マッパー)
4.2 環境設定 (environments)
複数の環境を設定できますが、SqlSessionFactoryインスタンスは1つの環境のみ選択できます。
4.3 プロパティ (properties)
外部設定ファイル(例:db.properties)を参照できます。
# db.properties
driver=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/mybatis?useSSL=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
username=root
password=123456
<properties resource="db.properties">
<property name="username" value="root"/>
<property name="password" value="123456"/>
</properties>
同名のプロパティがある場合、外部ファイルが優先されます。
4.4 型エイリアス (typeAliases)
完全修飾クラス名の冗長さを軽減します。
<typeAliases>
<typeAlias type="com.example.pojo.User" alias="User"/>
</typeAliases>
パッケージスキャンによる設定も可能です。
<typeAliases>
<package name="com.example.pojo"/>
</typeAliases>
エンティティクラスに@Aliasアノテーションを付与することもできます。
@Alias("user")
public class User {}
4.5 設定 (settings)
MyBatisの動作を変更する重要な設定です。例:キャッシュ、遅延ロード、ログ実装など。
4.6 マッパー (mappers)
マッパーXMLの登録方法は3つあります。
方法1: リソースパス
<mappers>
<mapper resource="com/example/dao/UserMapper.xml"/>
</mappers>
方法2: クラス
<mappers>
<mapper class="com.example.dao.UserMapper"/>
</mappers>
方法3: パッケージスキャン
<mappers>
<package name="com.example.dao"/>
</mappers>
方法2と3では、インターフェースとXMLファイルの名前とパッケージが一致している必要があります。
4.7 ライフサイクル
- SqlSessionFactoryBuilder: SqlSessionFactory作成後は不要。ローカル変数として使用。
- SqlSessionFactory: アプリケーション実行中は常に存在。データベース接続プールと考えるとよい。シングルトンパターンが推奨。
- SqlSession: リクエストまたはメソッドスコープ。スレッドセーフではないため、使用後は速やかにクローズする必要がある。
5. プロパティ名とフィールド名の不一致
5.1 問題
データベースのカラム名とエンティティのプロパティ名が異なる場合(例:pwd と password)、クエリ結果が正しくマッピングされません。
5.2 resultMap
resultMapを使用して明示的にマッピングを定義します。
<resultMap id="UserMap" type="User">
<result column="pwd" property="password"/>
</resultMap>
<select id="getUserById" resultMap="UserMap">
SELECT * FROM mybatis.user WHERE id = #{id}
</select>
6. ログ
6.1 ログファクトリ
MyBatisは以下のログ実装をサポートしています。
- SLF4J
- LOG4J (3.5.9以降は非推奨)
- LOG4J2
- JDK_LOGGING
- COMMONS_LOGGING
- STDOUT_LOGGING (標準出力)
- NO_LOGGING
6.2 LOG4Jの設定
依存関係の追加:
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
log4j.properties:
log4j.rootLogger=DEBUG,console,file
log4j.appender.console=org.apache.log4j.ConsoleAppender
log4j.appender.console.Target=System.out
log4j.appender.console.Threshold=DEBUG
log4j.appender.console.layout=org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern=【%c】-%m%n
log4j.appender.file=org.apache.log4j.RollingFileAppender
log4j.appender.file.File=./log/example.log
log4j.appender.file.MaxFileSize=10mb
log4j.appender.file.Threshold=DEBUG
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=【%p】【%d{yy-MM-dd}】【%c】%m%n
log4j.logger.org.mybatis=DEBUG
log4j.logger.java.sql=DEBUG
log4j.logger.java.sql.Statement=DEBUG
log4j.logger.java.sql.ResultSet=DEBUG
log4j.logger.java.sql.PreparedStatement=DEBUG
MyBatis設定:
<settings>
<setting name="logImpl" value="LOG4J"/>
</settings>
使用例:
import org.apache.log4j.Logger;
public class UserDaoTest {
static Logger logger = Logger.getLogger(UserDaoTest.class);
@Test
public void testLog() {
logger.info("info: テスト開始");
logger.debug("debug: デバッグ情報");
logger.error("error: エラー情報");
}
}
7. ページネーション
7.1 LIMITを使用したページネーション
// インターフェース
List<User> getUserLimit(Map<String, Integer> map);
// Mapper XML
<select id="getUserLimit" parameterType="map" resultMap="UserMap">
SELECT * FROM mybatis.user LIMIT #{startIndex}, #{pageSize}
</select>
// テスト
@Test
public void testLimit() {
SqlSession sqlSession = MybatisUtils.getSqlSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
HashMap<String, Integer> map = new HashMap<>();
map.put("startIndex", 0);
map.put("pageSize", 2);
List<User> userList = mapper.getUserLimit(map);
for (User user : userList) {
System.out.println(user);
}
sqlSession.close();
}
7.2 RowBoundsを使用したページネーション
// インターフェース
List<User> getUserRowBounds();
// Mapper XML
<select id="getUserRowBounds" resultMap="UserMap">
SELECT * FROM mybatis.user
</select>
// テスト
@Test
public void testRowBounds() {
SqlSession sqlSession = MybatisUtils.getSqlSession();
RowBounds rowBounds = new RowBounds(1, 2);
List<User> userList = sqlSession.selectList(
"com.example.dao.UserMapper.getUserRowBounds", null, rowBounds
);
for (User user : userList) {
System.out.println(user);
}
sqlSession.close();
}
8. アノテーションを使用した開発
8.1 アノテーションベースのCRUD
public interface UserMapper {
@Select("SELECT * FROM user")
List<User> getUsers();
@Select("SELECT * FROM user WHERE id = #{id}")
User getUserById(@Param("id") int id);
@Insert("INSERT INTO user(id, name, pwd) VALUES(#{id}, #{name}, #{password})")
int addUser(User user);
@Update("UPDATE user SET name=#{name}, pwd=#{password} WHERE id=#{id}")
int updateUser(User user);
@Delete("DELETE FROM user WHERE id=#{uid}")
int deleteUser(@Param("uid") int id);
}
@Paramアノテーション:
- 基本型やString型のパラメータには@Paramを付与
- 参照型(エンティティなど)は不要
- 複数パラメータがある場合は推奨
9. Lombok
Lombokを使用すると、ゲッター、セッター、コンストラクタなどを自動生成できます。
依存関係:
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
</dependency>
エンティティクラス:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private int id;
private String name;
private String password;
}
@Dataは、@Getter、@Setter、@ToString、@EqualsAndHashCode、@RequiredArgsConstructorをまとめて適用します。
10. 多対一の関連
例:複数の学生が一人の教師に所属する場合。
10.1 サブクエリによる実装
// StudentMapperインターフェース
public interface StudentMapper {
List<Student> getStudents();
}
// Mapper XML
<select id="getStudents" resultMap="StudentTeacher">
SELECT * FROM mybatis.student
</select>
<resultMap id="StudentTeacher" type="Student">
<result property="id" column="id"/>
<result property="name" column="name"/>
<association property="teacher" javaType="Teacher"
select="getTeacherById" column="tid"/>
</resultMap>
<select id="getTeacherById" resultType="Teacher">
SELECT * FROM mybatis.teacher WHERE id = #{id}
</select>
10.2 結合クエリによる実装
<select id="getStudents" resultMap="StudentTeacher2">
SELECT s.id sid, s.name sname, t.name tname
FROM student s, teacher t
WHERE s.tid = t.id
</select>
<resultMap id="StudentTeacher2" type="Student">
<result property="id" column="sid"/>
<result property="name" column="sname"/>
<association property="teacher" javaType="Teacher">
<result property="name" column="tname"/>
</association>
</resultMap>
11. 一対多の関連
例:一人の教師が複数の学生を持つ場合。
11.1 結合クエリによる実装
// TeacherMapperインターフェース
public interface TeacherMapper {
Teacher getTeacher(@Param("tid") int id);
}
// Mapper XML
<select id="getTeacher" resultMap="TeacherStudent">
SELECT s.id sid, s.name sname, t.name tname, t.id tid
FROM student s, teacher t
WHERE s.tid = t.id AND t.id = #{tid}
</select>
<resultMap id="TeacherStudent" type="Teacher">
<result property="id" column="tid"/>
<result property="name" column="tname"/>
<collection property="students" ofType="Student">
<result property="id" column="sid"/>
<result property="name" column="sname"/>
<result property="tid" column="tid"/>
</collection>
</resultMap>
11.2 サブクエリによる実装
<select id="getTeacher" resultMap="TeacherStudent2">
SELECT * FROM mybatis.teacher WHERE id = #{tid}
</select>
<resultMap id="TeacherStudent2" type="Teacher">
<collection property="students" javaType="ArrayList"
ofType="Student" select="getStudentsByTeacherId"
column="id"/>
</resultMap>
<select id="getStudentsByTeacherId" resultType="Student">
SELECT * FROM mybatis.student WHERE tid = #{tid}
</select>
ポイント:
- association: 多対一の関連(オブジェクト)
- collection: 一対多の関連(コレクション)
- javaType: プロパティの型
- ofType: コレクションのジェネリクス型
12. 動的SQL
12.1 if
<select id="queryBlogIf" parameterType="map" resultType="blog">
SELECT * FROM mybatis.blog WHERE 1=1
<if test="title != null">
AND title = #{title}
</if>
<if test="author != null">
AND author = #{author}
</if>
</select>
12.2 choose (when, otherwise)
<select id="queryBlogChoose" parameterType="map" resultType="blog">
SELECT * FROM mybatis.blog
<where>
<choose>
<when test="title != null">title = #{title}</when>
<when test="author != null">author = #{author}</when>
<otherwise>views = #{views}</otherwise>
</choose>
</where>
</select>
12.3 trim, where, set
<update id="updateBlog" parameterType="map">
UPDATE mybatis.blog
<set>
<if test="title != null">title = #{title},</if>
<if test="author != null">author = #{author}</if>
</set>
WHERE id = #{id}
</update>
12.4 SQLフラグメント
<sql id="if-title-author">
<if test="title != null">title = #{title}</if>
<if test="author != null">AND author = #{author}</if>
</sql>
<select id="queryBlogIf" parameterType="map" resultType="blog">
SELECT * FROM mybatis.blog
<where>
<include refid="if-title-author"/>
</where>
</select>
12.5 foreach
<select id="queryBlogForeach" parameterType="map" resultType="blog">
SELECT * FROM mybatis.blog
<where>
<foreach collection="ids" item="id" open="AND (" close=")" separator="OR">
id = #{id}
</foreach>
</where>
</select>
13. キャッシュ
13.1 概要
キャッシュはメモリに一時的にデータを保存し、頻繁にアクセスされるクエリのデータベースアクセスを減らしてパフォーマンスを向上させます。
13.2 MyBatisキャッシュのタイプ
- 1次キャッシュ: SqlSessionレベル。デフォルトで有効。
- 2次キャッシュ: namespaceレベル。手動で有効化する必要がある。
13.3 1次キャッシュ
同一SqlSession内で同じクエリを実行すると、2回目はキャッシュから結果を取得します。キャッシュが無効になる条件:
- 異なるクエリの実行
- 挿入、更新、削除の実行
- 異なるMapper XMLの使用
- 手動キャッシュクリア(sqlSession.clearCache())
13.4 2次キャッシュ
有効化手順:
- グローバル設定で有効化
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
- Mapper XMLで設定
<cache />
<!-- 詳細設定 -->
<cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>
- エンティティクラスをSerializableに実装
public class User implements Serializable {
private int id;
private String name;
private String pwd;
}
13.5 カスタムキャッシュ (EhCache)
依存関係:
<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-ehcache</artifactId>
<version>1.2.3</version>
</dependency>
ehcache.xml:
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"
updateCheck="false">
<diskStore path="./tmpdir/Tmp_EhCache"/>
<defaultCache eternal="false" maxElementsInMemory="10000"
overflowToDisk="false" diskPersistent="false"
timeToIdleSeconds="1800" timeToLiveSeconds="259200"
memoryStoreEvictionPolicy="LRU"/>
</ehcache>
Mapper XMLでEhCacheを使用:
<cache type="org.mybatis.caches.ehcache.EhcacheCache"/>