概要
MyBatis フレームワークを用いた開発において、複数のテーブルを結合したデータ取得および結果のページネーション処理は頻出する要件です。本稿では、動的 SQL 機能を活用し、単表查詢を超える複雑な結合查詢の実装方法と、MySQL 環境における効率的なページネーション仕組みについて解説します。
ドメインモデルの定義
まず、データベースのレコードを映射する Java クラスを準備します。ここでは「プロジェクト」と「担当者」の関係を例に、多対一の関連性を表現します。パッケージ構成は com.example.mybatis.domain とします。
プロジェクト情報を保持する Project クラスでは、担当者情報を表す Account クラスをメンバー変数として持ちます。また、テスト出力用に toString メソッドをオーバーライドします。
package com.example.mybatis.domain;
import java.text.SimpleDateFormat;
import java.util.Date;
public class Project {
private int projectId;
private String projectName;
private int ownerId;
private int activeFlag;
private Date registeredAt;
private Account owner;
public Project() {
this.registeredAt = new Date();
this.owner = new Account();
}
public Project(String name, int ownerId) {
this.projectName = name;
this.ownerId = ownerId;
this.owner = new Account();
this.activeFlag = 1;
this.registeredAt = new Date();
}
// Getter/Setter 省略(標準的な実装)
public int getProjectId() { return projectId; }
public void setProjectId(int projectId) { this.projectId = projectId; }
public Account getOwner() { return owner; }
public void setOwner(Account owner) { this.owner = owner; }
public String getProjectName() { return projectName; }
public void setProjectName(String projectName) { this.projectName = projectName; }
public int getActiveFlag() { return activeFlag; }
public void setActiveFlag(int activeFlag) { this.activeFlag = activeFlag; }
public Date getRegisteredAt() { return registeredAt; }
public void setRegisteredAt(Date registeredAt) { this.registeredAt = registeredAt; }
public int getOwnerId() {
return (owner != null) ? owner.getId() : this.ownerId;
}
public void setOwnerId(int ownerId) {
this.ownerId = ownerId;
}
@Override
public String toString() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
StringBuilder sb = new StringBuilder();
sb.append(String.format("Project=> {ID:%d, Name:%s, Time:%s}\n",
projectId, projectName, sdf.format(registeredAt)));
if (owner != null) {
sb.append(String.format("Account=> %s", owner.toString()));
}
return sb.toString();
}
}
Mapper インターフェースの作成
データアクセス層のインターフェースを定義します。 CRUD 操作に加え、リスト取得メソッドを宣言します。
package com.example.mybatis.mapper;
import com.example.mybatis.domain.Project;
import java.util.List;
public interface ProjectMapper {
int insert(Project project);
int delete(int id);
int update(Project project);
Project selectById(int id);
List<Project> selectAll();
}
多表結合查詢の設定
MyBatis の XML 映射ファイルにおいて、結合查詢の結果を適切にオブジェクトへマップするには、resultMap と association タグを使用します。重要な点は、結合元のテーブル間でカラム名が重複する場合(例:id, name など)、SQL 側でエイリアスを付与し、映射定義で明確に区別することです。
<?xml version="1.0" encoding="UTF-8"?>
<mapper namespace="com.example.mybatis.mapper.ProjectMapper">
<resultMap type="Account" id="accountResult">
<id column="acc_id" property="id" />
<result column="acc_name" property="name" />
</resultMap>
<resultMap type="Project" id="projectResult">
<id column="proj_id" property="projectId" />
<result column="proj_name" property="projectName" />
<result column="proj_status" property="activeFlag" />
<result column="proj_time" property="registeredAt" />
<association property="owner" javaType="Account" resultMap="accountResult" />
</resultMap>
<select id="selectById" parameterType="int" resultMap="projectResult">
SELECT
p.id AS proj_id,
p.name AS proj_name,
p.status AS proj_status,
p.created_at AS proj_time,
a.id AS acc_id,
a.name AS acc_name
FROM Project p
INNER JOIN Account a ON p.owner_id = a.id
WHERE p.status > 0 AND p.id = #{id}
</select>
<select id="selectAll" resultMap="projectResult">
SELECT
p.id AS proj_id,
p.name AS proj_name,
p.status AS proj_status,
p.created_at AS proj_time,
a.id AS acc_id,
a.name AS acc_name
FROM Project p
INNER JOIN Account a ON p.owner_id = a.id
WHERE p.status > 0
</select>
</mapper>
もしエイリアスを付与せず、両テーブルの id をそのまま選択した場合、後続のカラム値が前の値を上書きしてしまう現象が発生します。これを防ぐため、AS キーワードを用いて列名を一意に識別できるようにします。
ページネーション処理の実装
Web アプリケーションでは大量データの表示ためにページネーションが不可欠です。ここでは、開始オフセットと表示件数をパラメータとして受け取り、MySQL の LIMIT 句を用いて制御します。
查詢条件をカプセル化する PageCondition クラスと、ソート方向を定義する列挙型 SortType を作成します。
package com.example.mybatis.domain;
public class PageCondition {
private int pageNum;
private int pageSize;
private int offset;
private String sortColumn;
private String sortOrder;
public PageCondition(int pageNum, int pageSize, String sortColumn, String sortOrder) {
this.pageNum = pageNum;
this.pageSize = pageSize;
this.sortColumn = sortColumn;
this.sortOrder = sortOrder;
this.offset = pageNum * pageSize;
}
public int getOffset() { return offset; }
public int getPageSize() { return pageSize; }
public String getSortColumn() { return sortColumn; }
public String getSortOrder() { return sortOrder; }
}
public enum SortType {
ASC, DESC
}
動的 SQL による分页查詢
担当者リストの取得において、ページネーションと動的ソートを実現する Mapper 設定です。ソートカラムや方向はユーザー入力に依存するため、プレースホルダー ${} を使用しますが、SQL インジェクション対策として入力値の検証はアプリケーション側で行う必要があります。
<mapper namespace="com.example.mybatis.mapper.AccountMapper">
<sql id="baseQuery">
SELECT * FROM Account WHERE status > 0
</sql>
<sql id="orderClause">
ORDER BY ${sortColumn} ${sortOrder}
</sql>
<select id="selectByPage" parameterType="PageCondition" resultType="Account">
SELECT * FROM (
<include refid="baseQuery" />
<include refid="orderClause" />
) AS temp
<if test="offset >= 0 and pageSize > 0">
LIMIT #{offset}, #{pageSize}
</if>
</select>
</mapper>
テスト実行時には、例えば 2 ページ目(オフセット 5)、1 ページ 5 件表示、ID 降順といった条件を PageCondition に設定して渡します。これにより、データベース側で必要なレコードのみを取得でき、メモリ効率を最適化できます。
行番号の付与
MySQL には Oracle のような ROWNUM が標準で存在しないため、変数を用いて擬似的に行番号を生成する手法があります。查詢結果に現在の行位置を表示したい場合、以下のような SQL 構造を採用します。
<select id="selectByPageWithRowNum" resultMap="accountWithRowNumResult">
SELECT t.RowNum, t.Id, t.Name, t.Email, t.Status, t.CreateTime
FROM (
SELECT @rownum:=@rownum+1 AS RowNum, result.*
FROM (
SELECT @rownum:=0, Account.* FROM Account WHERE status > 0
) AS result
<include refid="orderClause" />
) AS t
<if test="offset >= 0 and pageSize > 0">
LIMIT #{offset}, #{pageSize}
</if>
</select>
この方法を用いることで、ページネーションされた結果セット内での相対的な行番号を Java オブジェクトの rownum プロパティに映射することが可能になります。