SpringとMyBatisを用いた多様なクエリ実装ガイド

1. プロジェクトの準備

Maven 依存関係

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>3.0.1</version>
    </dependency>
    <dependency>
        <groupId>com.mysql</groupId>
        <artifactId>mysql-connector-j</artifactId>
        <scope>runtime</scope>
    </dependency>
</dependencies>

アプリケーション設定(application.yml)

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/example_db?useSSL=false&serverTimezone=UTC
    username: app_user
    password: secret
    driver-class-name: com.mysql.cj.jdbc.Driver

mybatis:
  mapper-locations: classpath:mappers/*.xml
  type-aliases-package: com.example.domain.model

2. エンティティとマッパー

従業員エンティティ(Employee.java)

package com.example.domain.model;

public class Employee {
    private Long id;
    private String firstName;
    private String lastName;
    private String email;
    private Integer age;

    // ゲッターとセッター (省略)
}

マッパーインターフェース(EmployeeMapper.java)

package com.example.mapper;

import com.example.domain.model.Employee;
import org.apache.ibatis.annotations.*;

import java.util.List;

@Mapper
public interface EmployeeMapper {

    @Select("SELECT * FROM employees WHERE id = #{id}")
    Employee fetchById(@Param("id") Long employeeId);

    @Select("SELECT * FROM employees")
    List<Employee> fetchAll();

    @Select("SELECT * FROM employees WHERE last_name = #{lastName}")
    List<Employee> findByLastName(@Param("lastName") String surname);

    @Select("SELECT * FROM employees WHERE age > #{minAge} AND age < #{maxAge}")
    List<Employee> findByAgeRange(@Param("minAge") int min, @Param("maxAge") int max);

    @Insert("INSERT INTO employees(first_name, last_name, email, age) VALUES(#{fn}, #{ln}, #{em}, #{ag})")
    @Options(useGeneratedKeys = true, keyProperty = "id")
    void insertEmployee(@Param("fn") String firstName,
                        @Param("ln") String lastName,
                        @Param("em") String email,
                        @Param("ag") int age);

    @Update("UPDATE employees SET email = #{email} WHERE id = #{id}")
    int updateEmail(@Param("id") Long employeeId, @Param("email") String newEmail);

    @Delete("DELETE FROM employees WHERE id = #{id}")
    int remove(@Param("id") Long employeeId);
}

3. 各種クエリ操作

単純検索(主キーによる取得)

Employee employee = employeeMapper.fetchById(101L);
System.out.println(employee.getFirstName() + " " + employee.getLastName());

条件検索(年齢範囲)

List<Employee> midAgeWorkers = employeeMapper.findByAgeRange(25, 40);
midAgeWorkers.forEach(e -> System.out.println(e.getEmail()));

ページネーション

LIMIT と OFFSET を利用したページング。マッパーに追加:

@Select("SELECT * FROM employees ORDER BY id LIMIT #{pageSize} OFFSET #{offset}")
List<Employee> findWithPagination(@Param("offset") int offset,
                                   @Param("pageSize") int pageSize);

サービス層での利用例:

int page = 2;
int size = 20;
int start = (page - 1) * size;
List<Employee> employees = employeeMapper.findWithPagination(start, size);
for (Employee emp : employees) {
    System.out.println(emp.getFirstName());
}

関連テーブルとの結合

部署テーブル(departments)を用意し、従業員と部署を結合する例。

部署エンティティ:

package com.example.domain.model;

public class Department {
    private Long id;
    private String name;
    private String location;
    // ゲッターとセッター
}

従業員マッパーに結合メソッド追加(アノテーション使用):

@Select("SELECT e.*, d.name AS dept_name FROM employees e " +
        "JOIN departments d ON e.department_id = d.id WHERE e.id = #{eid}")
@Results({
    @Result(property = "id", column = "id"),
    @Result(property = "firstName", column = "first_name"),
    @Result(property = "department", column = "department_id",
            one = @One(select = "com.example.mapper.DepartmentMapper.findById"))
})
Employee findEmployeeWithDepartment(@Param("eid") Long employeeId);

DepartmentMapper の定義:

package com.example.mapper;

import com.example.domain.model.Department;
import org.apache.ibatis.annotations.*;

@Mapper
public interface DepartmentMapper {

    @Select("SELECT * FROM departments WHERE id = #{id}")
    Department findById(@Param("id") Long deptId);
}

呼び出し例:

Employee emp = employeeMapper.findEmployeeWithDepartment(101L);
System.out.println(emp.getFirstName() + " belongs to " + emp.getDepartment().getName());

動的SQL(XML マッパー利用)

アノテーションでは複雑な条件分岐が難しい場合、XML を用いる。以下は EmployeeMapper.xml


<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mapper.EmployeeMapper">
    <select id="searchByFlexibleCondition" parameterType="map"
            resultType="com.example.domain.model.Employee">
        SELECT * FROM employees
        <where>
            <if test="lastName != null and lastName != ''">
                AND last_name LIKE CONCAT('%', #{lastName}, '%')
            </if>
            <if test="age != null">
                AND age = #{age}
            </if>
            <if test="emailSuffix != null">
                AND email LIKE CONCAT('%@', #{emailSuffix})
            </if>
        </where>
    </select>
</mapper>

マッパーインターフェースにメソッド宣言:

List<Employee> searchByFlexibleCondition(Map<String, Object> params);

利用サンプル:

Map<String, Object> conditions = new HashMap<>();
conditions.put("lastName", "Smith");
conditions.put("age", 30);
// emailSuffix は指定しない
List<Employee> result = employeeMapper.searchByFlexibleCondition(conditions);
result.forEach(emp -> System.out.println(emp.getEmail()));

タグ: Spring MyBatis Java ORM 動的SQL

7月31日 19:51 投稿