Spring BootアプリケーションでDruid、MyBatis、PageHelperを統合する

Spring Bootにおけるデータアクセス層の構築

本記事では、Spring Bootアプリケーションにおいて、高機能なデータソースであるDruid、ORMフレームワークのMyBatis、そして柔軟なページネーション機能を提供するPageHelperを統合する方法について解説します。

1. Druidデータソースの統合

Druidは、高性能な接続プールと監視機能を備えたデータソース実装です。Spring BootにDruidを組み込むことで、データベース接続の効率化と詳細なモニタリングが可能になります。

依存関係の追加

pom.xmlにDruid Spring Boot Starterと必要なMySQLドライバーを追加します。

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.2.8</version> <!-- 最新の安定版に更新してください -->
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.28</version> <!-- 使用するMySQLのバージョンに合わせてください -->
</dependency>
<!-- 必要に応じてAOP依存を追加 (Druid統計に利用) -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
</dependency>

application.ymlの設定

application.ymlファイルに、Druidデータソースの接続情報と監視に関する設定を記述します。

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/sample_db?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Tokyo
    username: root
    password: password
    druid:
      # 接続プール設定
      initial-size: 5
      min-idle: 5
      max-active: 20
      max-wait: 60000
      time-between-eviction-runs-millis: 60000
      min-evictable-idle-time-millis: 30000
      validation-query: SELECT 1 FROM DUAL
      test-while-idle: true
      test-on-borrow: false
      test-on-return: false
      pool-prepared-statements: true
      max-pool-prepared-statement-per-connection-size: 20
      # 監視フィルター設定
      filter:
        stat:
          merge-sql: true
          slow-sql-millis: 5000
      # Web統計フィルター設定
      web-stat-filter:
        enabled: true
        url-pattern: /*
        exclusions: "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*"
        session-stat-enable: true
        session-stat-max-count: 100
      # 監視コンソール設定
      stat-view-servlet:
        enabled: true
        url-pattern: /druid/*
        reset-enable: true
        login-username: admin
        login-password: admin
        allow: 127.0.0.1
        # deny: 192.168.1.100

Druid監視機能の確認

簡単なRESTコントローラを作成し、DruidのURL監視機能が動作しているかを確認します。

package com.example.app.web.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/greetings")
public class GreetingController {

    @GetMapping("/hello")
    public String sayHello() {
        return "Spring BootとDruidの連携テスト成功!";
    }
}

アプリケーションを起動後、http://localhost:8080/api/greetings/hello(ポートは環境に合わせて)にアクセスします。その後、Druidの監視コンソール(例: http://localhost:8080/druid/index.html)にアクセスし、設定したユーザー名とパスワードでログインすると、URL監視が成功していることを確認できます。

2. MyBatisの統合とコード生成

MyBatisは、SQLをXMLまたはアノテーションで直接記述できる柔軟なORMフレームワークです。MyBatis Generatorを使用することで、データベーススキーマからエンティティ、Mapperインターフェース、XMLマッピングファイルを自動生成できます。

依存関係とMavenプラグインの設定

pom.xmlにMyBatis Spring Boot StarterとMyBatis Generatorプラグインを追加します。

<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.2.0</version> <!-- 最新の安定版に更新してください -->
</dependency>

<build>
    <resources>
        <!-- Mapper XMLファイルをビルドパスに含めるための設定 -->
        <resource>
            <directory>src/main/java</directory>
            <includes>
                <include>**/*.xml</include>
            </includes>
            <filtering>false</filtering>
        </resource>
        <resource>
            <directory>src/main/resources</directory>
            <includes>
                <include>*.properties</include>
                <include>*.xml</include>
                <include>*.yml</include>
            </includes>
            <filtering>false</filtering>
        </resource>
    </resources>

    <plugins>
        <plugin>
            <groupId>org.mybatis.generator</groupId>
            <artifactId>mybatis-generator-maven-plugin</artifactId>
            <version>1.4.0</version> <!-- 最新の安定版に更新してください -->
            <dependencies>
                <!-- ジェネレータが使用するMySQL JDBCドライバ -->
                <dependency>
                    <groupId>mysql</groupId>
                    <artifactId>mysql-connector-java</artifactId>
                    <version>8.0.28</version>
                </dependency>
            </dependencies>
            <configuration>
                <overwrite>true</overwrite>
                <configurationFile>${basedir}/src/main/resources/generatorConfig.xml</configurationFile>
            </configuration>
        </plugin>
    </plugins>
