MyBatis Plusで実現する!JavaのDB操作を10倍速くする実践ガイド2024

MyBatis Plusとは?初心者でもわかる基礎解説

MyBatis Plusが解決する3つの開発課題

MyBatis Plusは、MyBatisを拡張した強力なパーシステンスフレームワークです。以下の3つの主要な開発課題を効果的に解決します:

  1. ボイラープレートコードの削減
    • 従来のMyBatisでは必要だった基本的なCRUD操作のXMLマッピングが不要
    • 標準的なSQL操作を自動生成
    • アノテーションベースの簡潔な設定
  2. 開発生産性の向上
    • AutoMapperによる自動マッピング機能
    • コード生成ツールの提供
    • 豊富なプラグインエコシステム
  3. 保守性とスケーラビリティの改善
    • 統一された操作インターフェース
    • クリーンなアーキテクチャの促進
    • パフォーマンス最適化機能の組み込み

従来のMyBatisと比べて何が違う?

以下の表で、主要な違いを比較します:
機能MyBatisMyBatis Plus
CRUD操作XML定義が必要アノテーションのみで可能
ページネーション手動実装が必要組み込み機能として提供
楽観的ロック独自実装が必要@Versionアノテーションで対応
論理削除独自実装が必要@TableLogicで自動対応
コード生成外部ツールが必要統合ツールとして提供
動的テーブル名複雑な設定が必要簡単な設定で対応可能

導入による具体的なメリット

  1. 開発時間の短縮
    • 基本的なCRUD操作が数行で実装可能
    • コード生成機能による初期コードの自動生成
    • 豊富なユーティリティメソッドの提供
  2. 保守性の向上
    • 統一された操作インターフェース
    • 標準化されたコーディングスタイル
    • 充実したドキュメントとコミュニティサポート
  3. 拡張性の確保
    • プラグインによる機能拡張
    • カスタムインジェクターの実装
    • 独自の最適化戦略の適用
  4. パフォーマンスの最適化
    • SQLインジェクション防止
    • N+1問題の解決支援
    • 効率的なバッチ処理の実装

技術的特徴

// 従来のMyBatisでのRepository実装
public interface UserMapper {
    @Select("SELECT * FROM users WHERE id = #{id}")
    User selectById(Long id);

    @Insert("INSERT INTO users(name, age) VALUES(#{name}, #{age})")
    int insert(User user);
    // 他のCRUD操作も同様に実装が必要
}

// MyBatis Plusでの実装
public interface UserMapper extends BaseMapper<User> {
    // 基本的なCRUD操作は自動で提供される
    // カスタム操作のみを追加定義
}

このように、MyBatis Plusは従来のMyBatisの機能を完全に継承しながら、より効率的で保守性の高い開発を可能にします。特に、エンタープライズアプリケーションの開発において、その真価を発揮します。

【環境構築】5分で始めるMyBatis Plus導入手順

必要な依存関係の追加方法

Maven での設定

<!-- pom.xmlに以下の依存関係を追加 -->
<dependencies>
    <!-- MyBatis Plus本体 -->
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.5.5</version>
    </dependency>

    <!-- データベースドライバ(MySQL使用時の例) -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.33</version>
    </dependency>

    <!-- コード生成ツール(オプション) -->
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-generator</artifactId>
        <version>3.5.5</version>
    </dependency>
</dependencies>

Gradle での設定

// build.gradleに以下の依存関係を追加
dependencies {
    implementation 'com.baomidou:mybatis-plus-boot-starter:3.5.5'
    implementation 'mysql:mysql-connector-java:8.0.33'
    implementation 'com.baomidou:mybatis-plus-generator:3.5.5'
}

Spring Bootでの基本設定

1. アプリケーション設定

# application.yml
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/your_database
    username: your_username
    password: your_password
    driver-class-name: com.mysql.cj.jdbc.Driver

mybatis-plus:
  configuration:
    # SQL実行ログの出力(開発時のみ推奨)
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    # マップアンダースコア型のカラム名をキャメルケースのプロパティにマッピング
    map-underscore-to-camel-case: true
  global-config:
    db-config:
      # プライマリーキーの自動採番方式(AUTO、INPUT、ASSIGN_ID、ASSIGN_UUID)
      id-type: ASSIGN_ID
      # テーブルの接頭辞設定(オプション)
      table-prefix: t_
      # 論理削除フィールドの設定
      logic-delete-field: deleted
      logic-delete-value: 1
      logic-not-delete-value: 0

