MyBatis入門ガイド:基本的なCRUD操作から高度な機能まで

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&amp;useUnicode=true&amp;characterEncoding=UTF-8&amp;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次キャッシュ

有効化手順:

  1. グローバル設定で有効化
<settings>
    <setting name="cacheEnabled" value="true"/>
</settings>
  1. Mapper XMLで設定
<cache />

<!-- 詳細設定 -->
<cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>
  1. エンティティクラスを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"/>

タグ: MyBatis JDBC SQL ORM Mapper

5月18日 11:45 投稿