</build>

generatorConfig.xmlの設定

MyBatis Generatorの設定ファイルを作成し、エンティティ、Mapperインターフェース、SQLマッピングXMLファイルの生成ルールを定義します。ここでは、sample_dbデータベースのapp_itemsテーブルからItemエンティティを生成する例を示します。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
        PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
        "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">

<generatorConfiguration>
    <properties resource="jdbc.properties"/>

    <!-- JDBCドライバJARへのパスを指定してください (例: Mavenリポジトリからダウンロードされるパス) -->
    <!-- <classPathEntry location="/path/to/mysql-connector-java-8.0.28.jar"/> -->

    <context id="SpringBootApp" targetRuntime="MyBatis3">
        <commentGenerator>
            <property name="suppressAllComments" value="true"/>
            <property name="suppressDate" value="true"/>
        </commentGenerator>

        <jdbcConnection driverClass="${jdbc.driver}"
                        connectionURL="${jdbc.url}"
                        userId="${jdbc.username}"
                        password="${jdbc.password}"/>

        <javaTypeResolver>
            <property name="forceBigDecimals" value="false"/>
        </javaTypeResolver>

        <!-- エンティティクラスの生成設定 -->
        <javaModelGenerator targetPackage="com.example.app.model.entity"
                            targetProject="src/main/java">
            <property name="enableSubPackages" value="false"/>
            <property name="constructorBased" value="true"/>
            <property name="trimStrings" value="true"/>
        </javaModelGenerator>

        <!-- SQLマッピングXMLファイルの生成設定 -->
        <sqlMapGenerator targetPackage="com.example.app.mapper"
                         targetProject="src/main/java">
            <property name="enableSubPackages" value="false"/>
        </sqlMapGenerator>

        <!-- Mapperインターフェースの生成設定 -->
        <javaClientGenerator targetPackage="com.example.app.mapper"
                             targetProject="src/main/java" type="XMLMAPPER">
            <property name="enableSubPackages" value="false"/>
        </javaClientGenerator>

        <!-- 対象テーブルの設定 -->
        <table tableName="app_items" domainObjectName="Item"
               enableCountByExample="false" enableDeleteByExample="false"
               enableSelectByExample="false" enableUpdateByExample="false">
            <property name="useActualColumnNames" value="true" />
        </table>
    </context>
</generatorConfiguration>

jdbc.propertiesファイル

データベース接続情報を外部ファイルとして定義します。

jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/sample_db?useUnicode=true&characterEncoding=UTF-8&nullCatalogMeansCurrent=true&serverTimezone=Asia/Tokyo
jdbc.username=root
jdbc.password=password

コードの生成

Mavenコマンドを実行してコードを生成します。

mvn mybatis-generator:generate -e

これにより、com.example.app.model.entity.Itemcom.example.app.mapper.ItemMappercom.example.app.mapper.ItemMapper.xmlなどが生成されます。

Mapperインターフェースのスキャン

Spring Bootアプリケーションのメインクラスに@MapperScanアノテーションを追加し、Mapperインターフェースを認識させます。

package com.example.app;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@MapperScan("com.example.app.mapper")
public class SampleApplication {
    public static void main(String[] args) {
        SpringApplication.run(SampleApplication.class, args);
    }
}

CRUD操作のテスト

生成されたMapperと連携するService層を作成し、CRUD操作をテストします。

// src/main/java/com/example/app/model/entity/Item.java (MyBatis Generatorで生成)
package com.example.app.model.entity;

public class Item {
    private Integer id;
    private String name;
    // Constructor, getters, setters...
    public Item(Integer id, String name) {
        this.id = id;
        this.name = name;
    }
    // ...
    public Integer getId() { return id; }
    public String getName() { return name; }
    public void setId(Integer id) { this.id = id; }
    public void setName(String name) { this.name = name; }

    @Override
    public String toString() {
        return "Item{id=" + id + ", name='" + name + "'}";
    }
}
// src/main/java/com/example/app/mapper/ItemMapper.java (MyBatis Generatorで生成)
package com.example.app.mapper;