2. エンティティクラスの作成

import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;

@Data
@TableName("users")  // テーブル名の指定
public class User {
    @TableId(type = IdType.ASSIGN_ID)  // プライマリーキーの設定
    private Long id;

    @TableField("user_name")  // カラム名の指定
    private String userName;

    private Integer age;

    @TableLogic  // 論理削除フィールド
    private Integer deleted;

    @Version  // 楽観的ロック用のバージョンフィールド
    private Integer version;
}

3. Mapperインターフェースの作成

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface UserMapper extends BaseMapper<User> {
    // BaseMapperを継承するだけで基本的なCRUD操作が使用可能
}

4. サービス層の実装

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;

@Service
public class UserService extends ServiceImpl<UserMapper, User> {
    // ServiceImplを継承することで、より高度な操作メソッドが使用可能
}

設定確認のためのテストコード

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
public class UserServiceTest {
    @Autowired
    private UserService userService;

    @Test
    public void testCrud() {
        // ユーザーの作成
        User user = new User();
        user.setUserName("テストユーザー");
        user.setAge(25);
        userService.save(user);

        // ユーザーの取得
        User saved = userService.getById(user.getId());
        assert saved != null;
        assert saved.getUserName().equals("テストユーザー");
    }
}

主な設定オプション一覧

設定項目説明デフォルト値
map-underscore-to-camel-caseスネークケースからキャメルケースへの自動変換true
id-typeプライマリーキーの生成方式ASSIGN_ID
table-prefixテーブル名の接頭辞なし
logic-delete-field論理削除フィールド名なし
capital-mode大文字テーブル名の使用false
refresh-mapperXMLの熱リロードtrue
セットアップ時の注意点
  1. データベース接続設定
    • 接続URLが正しいことを確認
    • 適切な権限を持つユーザーを使用
    • 文字コード設定の確認
  2. エンティティマッピング
    • テーブル名とクラス名の対応関係の確認
    • カラム名とフィールド名の対応確認
    • 適切なアノテーションの使用
  3. 開発環境での設定
    • デバッグログの有効化
    • HotReloadの設定
    • テスト環境の分離

CRUD操作を劇的に簡略化!基本機能の使い方

アノテーションだけで実装するテーブル連携

基本的なエンティティ設定

import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;

@Data
@TableName("t_product")
public class Product {
    // IDの自動生成(雪花アルゴリズム)
    @TableId(type = IdType.ASSIGN_ID)
    private Long id;

    // カラム名が異なる場合の明示的なマッピング
    @TableField("product_name")
    private String name;

    private BigDecimal price;

    // 特定のフィールドをカラムとして扱わない
    @TableField(exist = false)
    private String tempField;

    // 楽観的ロック用のバージョン管理
    @Version
    private Integer version;

    // 論理削除フラグ
    @TableLogic
    private Integer deleted;

    // 自動填充機能
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;

    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;
}

自動填充の実装例

@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
    @Override
    public void insertFill(MetaObject metaObject) {
        // 新規登録時の自動設定
        this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
        this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
    }

    @Override
    public void updateFill(MetaObject metaObject) {
        // 更新時の自動設定
        this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
    }
}

たった3行で実装できるCRUD操作

基本的なCRUD操作例

@Service
@RequiredArgsConstructor
public class ProductService extends ServiceImpl<ProductMapper, Product> {

    // 新規登録
    public void createProduct() {
        Product product = new Product();
        product.setName("新商品");
        product.setPrice(new BigDecimal("1000"));
        // 1行で登録完了
        save(product);
    }

    // 一件取得
    public Product getProduct(Long id) {
        // 1行で取得完了
        return getById(id);
    }

    // 更新
    public void updateProduct(Long id) {
        Product product = getById(id);
        product.setPrice(new BigDecimal("2000"));
        // 1行で更新完了
        updateById(product);
    }

    // 削除
    public void deleteProduct(Long id) {
        // 1行で削除完了(論理削除の場合は自動的に論理削除になる)
        removeById(id);
    }

