1.ケーススタディ
複数の異なるデータ型のパラメーター(例えばIDは数値型、都道府県は文字列型)で検索を絞り込み、さらに都道府県ごとにレコードを返す 、というテーマで考えてみましょう。
これは少し複雑になります。MyBatisでこのような処理を実現するには、主に以下の要素を組み合わせる必要があります。
- 複数のパラメーターの受け渡し: 複数のパラメーターをXMLマッパーに渡す方法。
-
動的SQL (
<where>
タグと<if>
タグ): 渡されたパラメーターに基づいて検索条件を動的に組み立てる。 -
GROUP BY
句と<foreach>
タグ: 都道府県でグループ化し、それぞれのグループのレコードを取得する。
しかし、提示された要件「都道府県別にレコードを返す」の解釈がいくつか考えられます。
- 解釈1:都道府県ごとにリストの要素を分けるのではなく、最終的に1つのリストとして返す。ただし、検索条件にはIDと都道府県の両方が指定可能である。
- 解釈2:都道府県をキーとするMapのような構造で結果を返す。例えば、
Map<String, List<Model>>
のように、都道府県名をキーとして、その都道府県のレコードリストを値とする。
まずは解釈1の「最終的に1つのリストとして返す」方法を示し、必要であれば解釈2の方法も説明します。
解釈1:最終的に1つのリストとして返す場合
Javaインターフェース (UserMapper
):
package com.example.mapper;
import com.example.model.UserModel;
import java.util.List;
import java.util.Map;
import org.apache.ibatis.annotations.Param;
public interface UserMapper {
List<UserModel> findUsersByIdAndPrefecture(@Param("id") Long id, @Param("prefecture") String prefecture);
}
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.mapper.UserMapper">
<resultMap id="userResultMap" type="com.example.model.UserModel">
<id column="id" property="id" />
<result column="name" property="name" />
<result column="prefecture" property="prefecture" />
</resultMap>
<select id="findUsersByIdAndPrefecture" resultMap="userResultMap">
SELECT id, name, prefecture
FROM users
<where>
<if test="id != null">
AND id = #{id}
</if>
<if test="prefecture != null and prefecture != ''">
AND prefecture = #{prefecture}
</if>
</where>
ORDER BY prefecture </select>
</mapper>
解説 (解釈1):
-
@Param("id") Long id, @Param("prefecture") String prefecture
:- Javaインターフェースのメソッドで複数のパラメーターを受け取る場合、
@Param
アノテーションを使用してXMLマッパー内で参照するための名前を明示的に指定します。ここでは、id
パラメーターは"id"
という名前で、prefecture
パラメーターは"prefecture"
という名前で参照できるようになります。
- Javaインターフェースのメソッドで複数のパラメーターを受け取る場合、
-
<where>
タグ:-
<where>
タグは、囲まれた条件の少なくとも1つが真の場合にWHERE
句を自動的に追加し、先頭のAND
やOR
を削除する便利なタグです。
-
-
<if test="id != null">
:-
<if>
タグは、指定されたtest
属性の条件が真の場合に、囲まれたSQLフラグメントをSQLに含めます。ここでは、id
がnull
でない場合にAND id = #{id}
という条件が追加されます。
-
-
<if test="prefecture != null and prefecture != ''">
:- 同様に、
prefecture
がnull
でなく、かつ空文字列でない場合にAND prefecture = #{prefecture}
という条件が追加されます。
- 同様に、
-
ORDER BY prefecture
:- 結果を都道府県でソートしておくことで、Java側で都道府県ごとの処理が必要になった場合に扱いやすくなります。
解釈2:都道府県をキーとする Map で結果を返す場合
この場合は、MyBatisのXMLマッパーだけで完全に都道府県ごとのリストを作成するのは少し難しいです。通常は、上記のように検索結果を1つのリストとして取得した後、Java側で都道府県をキーとする Map
に変換する処理を行います。
Javaでの後処理の例:
List<UserModel> userList = userMapper.findUsersByIdAndPrefecture(someId, somePrefecture);
Map<String, List<UserModel>> usersByPrefecture = new HashMap<>();
for (UserModel user : userList) {
String prefecture = user.getPrefecture();
if (!usersByPrefecture.containsKey(prefecture)) {
usersByPrefecture.put(prefecture, new ArrayList<>());
}
usersByPrefecture.get(prefecture).add(user);
}
// usersByPrefecture には都道府県ごとのユーザーリストが格納されています
XMLマッパー (解釈2の場合でも基本的には同じ):
XMLマッパー自体は解釈1と大きく変わりません。findUsersByIdAndPrefecture
メソッドは List<UserModel>
を返すように定義します。
<?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.mapper.UserMapper">
<resultMap id="userResultMap" type="com.example.model.UserModel">
<id column="id" property="id" />
<result column="name" property="name" />
<result column="prefecture" property="prefecture" />
</resultMap>
<select id="findUsersByIdAndPrefecture" resultMap="userResultMap">
SELECT id, name, prefecture
FROM users
<where>
<if test="id != null">
AND id = #{id}
</if>
<if test="prefecture != null and prefecture != ''">
AND prefecture = #{prefecture}
</if>
</where>
ORDER BY prefecture
</select>
</mapper>
Javaインターフェース (解釈2の場合):
package com.example.mapper;
import com.example.model.UserModel;
import java.util.List;
import org.apache.ibatis.annotations.Param;
public interface UserMapper {
List<UserModel> findUsersByIdAndPrefecture(@Param("id") Long id, @Param("prefecture") String prefecture);
}
FOREACH
を使って都道府県別に処理する場合 (より複雑なケース)
もし、SQLレベルで都道府県ごとに何らかの処理を行いたい(例えば、都道府県ごとに集計関数を適用するなど)のであれば、GROUP BY
句と組み合わせて使うことが考えられますが、「都道府県別にレコードを返す」という要件で FOREACH
を直接的に使うのは少し不自然です。FOREACH
は通常、コレクション型のパラメーターをSQLの中で展開するために使用されます(例えば、複数のIDで検索する場合の IN
句など)。
もし、都道府県のリストが事前にJava側で用意されており、そのリストの各都道府県に対応するレコードを取得したい、という意図であれば、以下のような実装になります。
Javaインターフェース (UserMapper
):
package com.example.mapper;
import com.example.model.UserModel;
import java.util.List;
import org.apache.ibatis.annotations.Param;
public interface UserMapper {
List<UserModel> findUsersByIdAndPrefectures(@Param("id") Long id, @Param("prefectures") List<String> prefectures);
}
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.mapper.UserMapper">
<resultMap id="userResultMap" type="com.example.model.UserModel">
<id column="id" property="id" />
<result column="name" property="name" />
<result column="prefecture" property="prefecture" />
</resultMap>
<select id="findUsersByIdAndPrefectures" resultMap="userResultMap">
SELECT id, name, prefecture
FROM users
<where>
<if test="id != null">
AND id = #{id}
</if>
<if test="prefectures != null and !prefectures.isEmpty()">
AND prefecture IN
<foreach item="prefecture" collection="prefectures" open="(" separator="," close=")">
#{prefecture}
</foreach>
</if>
</where>
ORDER BY prefecture
</select>
</mapper>
解説 (複数の都道府県で検索する場合):
- Java側から
List<String>
型のprefectures
パラメーターを渡します。 - XMLマッパーでは、
<foreach>
タグを使ってこのリストを展開し、IN
句を作成します。-
item="prefecture"
: リストの各要素をprefecture
という名前で参照します。 -
collection="prefectures"
: 繰り返しの対象となるコレクション(リスト)を指定します。 -
open="("
:IN
句の開始括弧。 -
separator=","
: 各要素の間の区切り文字。 -
close=")"
:IN
句の終了括弧。 -
#{prefecture}
: 各都道府県の値をプレースホルダーとして埋め込みます。
-
2.データバインディングの仕方
MyBatis でよく使う jdbcType をいくつか、データ型ごとにバインド例と一緒に紹介します。
明示的にデータバインドを行うことで、サービスクラスでMapper呼び出し時に、引数をHashMapで準備する必要なく、引数をそのまま代入する事が可能となり、コードの可読性が向上します。
1. 数値型
-- INTEGER(整数)
#{id, jdbcType=INTEGER}
-- BIGINT(長整数)
#{bigId, jdbcType=BIGINT}
-- DECIMAL(小数)
#{price, jdbcType=DECIMAL}
2. 文字列型
-- VARCHAR
#{name, jdbcType=VARCHAR}
-- CHAR
#{initial, jdbcType=CHAR}
-- TEXT(DB依存:MySQLなど)
#{description, jdbcType=LONGVARCHAR}
3. 日付・時刻型
-- DATE(日付のみ)
#{birthDate, jdbcType=DATE}
-- TIME(時間のみ)
#{startTime, jdbcType=TIME}
-- TIMESTAMP(日付+時刻)
#{createdAt, jdbcType=TIMESTAMP}
4. 真偽値型
-- BOOLEAN
#{isActive, jdbcType=BOOLEAN}
5. バイナリ型
-- BINARY / VARBINARY
#{fileData, jdbcType=VARBINARY}
-- BLOB
#{image, jdbcType=BLOB}
6. その他(状況によって)
-- NULL 値明示(INSERTなどで意図的にNULLを入れる場合)
#{optionalValue, jdbcType=NULL}
使うデータベース(MySQL、PostgreSQL、Oracle など)によって対応する jdbcType は微妙に変わることもあるので、ドライバの仕様も参考にすると安心です。