import com.example.app.model.entity.Item;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;

@Mapper
public interface ItemMapper {
    int deleteByPrimaryKey(Integer id);
    int insert(Item record);
    Item selectByPrimaryKey(Integer id);
    List<Item> selectAll();
    int updateByPrimaryKey(Item record);
}
// src/main/java/com/example/app/service/ItemService.java
package com.example.app.service;

import com.example.app.model.entity.Item;
import java.util.List;
import java.util.Optional;

public interface ItemService {
    Optional<Item> findItemById(Integer itemId);
    boolean deleteItemById(Integer itemId);
    List<Item> findAllItems();
}
// src/main/java/com/example/app/service/impl/ItemServiceImpl.java
package com.example.app.service.impl;

import com.example.app.mapper.ItemMapper;
import com.example.app.model.entity.Item;
import com.example.app.service.ItemService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;

@Service
public class ItemServiceImpl implements ItemService {

    private final ItemMapper itemMapper;

    public ItemServiceImpl(ItemMapper itemMapper) {
        this.itemMapper = itemMapper;
    }

    @Override
    public Optional<Item> findItemById(Integer itemId) {
        return Optional.ofNullable(itemMapper.selectByPrimaryKey(itemId));
    }

    @Override
    public boolean deleteItemById(Integer itemId) {
        return itemMapper.deleteByPrimaryKey(itemId) > 0;
    }

    @Override
    public List<Item> findAllItems() {
        return itemMapper.selectAll();
    }
}
// src/test/java/com/example/app/service/ItemServiceTest.java
package com.example.app.service;

import com.example.app.model.entity.Item;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;

import java.util.Optional;

import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest
@Transactional // テスト後のデータ変更をロールバック
public class ItemServiceTest {

    @Autowired
    private ItemService itemService;

    @Test
    void testItemRetrieval() {
        // 事前にデータが存在することを確認または挿入
        Item newItem = new Item(null, "テストアイテムA");
        // itemMapper.insert(newItem); // テスト用に挿入ロジックが必要な場合
        // 適切なIDで検索
        Optional<Item> item = itemService.findItemById(1);
        assertThat(item).isPresent();
        System.out.println("取得したアイテム: " + item.get());
    }

    @Test
    void testItemDeletion() {
        // 事前にデータが存在することを確認または挿入
        Item newItem = new Item(null, "削除対象アイテム");
        // itemMapper.insert(newItem); // テスト用に挿入ロジックが必要な場合
        // 適切なIDで削除
        boolean deleted = itemService.deleteItemById(2);
        assertThat(deleted).isTrue();
        Optional<Item> itemAfterDeletion = itemService.findItemById(2);
        assertThat(itemAfterDeletion).isNotPresent();
        System.out.println("アイテムID 2 を削除しました。");
    }
}

3. PageHelperによるページネーションの統合

PageHelperは、MyBatisのクエリ結果に簡単にページネーションを適用できるプラグインです。

依存関係の追加

pom.xmlにPageHelper Spring Boot Starterを追加します。

<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper-spring-boot-starter</artifactId>
    <version>1.4.1</version> <!-- 最新の安定版に更新してください -->
</dependency>

application.ymlの設定

PageHelperの設定をapplication.ymlに追加します。

pagehelper:
  helperDialect: mysql
  reasonable: true # 不正なページ番号を自動修正
  supportMethodsArguments: true # メソッドの引数からページング情報を取得
  params: count=countSql # countSqlパラメータを使用
logging:
  level:
    com.example.app.mapper: debug # MyBatisのSQLログ表示

ページネーション情報クラスの作成

ページネーションの状態を保持するための汎用クラスを作成します。

package com.example.app.util.pagination;

import java.io.Serializable;
import java.util.Optional;

public class PaginationInfo implements Serializable {
    private static final long serialVersionUID = 1L;

    private int pageIndex = 1;      // 現在のページ番号 (1から開始)
    private int pageSize = 10;      // 1ページあたりのレコード数
    private long totalRecords = 0;  // 総レコード数
    private boolean enabled = true; // ページネーションを有効にするか

    public PaginationInfo() {}

    public PaginationInfo(int pageIndex, int pageSize) {
        this.pageIndex = Math.max(1, pageIndex);
        this.pageSize = Math.max(1, pageSize);
    }