    // バッチ操作
    public void batchOperation() {
        // バッチ登録
        List<Product> products = Arrays.asList(
            new Product().setName("商品1").setPrice(new BigDecimal("1000")),
            new Product().setName("商品2").setPrice(new BigDecimal("2000"))
        );
        saveBatch(products);

        // バッチ更新
        updateBatchById(products);

        // バッチ削除
        removeBatchByIds(Arrays.asList(1L, 2L, 3L));
    }
}

カスタムSQLの書き方とベストプラクティス

1. アノテーションベースの方法

@Mapper
public interface ProductMapper extends BaseMapper<Product> {
    // SELECT文の例
    @Select("SELECT * FROM t_product WHERE price > #{minPrice}")
    List<Product> findExpensiveProducts(@Param("minPrice") BigDecimal minPrice);

    // UPDATE文の例
    @Update("UPDATE t_product SET stock = stock - #{quantity} WHERE id = #{productId}")
    int decreaseStock(@Param("productId") Long productId, @Param("quantity") Integer quantity);

    // 動的SQLの例
    @Select("""
        SELECT * FROM t_product 
        WHERE 1=1
        <if test="categoryId != null">
            AND category_id = #{categoryId}
        </if>
        <if test="minPrice != null">
            AND price >= #{minPrice}
        </if>
        ORDER BY create_time DESC
    """)
    List<Product> findByCriteria(@Param("categoryId") Long categoryId, @Param("minPrice") BigDecimal minPrice);
}

2. Wrapper APIを使用した動的クエリ

@Service
@RequiredArgsConstructor
public class ProductService extends ServiceImpl<ProductMapper, Product> {

    public List<Product> searchProducts(String name, BigDecimal minPrice, BigDecimal maxPrice) {
        // QueryWrapperを使用した動的クエリ構築
        QueryWrapper<Product> queryWrapper = new QueryWrapper<Product>()
            .like(StringUtils.isNotBlank(name), "product_name", name)
            .ge(minPrice != null, "price", minPrice)
            .le(maxPrice != null, "price", maxPrice)
            .orderByDesc("create_time");

        return list(queryWrapper);
    }

    public List<Product> advancedSearch(ProductSearchDTO searchDTO) {
        // LambdaQueryWrapperを使用した型安全なクエリ構築
        LambdaQueryWrapper<Product> wrapper = new LambdaQueryWrapper<Product>()
            .like(StringUtils.isNotBlank(searchDTO.getName()), Product::getName, searchDTO.getName())
            .between(
                searchDTO.getMinPrice() != null && searchDTO.getMaxPrice() != null,
                Product::getPrice,
                searchDTO.getMinPrice(),
                searchDTO.getMaxPrice()
            )
            .in(CollectionUtils.isNotEmpty(searchDTO.getCategoryIds()), 
                Product::getCategoryId,
                searchDTO.getCategoryIds()
            );

        return list(wrapper);
    }
}

ベストプラクティス

  1. 性能最適化のポイント
    • バッチ操作の活用
    • インデックスの適切な設定
    • 必要なカラムのみの取得
// 必要なカラムのみを取得する例
List<Product> products = new LambdaQueryWrapper<Product>()
    .select(Product::getId, Product::getName, Product::getPrice)
    .list();
  1. トランザクション管理
@Transactional(rollbackFor = Exception.class)
public void complexOperation() {
    // 商品の登録
    Product product = new Product();
    product.setName("新商品");
    save(product);

    // 在庫の登録
    Stock stock = new Stock();
    stock.setProductId(product.getId());
    stockMapper.insert(stock);

    // エラーが発生した場合は全て自動ロールバック
}
  1. 楽観的ロックの活用
@Transactional
public boolean updateStock(Long productId, Integer quantity) {
    Product product = getById(productId);
    product.setStock(product.getStock() - quantity);
    // バージョンチェックが自動的に行われる
    return updateById(product);
}
  1. カスタムSQLとWrapper APIの使い分け
    • 単純な条件検索 → Wrapper API
    • 複雑なJOIN、集計 → カスタムSQL
    • パフォーマンスが重要な場合 → カスタムSQL

開発効率を2倍にする!実践的な活用テクニック

ページネーション機能の実装方法

基本的なページネーション

@Service
@RequiredArgsConstructor
public class ProductService extends ServiceImpl<ProductMapper, Product> {

