MyBatis 中級: 動的SQLと型ハンドラを使いこなそう

docs

前回の記事ではMyBatisの基本的な使い方をマスターしましたね。今回はMyBatisをより強力にする動的SQLと、データの扱いを柔軟にする型ハンドラについて深掘りしていきましょう。これらを使いこなせば、あなたのMyBatisコードはさらに洗練されたものになるはずです!


動的SQLでSQLを賢く変化させる

動的SQLとは、その名の通り条件によってSQLの内容を動的に変化させる機能です。これにより、複雑な条件分岐を持つ検索クエリなどを柔軟に表現できます。MyBatisでは、<if>, <where>, <choose>, <when>, <otherwise>, <foreach>といった要素を使って動的SQLを記述します。

1. <if>: シンプルな条件分岐

<if>は最も基本的な動的SQL要素で、指定した条件が真の場合にSQLの一部を含めます。

例:ユーザ検索機能

ユーザ名を指定した場合のみ、WHERE句にユーザ名による絞り込みを追加する例です。

XML

<select id="findUsers" resultType="com.example.User">
  SELECT * FROM users
  <if test="username != null and username != ''">
    WHERE username LIKE #{username}
  </if>
</select>

Java

// UserMapper.java
public interface UserMapper {
  List<User> findUsers(@Param("username") String username);
}

Java

// 実行コード
// ユーザ名を指定する場合
List<User> users1 = userMapper.findUsers("Taro");
// ユーザ名を指定しない場合
List<User> users2 = userMapper.findUsers(null);

実行されるSQL (usernameが”Taro”の場合):

SQL

SELECT * FROM users WHERE username LIKE 'Taro'

実行されるSQL (usernameがnullの場合):

SQL

SELECT * FROM users

2. <where>: WHERE句の自動調整

<where>要素を使うと、内部の条件が真になった場合に自動的にWHEREキーワードを付与し、余分なANDORを取り除いてくれます。

例:複数の条件でユーザを検索

XML

<select id="searchUsers" resultType="com.example.User">
  SELECT * FROM users
  <where>
    <if test="username != null and username != ''">
      username LIKE #{username}
    </if>
    <if test="age != null">
      AND age = #{age}
    </if>
  </where>
</select>

Java

// UserMapper.java
public interface UserMapper {
  List<User> searchUsers(@Param("username") String username, @Param("age") Integer age);
}

Java

// 実行コード
// ユーザ名と年齢を指定
List<User> users1 = userMapper.searchUsers("Jiro", 30);
// ユーザ名のみ指定
List<User> users2 = userMapper.searchUsers("Hanako", null);
// 年齢のみ指定
List<User> users3 = userMapper.searchUsers(null, 25);

実行されるSQL (username=”Jiro”, age=30 の場合):

SQL

SELECT * FROM users WHERE username LIKE 'Jiro' AND age = 30

実行されるSQL (username=”Hanako”, age=null の場合):

SQL

SELECT * FROM users WHERE username LIKE 'Hanako'

実行されるSQL (username=null, age=25 の場合):

SQL

SELECT * FROM users WHERE age = 25

このように、<where>を使うことで条件が増えてもSQLが破綻せず、記述がシンプルになります。

3. <choose>, <when>, <otherwise>: いずれか一つの条件

Javaのif-else if-elseのように、複数の条件の中から一つだけ選択したい場合に便利です。

例:ソート順の指定

XML

<select id="getSortedUsers" resultType="com.example.User">
  SELECT * FROM users
  ORDER BY
  <choose>
    <when test="orderBy == 'name'">
      username
    </when>
    <when test="orderBy == 'age'">
      age DESC
    </when>
    <otherwise>
      id
    </otherwise>
  </choose>
</select>

Java

// UserMapper.java
public interface UserMapper {
  List<User> getSortedUsers(@Param("orderBy") String orderBy);
}

Java

// 実行コード
List<User> users1 = userMapper.getSortedUsers("name"); // ユーザ名でソート
List<User> users2 = userMapper.getSortedUsers("age");  // 年齢でソート
List<User> users3 = userMapper.getSortedUsers("id");   // IDでソート (otherwise)

実行されるSQL (orderBy=”name” の場合):

SQL

SELECT * FROM users ORDER BY username

実行されるSQL (orderBy=”age” の場合):

SQL

SELECT * FROM users ORDER BY age DESC

実行されるSQL (orderBy=”id” の場合):

SQL

SELECT * FROM users ORDER BY id

4. <foreach>: コレクションのループ処理