    public int getPageIndex() { return pageIndex; }
    public void setPageIndex(int pageIndex) { this.pageIndex = Math.max(1, pageIndex); }

    public int getPageSize() { return pageSize; }
    public void setPageSize(int pageSize) { this.pageSize = Math.max(1, pageSize); }

    public long getTotalRecords() { return totalRecords; }
    public void setTotalRecords(long totalRecords) { this.totalRecords = totalRecords; }

    public boolean isEnabled() { return enabled; }
    public void setEnabled(boolean enabled) { this.enabled = enabled; }

    /**
     * SQLのOFFSET値 (スキップするレコード数) を計算します。
     * @return OFFSET値
     */
    public int getOffset() {
        return (this.pageIndex - 1) * this.pageSize;
    }

    /**
     * 総ページ数を計算します。
     * @return 総ページ数
     */
    public long getTotalPages() {
        if (pageSize == 0) return 0;
        return (totalRecords + pageSize - 1) / pageSize;
    }

    /**
     * 次のページ番号を返します。現在のページが最終ページの場合は最終ページ番号を返します。
     * @return 次のページ番号
     */
    public int getNextPageIndex() {
        long totalPages = getTotalPages();
        return (int) (pageIndex >= totalPages ? totalPages : pageIndex + 1);
    }

    /**
     * 前のページ番号を返します。現在のページが最初のページの場合は1を返します。
     * @return 前のページ番号
     */
    public int getPreviousPageIndex() {
        return Math.max(1, pageIndex - 1);
    }

    @Override
    public String toString() {
        return "PaginationInfo{" +
               "pageIndex=" + pageIndex +
               ", pageSize=" + pageSize +
               ", totalRecords=" + totalRecords +
               ", enabled=" + enabled +
               '}';
    }
}

AOPによるページネーション処理の適用

アスペクト指向プログラミング(AOP)を利用して、特定のServiceメソッドが呼び出された際に自動的にPageHelperを適用します。

package com.example.app.aop;

import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.example.app.util.pagination.PaginationInfo;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

import java.util.List;

@Component
@Aspect
public class PaginationAspect {

    /**
     * Service層のメソッドで、引数にPaginationInfoを含み、
     * メソッド名が 'search' または 'find' で始まるものに適用
     */
    @Around("execution(* com.example.app.service.*Service.search*(..)) || execution(* com.example.app.service.*Service.find*(..))")
    public Object handlePagination(ProceedingJoinPoint joinPoint) throws Throwable {
        PaginationInfo paginationInfo = null;
        Object[] args = joinPoint.getArgs();

        // メソッド引数からPaginationInfoオブジェクトを検索
        for (Object arg : args) {
            if (arg instanceof PaginationInfo) {
                paginationInfo = (PaginationInfo) arg;
                break;
            }
        }

        if (paginationInfo != null && paginationInfo.isEnabled()) {
            // PageHelperを有効化
            PageHelper.startPage(paginationInfo.getPageIndex(), paginationInfo.getPageSize());
        }

        // 元のメソッドを実行
        Object result = joinPoint.proceed(args);

        if (paginationInfo != null && paginationInfo.isEnabled() && result instanceof List) {
            // PageInfoで結果をラップし、PaginationInfoに総レコード数を設定
            PageInfo<?> pageInfo = new PageInfo<>((List<?>) result);
            paginationInfo.setTotalRecords(pageInfo.getTotal());
        }

        return result;
    }
}

検索条件ユーティリティ

検索クエリでLIKE句を使用するためのシンプルなユーティリティクラス。

package com.example.app.util;

public class SearchUtil {
    public static String wrapWithLikeOperators(String input) {
        return Optional.ofNullable(input)
                .map(s -> "%" + s.trim() + "%")
                .orElse(null);
    }
}

ページネーション機能のテスト

MyBatis Mapperに検索メソッドを追加し、Service層でページネーションを適用してテストします。

// ItemMapperに検索メソッドを追加
// src/main/java/com/example/app/mapper/ItemMapper.java
package com.example.app.mapper;

import com.example.app.model.entity.Item;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;