    public IPage<Product> getProductPage(Integer pageNum, Integer pageSize) {
        // ページネーションオブジェクトの作成
        Page<Product> page = new Page<>(pageNum, pageSize);

        // ソート条件の追加
        page.addOrder(OrderItem.desc("create_time"));

        // ページング検索の実行
        return page(page);
    }

    // 検索条件付きページネーション
    public IPage<Product> searchProductPage(ProductSearchDTO searchDTO) {
        Page<Product> page = new Page<>(searchDTO.getPageNum(), searchDTO.getPageSize());

        LambdaQueryWrapper<Product> wrapper = new LambdaQueryWrapper<Product>()
            .like(StringUtils.isNotBlank(searchDTO.getName()), 
                  Product::getName, searchDTO.getName())
            .ge(searchDTO.getMinPrice() != null, 
                Product::getPrice, searchDTO.getMinPrice())
            .orderByDesc(Product::getCreateTime);

        return page(page, wrapper);
    }
}

カスタムページネーション

@Mapper
public interface ProductMapper extends BaseMapper<Product> {
    // 複雑なJOINを含むページネーション
    @Select("""
        SELECT p.*, c.category_name, s.stock_quantity 
        FROM t_product p 
        LEFT JOIN t_category c ON p.category_id = c.id 
        LEFT JOIN t_stock s ON p.id = s.product_id 
        WHERE p.deleted = 0 
        ${ew.customSqlSegment}
    """)
    IPage<ProductDTO> getProductWithDetails(Page<ProductDTO> page, 
                                          @Param("ew") QueryWrapper<Product> wrapper);
}

// サービス層での使用
@Service
public class ProductService extends ServiceImpl<ProductMapper, Product> {
    public IPage<ProductDTO> getProductDetailsPage(ProductSearchDTO searchDTO) {
        Page<ProductDTO> page = new Page<>(searchDTO.getPageNum(), searchDTO.getPageSize());

        QueryWrapper<Product> wrapper = new QueryWrapper<Product>()
            .like(StringUtils.isNotBlank(searchDTO.getName()), "p.product_name", searchDTO.getName())
            .ge(searchDTO.getMinPrice() != null, "p.price", searchDTO.getMinPrice())
            .orderByDesc("p.create_time");

        return baseMapper.getProductWithDetails(page, wrapper);
    }
}

動的SQLを使いこなすコツ

1. 条件構築パターン

public class QueryHelper {
    public static <T> LambdaQueryWrapper<T> buildWrapper(Consumer<LambdaQueryWrapper<T>> consumer) {
        LambdaQueryWrapper<T> wrapper = new LambdaQueryWrapper<>();
        consumer.accept(wrapper);
        return wrapper;
    }
}

// 使用例
@Service
public class ProductService extends ServiceImpl<ProductMapper, Product> {
    public List<Product> searchProducts(ProductSearchDTO searchDTO) {
        return list(QueryHelper.buildWrapper(w -> {
            // 検索条件を関数型で構築
            Optional.ofNullable(searchDTO.getName())
                .ifPresent(name -> w.like(Product::getName, name));

            Optional.ofNullable(searchDTO.getCategoryId())
                .ifPresent(categoryId -> w.eq(Product::getCategoryId, categoryId));

            Optional.ofNullable(searchDTO.getStatus())
                .ifPresent(status -> w.eq(Product::getStatus, status));
        }));
    }
}

2. 複雑な検索条件の実装

@Service
public class ProductService extends ServiceImpl<ProductMapper, Product> {
    // ネストされた条件の例
    public List<Product> advancedSearch(ProductSearchDTO searchDTO) {
        return list(new LambdaQueryWrapper<Product>()
            .nested(w -> 
                w.like(Product::getName, searchDTO.getName())
                 .or()
                 .like(Product::getDescription, searchDTO.getKeyword())
            )
            .and(w ->
                w.ge(Product::getPrice, searchDTO.getMinPrice())
                 .le(Product::getPrice, searchDTO.getMaxPrice())
            )
            .orderByDesc(Product::getCreateTime));
    }

    // OR条件の組み合わせ
    public List<Product> searchByMultipleCategories(List<Long> categoryIds, 
                                                  List<String> tags) {
        return list(new LambdaQueryWrapper<Product>()
            .and(w -> 
                w.in(Product::getCategoryId, categoryIds)
                 .or()
                 .in(Product::getTags, tags)
            )
            .orderByDesc(Product::getCreateTime));
    }
}