コレクション(ListやArrayなど)を反復処理し、SQLのIN句などを生成するのに非常に役立ちます。

例:複数のIDでユーザを検索

XML

<select id="findUsersByIds" resultType="com.example.User">
  SELECT * FROM users
  WHERE id IN
  <foreach item="id" collection="ids" open="(" separator="," close=")">
    #{id}
  </foreach>
</select>

Java

// UserMapper.java
public interface UserMapper {
  List<User> findUsersByIds(@Param("ids") List<Integer> ids);
}

Java

// 実行コード
List<Integer> userIds = Arrays.asList(1, 3, 5);
List<User> users = userMapper.findUsersByIds(userIds);

実行されるSQL:

SQL

SELECT * FROM users WHERE id IN (1, 3, 5)

<foreach>の属性は以下の通りです。

  • item: コレクションの各要素にアクセスする際に使用する変数名
  • index: ループカウンタにアクセスする際に使用する変数名(Mapの場合、キーに相当)
  • collection: 渡されたコレクションのパラメータ名
  • open: ループ開始時に挿入される文字列
  • separator: 各要素の間に挿入される文字列
  • close: ループ終了時に挿入される文字列

型ハンドラでデータの変換を自在に

MyBatisはJDBCの型とJavaの型を自動でマッピングしてくれますが、時としてデフォルトのマッピングでは対応できないケースや、独自の変換ロジックを適用したい場合があります。そんな時に使うのが型ハンドラ (Type Handler) です。

型ハンドラは、JavaオブジェクトをJDBCに渡す際、またはResultSetからJavaオブジェクトに変換する際の処理をカスタマイズできます。

1. デフォルトの型ハンドラ

MyBatisは多くのデータ型に対してデフォルトの型ハンドラを提供しています。例えば、StringVARCHARIntegerINTEGERなどは特別な設定なしで自動的にマッピングされます。

2. カスタム型ハンドラの作成

例えば、データベースには'TRUE'または'FALSE'という文字列で保存されている真偽値を、Java側ではboolean型として扱いたいとします。この場合、カスタム型ハンドラを作成します。

例:文字列の’TRUE’/’FALSE’とbooleanのマッピング

まず、カスタム型ハンドラクラスを作成します。BaseTypeHandlerを継承し、抽象メソッドを実装します。

Java

// StringBooleanTypeHandler.java
package com.example.typehandler;

import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;

import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class StringBooleanTypeHandler extends BaseTypeHandler<Boolean> {

    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, Boolean parameter, JdbcType jdbcType) throws SQLException {
        // JavaのbooleanをJDBCにセットする際の処理
        if (parameter) {
            ps.setString(i, "TRUE");
        } else {
            ps.setString(i, "FALSE");
        }
    }

    @Override
    public Boolean getNullableResult(ResultSet rs, String columnName) throws SQLException {
        // ResultSetからカラム名で取得し、Javaのbooleanに変換する際の処理
        String value = rs.getString(columnName);
        return "TRUE".equalsIgnoreCase(value);
    }

    @Override
    public Boolean getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        // ResultSetからカラムインデックスで取得し、Javaのbooleanに変換する際の処理
        String value = rs.getString(columnIndex);
        return "TRUE".equalsIgnoreCase(value);
    }

    @Override
    public Boolean getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        // CallableStatementから取得し、Javaのbooleanに変換する際の処理 (ストアドプロシージャなど)
        String value = cs.getString(columnIndex);
        return "TRUE".equalsIgnoreCase(value);
    }
}

次に、MyBatisの設定ファイル (mybatis-config.xml) でこの型ハンドラを登録します。

XML

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
  PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
  "https://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
  <typeHandlers>
    <typeHandler handler="com.example.typehandler.StringBooleanTypeHandler"/>
  </typeHandlers>

  <environments default="development">
    <environment id="development">
      <transactionManager type="JDBC"/>
      <dataSource type="POOLED">
        <property name="driver" value="org.h2.Driver"/>
        <property name="url" value="jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1"/>
        <property name="username" value="sa"/>
        <property name="password" value=""/>
      </dataSource>
    </environment>
  </environments>

  <mappers>
    <mapper resource="mappers/UserMapper.xml"/>
  </mappers>
</configuration>

これで、boolean型のプロパティとデータベースのVARCHAR型のカラムが、カスタム型ハンドラを通じてマッピングされるようになります。

例:ユーザにアクティブフラグ(boolean)を追加

データベースのusersテーブルにis_activeというVARCHAR(5)型のカラムがあり、'TRUE'または'FALSE'が格納されるとします。

Java

// User.java
package com.example;