@Mapper
public interface ItemMapper {
    // ... 既存のメソッド ...
    List<Item> findItemsByNameLike(@Param("itemName") String itemName);
}
<!-- ItemMapper.xml に対応するSQLを追加 -->
<!-- src/main/java/com/example/app/mapper/ItemMapper.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.app.mapper.ItemMapper">
    <resultMap id="BaseResultMap" type="com.example.app.model.entity.Item">
        <id column="id" jdbcType="INTEGER" property="id" />
        <result column="name" jdbcType="VARCHAR" property="name" />
    </resultMap>

    <!-- ... 既存のSQL ... -->

    <select id="findItemsByNameLike" resultMap="BaseResultMap">
        SELECT id, name
        FROM app_items
        <where>
            <if test="itemName != null and itemName != ''">
                AND name LIKE #{itemName}
            </if>
        </where>
        ORDER BY id
    </select>
</mapper>
// ItemServiceにページネーション検索メソッドを追加
// src/main/java/com/example/app/service/ItemService.java
package com.example.app.service;

import com.example.app.model.entity.Item;
import com.example.app.util.pagination.PaginationInfo;
import java.util.List;
import java.util.Optional;

public interface ItemService {
    // ... 既存のメソッド ...
    List<Item> searchItemsPaged(String itemName, PaginationInfo paginationInfo);
}
// ItemServiceImplにページネーション検索の実装を追加
// src/main/java/com/example/app/service/impl/ItemServiceImpl.java
package com.example.app.service.impl;

import com.example.app.mapper.ItemMapper;
import com.example.app.model.entity.Item;
import com.example.app.service.ItemService;
import com.example.app.util.SearchUtil;
import com.example.app.util.pagination.PaginationInfo;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;

@Service
public class ItemServiceImpl implements ItemService {

    private final ItemMapper itemMapper;

    public ItemServiceImpl(ItemMapper itemMapper) {
        this.itemMapper = itemMapper;
    }

    // ... 既存のメソッド ...

    @Override
    public List<Item> searchItemsPaged(String itemName, PaginationInfo paginationInfo) {
        String likeName = SearchUtil.wrapWithLikeOperators(itemName);
        return itemMapper.findItemsByNameLike(likeName);
    }
}
// src/test/java/com/example/app/service/ItemServiceIntegrationTest.java
package com.example.app.service;

import com.example.app.model.entity.Item;
import com.example.app.util.pagination.PaginationInfo;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest
@Transactional
public class ItemServiceIntegrationTest {

    @Autowired
    private ItemService itemService;

    @BeforeEach
    void setup() {
        // テスト用のダミーデータを挿入 (MyBatis Mapperを直接使用)
        // itemService.deleteItemById(999); // 必要に応じて既存データをクリーンアップ
        // itemService.deleteItemById(998);
        // itemService.insert(new Item(null, "サンプル商品A"));
        // itemService.insert(new Item(null, "商品B"));
        // itemService.insert(new Item(null, "テスト商品C"));
        // itemService.insert(new Item(null, "サンプル品D"));
        // itemService.insert(new Item(null, "別カテゴリ商品E"));
        // ... 十分なデータがあればコメントアウト
    }

    @Test
    void testPagedItemSearch() {
        // ページネーション情報を設定
        PaginationInfo pInfo = new PaginationInfo();
        pInfo.setPageIndex(1); // 1ページ目
        pInfo.setPageSize(3);  // 3件/ページ

        // 検索を実行
        List<Item> resultPage1 = itemService.searchItemsPaged("商品", pInfo);

        System.out.println("--- 1ページ目の結果 ---");
        resultPage1.forEach(System.out::println);
        System.out.println("ページ情報: " + pInfo);
        assertThat(resultPage1).hasSize(3); // 3件取得されるか
        assertThat(pInfo.getTotalRecords()).isGreaterThan(0); // 総レコード数が更新されるか

        // 2ページ目を試す
        pInfo.setPageIndex(2);
        List<Item> resultPage2 = itemService.searchItemsPaged("商品", pInfo);
        System.out.println("\n--- 2ページ目の結果 ---");
        resultPage2.forEach(System.out::println);
        System.out.println("ページ情報: " + pInfo);
        //assertThat(resultPage2.size()).isLessThanOrEqualTo(3); // 2ページ目も取得されるか (データ数による)
    }
}

タグ: Spring Boot Druid MyBatis PageHelper データソース

8月28日 13:04 投稿