パフォーマンスを最適化するためのヒント

1. バッチ処理の最適化

@Service
@Transactional(rollbackFor = Exception.class)
public class BatchService {
    private static final int BATCH_SIZE = 1000;

    @Autowired
    private ProductService productService;

    public void batchInsert(List<Product> products) {
        // リストを適切なサイズに分割して処理
        Lists.partition(products, BATCH_SIZE).forEach(batch -> {
            productService.saveBatch(batch, BATCH_SIZE);
        });
    }

    // バッチ更新の最適化
    public void batchUpdate(List<Product> products) {
        Lists.partition(products, BATCH_SIZE).forEach(batch -> {
            productService.updateBatchById(batch, BATCH_SIZE);
        });
    }
}

2. クエリパフォーマンスの最適化

@Service
public class ProductService extends ServiceImpl<ProductMapper, Product> {
    // 必要なカラムのみを取得
    public List<ProductSimpleDTO> getProductList() {
        return list(new LambdaQueryWrapper<Product>()
            .select(Product::getId, Product::getName, Product::getPrice)
            .orderByDesc(Product::getCreateTime));
    }

    // EXISTS句の活用
    public boolean hasAvailableProducts(Long categoryId) {
        return exists(new LambdaQueryWrapper<Product>()
            .eq(Product::getCategoryId, categoryId)
            .eq(Product::getStatus, ProductStatus.AVAILABLE));
    }

    // COUNT最適化
    public long countAvailableProducts() {
        return count(new LambdaQueryWrapper<Product>()
            .select(Product::getId) // IDのみをカウント
            .eq(Product::getStatus, ProductStatus.AVAILABLE));
    }
}

3. キャッシュ戦略

@Service
@CacheConfig(cacheNames = "product")
public class ProductService extends ServiceImpl<ProductMapper, Product> {

    @Cacheable(key = "#id")
    @Override
    public Product getById(Serializable id) {
        return super.getById(id);
    }

    @CacheEvict(key = "#id")
    @Override
    public boolean removeById(Serializable id) {
        return super.removeById(id);
    }

    @CachePut(key = "#product.id")
    @Override
    public boolean updateById(Product product) {
        return super.updateById(product);
    }

    // キャッシュを使用した高速な存在チェック
    @Cacheable(key = "'exists:' + #id")
    public boolean existsById(Long id) {
        return exists(new LambdaQueryWrapper<Product>()
            .eq(Product::getId, id));
    }
}

パフォーマンス最適化のベストプラクティス

  1. インデックス戦略
    • 検索条件によく使用されるカラムにインデックスを作成
    • 複合インデックスの適切な設計
    • インデックスの効果を確認するためのEXPLAIN分析
  2. メモリ使用量の最適化
    • ページネーションの適切な使用
    • 大量データ処理時のストリーム活用
    • 適切なバッチサイズの設定
  3. クエリの最適化
    • 必要なカラムのみを選択
    • JOINの最小化
    • サブクエリの適切な使用

現場で使える!トラブルシューティングとTips

よくあるエラーと解決方法

1. マッピング関連のエラー

問題1: Unknown Column Error

org.apache.ibatis.exceptions.PersistenceException: Error attempting to get column 'user_name' from result set. 
Cause: java.sql.SQLSyntaxErrorException: Unknown column 'user_name' in 'field list'

解決策:

// 正しいマッピング設定
@TableName("t_user")
public class User {
    // カラム名が異なる場合は明示的にマッピング
    @TableField("user_name")
    private String userName;

    // キャメルケース→スネークケースの自動変換を無効化する場合
    @TableField(value = "user_name", strategy = FieldStrategy.NOT_NULL)
    private String userName;
}

問題2: Type Handling Error

org.apache.ibatis.type.TypeException: Error setting non null for parameter #1 with JdbcType null

解決策:

// カスタムタイプハンドラーの実装
@MappedTypes(YourEnum.class)
public class YourEnumTypeHandler extends BaseTypeHandler<YourEnum> {
    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, YourEnum parameter, JdbcType jdbcType) 
            throws SQLException {
        ps.setString(i, parameter.getCode());
    }

    @Override
    public YourEnum getNullableResult(ResultSet rs, String columnName) throws SQLException {
        return YourEnum.fromCode(rs.getString(columnName));
    }
    // 他のメソッドも実装
}