public class User {
    private int id;
    private String username;
    private boolean active; // boolean型

    // コンストラクタ、getter, setter 省略
    public User(int id, String username, boolean active) {
        this.id = id;
        this.username = username;
        this.active = active;
    }

    public int getId() { return id; }
    public void setId(int id) { this.id = id; }
    public String getUsername() { return username; }
    public void setUsername(String username) { this.username = username; }
    public boolean isActive() { return active; }
    public void setActive(boolean active) { this.active = active; }

    @Override
    public String toString() {
        return "User{id=" + id + ", username='" + username + "', active=" + active + '}';
    }
}

XML

<mapper namespace="com.example.UserMapper">
  <resultMap id="userResultMap" type="com.example.User">
    <id property="id" column="id"/>
    <result property="username" column="username"/>
    <result property="active" column="is_active" javaType="boolean" jdbcType="VARCHAR" typeHandler="com.example.typehandler.StringBooleanTypeHandler"/>
  </resultMap>

  <insert id="insertUser">
    INSERT INTO users (username, is_active) VALUES (#{username}, #{active, javaType=boolean, jdbcType=VARCHAR, typeHandler=com.example.typehandler.StringBooleanTypeHandler})
  </insert>

  <select id="getUserById" resultMap="userResultMap">
    SELECT id, username, is_active FROM users WHERE id = #{id}
  </select>
</mapper>

Java

// UserMapper.java
package com.example;

import org.apache.ibatis.annotations.Param;
import java.util.List;

public interface UserMapper {
  void insertUser(User user);
  User getUserById(@Param("id") int id);
}

Java

// 実行コード
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

import java.io.IOException;
import java.io.Reader;
import java.sql.Connection;
import java.sql.Statement;

public class Main {
    public static void main(String[] args) throws IOException, SQLException {
        // H2データベースの初期化(テーブル作成)
        Connection conn = null;
        Statement stmt = null;
        try {
            conn = java.sql.DriverManager.getConnection("jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1", "sa", "");
            stmt = conn.createStatement();
            stmt.execute("CREATE TABLE users (id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(255), is_active VARCHAR(5))");
        } finally {
            if (stmt != null) stmt.close();
            if (conn != null) conn.close();
        }

        Reader reader = Resources.getResourceAsReader("mybatis-config.xml");
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);

        try (SqlSession session = sqlSessionFactory.openSession()) {
            UserMapper userMapper = session.getMapper(UserMapper.class);

            // データの挿入
            User user1 = new User(0, "Alice", true);
            userMapper.insertUser(user1);
            System.out.println("挿入されたユーザー1: " + user1);

            User user2 = new User(0, "Bob", false);
            userMapper.insertUser(user2);
            System.out.println("挿入されたユーザー2: " + user2);

            // データの取得
            User fetchedUser1 = userMapper.getUserById(1);
            System.out.println("取得したユーザー1: " + fetchedUser1);

            User fetchedUser2 = userMapper.getUserById(2);
            System.out.println("取得したユーザー2: " + fetchedUser2);

            session.commit();
        }
    }
}

実行結果:

挿入されたユーザー1: User{id=0, username='Alice', active=true}
挿入されたユーザー2: User{id=0, username='Bob', active=false}
取得したユーザー1: User{id=1, username='Alice', active=true}
取得したユーザー2: User{id=2, username='Bob', active=false}

このように、StringBooleanTypeHandlerを使うことで、データベースには文字列として保存された真偽値が、Java側では正しくboolean型として扱われていることがわかります。

<result><insert>などでtypeHandler属性を指定することで、特定のプロパティやパラメータに対してカスタム型ハンドラを適用できます。


まとめ

今回はMyBatisの動的SQL型ハンドラについて解説しました。

  • 動的SQLは、<if>, <where>, <choose>, <foreach>などのタグを使って、実行時にSQLを柔軟に生成する強力な機能です。これにより、条件によって変化する複雑なクエリもスマートに記述できます。
  • 型ハンドラは、Javaのデータ型とデータベースのデータ型間のマッピングをカスタマイズするための機能です。デフォルトのマッピングで対応できない場合や、独自の変換ロジックを適用したい場合に威力を発揮します。

これらの機能を使いこなせば、MyBatisを使ったアプリケーション開発の幅がぐっと広がるはずです。ぜひあなたのプロジェクトで活用してみてください!


MyBatisの基本については、こちらの記事も参考にしてくださいね! 
MyBatis 初級: JavaとDBをもっと仲良しに! – Hello Java World

コメント

タイトルとURLをコピーしました