// application.ymlでの設定
mybatis-plus:
  type-handlers-package: com.your.package.handlers

2. パフォーマンス関連の問題

問題1: N+1問題

// 問題のあるコード
@Service
public class OrderService extends ServiceImpl<OrderMapper, Order> {
    @Autowired
    private UserService userService;

    public List<OrderDTO> getOrders() {
        // N+1問題を引き起こす実装
        return list().stream()
            .map(order -> {
                OrderDTO dto = new OrderDTO();
                // 各オーダーに対して個別にユーザー情報を取得
                User user = userService.getById(order.getUserId()); // N回のクエリ発生
                dto.setUserName(user.getName());
                return dto;
            })
            .collect(Collectors.toList());
    }
}

// 最適化された実装
@Mapper
public interface OrderMapper extends BaseMapper<Order> {
    @Select("""
        SELECT o.*, u.name as user_name 
        FROM t_order o 
        LEFT JOIN t_user u ON o.user_id = u.id
    """)
    List<OrderDTO> getOrdersWithUser();
}

問題2: メモリ不足

// 問題のあるコード
public void processLargeData() {
    // 全データを一度にメモリに読み込む
    List<Product> products = list(); // メモリ不足の可能性

    products.forEach(this::processProduct);
}

// 最適化された実装
public void processLargeData() {
    // ページング処理による最適化
    long count = count();
    int pageSize = 1000;
    int pages = (int) Math.ceil(count / (double) pageSize);

    for (int i = 1; i <= pages; i++) {
        Page<Product> page = new Page<>(i, pageSize);
        page(page).getRecords().forEach(this::processProduct);
    }
}

デバッグのコツと効率的なログ設定

1. SQLログの設定

# application.yml
mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

logging:
  level:
    com.baomidou.mybatisplus: debug
    com.your.mapper.package: debug

2. カスタムインターセプターによるSQL監視

@Intercepts({
    @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
    @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, 
              RowBounds.class, ResultHandler.class})
})
@Component
public class SqlMonitorInterceptor implements Interceptor {
    private static final Logger log = LoggerFactory.getLogger(SqlMonitorInterceptor.class);

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        long startTime = System.currentTimeMillis();
        try {
            return invocation.proceed();
        } finally {
            long endTime = System.currentTimeMillis();
            long sqlCost = endTime - startTime;

            MappedStatement statement = (MappedStatement) invocation.getArgs()[0];
            String sqlId = statement.getId();
            log.info("SQL実行時間 - ID: {}, コスト: {}ms", sqlId, sqlCost);

            if (sqlCost > 1000) {
                log.warn("スロークエリ検出 - ID: {}, コスト: {}ms", sqlId, sqlCost);
            }
        }
    }
}

3. デバッグモードでの開発効率化

@Slf4j
@Service
public class DebugService {
    // デバッグ用のユーティリティメソッド
    public void debugQuery(LambdaQueryWrapper<Product> wrapper) {
        String sql = SqlUtils.showSql(wrapper);
        log.debug("生成されたSQL: {}", sql);

        // パラメータの出力
        Map<String, Object> params = wrapper.getParamNameValuePairs();
        log.debug("パラメータ: {}", params);
    }
}

本番環境での運用ベストプラクティス

1. パフォーマンスモニタリング

@Aspect
@Component
@Slf4j
public class PerformanceMonitorAspect {

    @Around("execution(* com.your.package.mapper.*Mapper.*(..))")
    public Object monitorMapperMethods(ProceedingJoinPoint joinPoint) throws Throwable {
        String methodName = joinPoint.getSignature().getName();
        long startTime = System.currentTimeMillis();

        try {
            return joinPoint.proceed();
        } finally {
            long executionTime = System.currentTimeMillis() - startTime;
            if (executionTime > 500) {
                log.warn("スロークエリ検出: {} - 実行時間: {}ms", methodName, executionTime);
                // メトリクス収集システムへの記録
                MetricsCollector.recordSlowQuery(methodName, executionTime);
            }
        }
    }
}

2. 本番環境での設定最適化

# application-prod.yml
mybatis-plus:
  configuration:
    # キャッシュを有効化
    cache-enabled: true
    # ログ出力を必要最小限に
    log-impl: org.apache.ibatis.logging.nologging.NoLoggingImpl
  global-config:
    db-config:
      # バッチサイズの最適化
      batch-size: 1000
      # IDタイプの設定
      id-type: assign_id
      # 論理削除の設定
      logic-delete-field: deleted
      logic-delete-value: 1
      logic-not-delete-value: 0
    # バナー(起動時のロゴ表示)を無効化
    banner: false

3. セキュリティ対策

// SQLインジェクション対策
@Service
public class SecureService {
    // 安全なLike検索の実装
    public List<Product> searchProducts(String keyword) {
        // キーワードのエスケープ処理
        String escapedKeyword = SqlUtils.escapeSQL(keyword);

        return list(new LambdaQueryWrapper<Product>()
            .like(Product::getName, escapedKeyword));
    }

    // 権限チェックの実装
    @PreAuthorize("hasRole('ADMIN')")
    public void deleteProduct(Long id) {
        removeById(id);
    }
}

運用上の重要なチェックポイント

  1. パフォーマンスモニタリング
    • スロークエリの監視
    • コネクションプールの最適化
    • メモリ使用量の監視
  2. セキュリティ対策
    • SQLインジェクション対策
    • アクセス権限の適切な設定
    • センシティブデータの暗号化
  3. バックアップと復旧
    • 定期的なバックアップ
    • リストア手順の確認
    • 障害時の切り戻し計画
  4. メンテナンス計画
    • インデックスの再構築
    • 統計情報の更新
    • ログローテーション

まとめ:MyBatis Plusで実現する効率的なDB開発

本記事のポイント

  1. MyBatis Plusの主要な利点
    • ボイラープレートコードの大幅な削減
    • 開発生産性の向上
    • 保守性とスケーラビリティの改善
    • 豊富な機能と拡張性
  2. 実装のベストプラクティス
    • アノテーションベースの簡潔な実装
    • Wrapper APIを活用した動的クエリ
    • 適切なページネーション処理
    • パフォーマンスを考慮した実装
  3. 運用時の重要ポイント
    • 効果的なログ設定とモニタリング
    • 適切なキャッシュ戦略
    • セキュリティ対策の実施
    • トラブルシューティング手法の確立

次のステップ

  1. スキルアップの方向性
   // 以下の順序での学習を推奨
   Step 1: 基本的なCRUD操作の習得
   Step 2: 動的クエリビルダーの活用
   Step 3: パフォーマンス最適化手法の実践
   Step 4: 高度な機能(プラグイン開発など)の習得
  1. 実践的な活用シーン
    • マイクロサービスでのデータアクセス層実装
    • レガシーシステムのリファクタリング
    • 大規模データ処理システムの開発
    • API開発プロジェクトでの活用
  2. 参考リソース

導入効果の指標

項目従来の実装MyBatis Plus使用時
CRUD実装時間2-3時間30分以内
コード行数100-200行20-30行
メンテナンスコスト
学習コスト低-中
パフォーマンス標準最適化済み

最後に

MyBatis Plusは、Java開発者にとって強力な武器となります。本記事で解説した内容を実践することで、以下のような効果が期待できます:

  • 開発時間の50%以上削減
  • コードの保守性向上
  • チーム全体の生産性向上
  • 高品質なデータアクセス層の実現

特に重要なのは、単なる機能の使用だけでなく、適切な設計とベストプラクティスの適用です。これにより、長期的なメンテナンス性とパフォーマンスの両立が可能となります。

今後のバージョンアップでさらなる機能追加が期待される中、基本的な概念と設計思想を理解することで、新機能もスムーズに活用できるでしょう。

Quick Reference

// 基本実装テンプレート
@Service
public class YourService extends ServiceImpl<YourMapper, YourEntity> {
    // 基本的なCRUD操作
    public void basicOperations() {
        // 保存
        save(entity);

        // 検索
        getById(id);

        // 更新
        updateById(entity);

        // 削除
        removeById(id);
    }

    // 応用操作
    public void advancedOperations() {
        // 条件検索
        list(new LambdaQueryWrapper<YourEntity>()
            .eq(YourEntity::getField, value));

        // ページネーション
        page(new Page<>(1, 10));
    }
}

このコードテンプレートを起点に、プロジェクトの要件に応じて機能を拡張していくことで、効率的な開発が可能となります。

MyBatis Plusの導入は、モダンなJava開発における重要な一歩となるでしょう。本記事の内容を参考に、ぜひ実践してみてください。