Spring Boot と MyBatis-Plus を活用したコード自動生成

Spring BootにおけるMyBatis-Plusコード生成器の利用

MyBatis-Plusのコード生成機能は、従来のMyBatisジェネレータと比較して、Controller層やService層のコードも生成できる点で優れています。また、より豊富な設定オプションを提供し、Freemarkerテンプレートのカスタマイズや追加を通じて、開発効率を大幅に向上させることが可能です。

設定と実装手順

プロジェクトの構成例:

  • `CustomTemplateEngine` クラスは `FreemarkerTemplateEngine` を継承し、カスタムテンプレートのパスを定義します。
  • `templates.generator` ディレクトリには、独自のFreemarkerテンプレートファイルを配置します。
  • `CodeGenerationRunner` クラスはコード生成プロセスを実行するためのメインクラスです。

1. プロジェクト依存関係の追加

`pom.xml` ファイルに、MyBatis-Plusとコード生成器、およびテンプレートエンジン(例: VelocityまたはFreemarker)の依存関係を追加します。ここではFreemarkerを使用します。

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId<
            <version>3.5.7</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>3.5.6</version>
        </dependency>
        <dependency>
            <groupId>org.freemarker</groupId>
            <artifactId>freemarker</artifactId>
            <version>2.3.32</version>
        </dependency>
        <!-- 必要に応じてSwagger/OpenAPIの依存関係を追加 -->
        <dependency>
            <groupId>org.springdoc</groupId>
            <artifactId>springdoc-openapi-ui</artifactId>
            <version>1.7.0</version>
        </dependency>

2. コード生成実行クラスの作成

テストディレクトリ内に、ファイル生成を実行するためのクラスを作成します。データベース接続情報、出力ディレクトリ、パッケージ名などを適切に設定してください。

package dev.sample.codegen;

import com.baomidou.mybatisplus.generator.FastAutoGenerator;
import com.baomidou.mybatisplus.generator.config.OutputFile;
import com.baomidou.mybatisplus.generator.config.builder.CustomFile;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
import dev.sample.codegen.engine.CustomPathFreemarkerEngine; // 後述のカスタムエンジン

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.Collections;
import java.util.ArrayList;
import java.util.List;

@SpringBootTest
public class CodeGenerationRunner {

    @Test
    void generateMyBatisPlusCode() {
        // FastAutoGenerator を使用してコード生成器を迅速に設定
        FastAutoGenerator.create("jdbc:mysql://localhost:3306/your_database?serverTimezone=Asia/Tokyo&useUnicode=true&characterEncoding=utf-8&useSSL=false&allowPublicKeyRetrieval=true",
                                "username", "password")
                .globalConfig(builder -> {
                    builder.author("YourName") // 作者名を設定
                            .outputDir("src/main/java") // 生成物の出力ディレクトリ
                            .enableSwagger() // Swagger (OpenAPI) モードを有効化
                            .commentDate("yyyy-MM-dd"); // コメントの日付フォーマット
                })
                .packageConfig(builder -> {
                    builder.parent("dev.sample.codegen") // 親パッケージ名
                            .entity("domain.model") // エンティティクラスのパッケージ名
                            .mapper("infra.mapper") // Mapperインターフェースのパッケージ名
                            .service("app.service") // Serviceインターフェースのパッケージ名
                            .serviceImpl("app.service.impl") // Service実装クラスのパッケージ名
                            .controller("api.controller") // Controllerクラスのパッケージ名
                            .pathInfo(Collections.singletonMap(OutputFile.xml,
                                    "src/main/resources/mapper")); // Mapper XMLファイルの出力パス
                })
                .strategyConfig(builder -> {
                    builder.addInclude("product_item", "order_detail") // 生成対象のテーブル名
                            .entityBuilder()
                                .enableLombok() // Lombokを有効化
                                .enableTableFieldAnnotation() // フィールドに @TableField アノテーションを追加
                                .logicDeleteColumnName("is_deleted") // 論理削除カラム名
                                .naming(NamingStrategy.underline_to_camel) // テーブル名をキャメルケースに変換
                                .columnNaming(NamingStrategy.underline_to_camel) // カラム名をキャメルケースに変換
                                .addSuperEntityColumns("created_at", "updated_at") // 共通フィールドを追加
                                .formatFileName("%sEntity") // エンティティファイル名のフォーマット
                            .mapperBuilder()
                                .enableBaseResultMap() // BaseResultMapを生成
                                .enableBaseColumnList() // BaseColumnListを生成
                                .formatMapperFileName("%sRepository") // Mapperファイル名のフォーマット
                                .formatXmlFileName("%sMapper") // XMLファイル名のフォーマット
                            .serviceBuilder()
                                .formatServiceFileName("%sAppService") // Serviceインターフェース名のフォーマット
                                .formatServiceImplFileName("%sAppServiceImpl") // Service実装クラス名のフォーマット
                            .controllerBuilder()
                                .enableRestStyle() // RESTfulスタイルを有効化
                                .formatFileName("%sApi"); // Controllerファイル名のフォーマット
                })
                .injectionConfig(consumer -> {
                    // カスタムファイルを生成する設定例
                    List<CustomFile> customFiles = new ArrayList<>();
                    customFiles.add(new CustomFile.Builder()
                            .fileName("CommonResponse.java")
                            .templatePath("/templates/generator/commonResponse.java.ftl")
                            .build());
                    consumer.customFile(customFiles);
                })
                // カスタムテンプレートエンジンを使用
                .templateEngine(new CustomPathFreemarkerEngine())
                .execute();
    }
}

3. カスタムテンプレートエンジンの実装

特定のカスタムファイル(例: 共通レスポンスクラス)をデフォルトとは異なるパスに出力するために、`FreemarkerTemplateEngine`を拡張したクラスを作成します。

package dev.sample.codegen.engine;

import com.baomidou.mybatisplus.core.toolkit.StringPool;
import com.baomidou.mybatisplus.generator.config.OutputFile;
import com.baomidou.mybatisplus.generator.config.builder.CustomFile;
import com.baomidou.mybatisplus.generator.config.po.TableInfo;
import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;
import org.apache.commons.lang3.StringUtils;

import javax.validation.constraints.NotNull;
import java.io.File;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.function.Function;

public class CustomPathFreemarkerEngine extends FreemarkerTemplateEngine {

    public static final String[] UTILITY_FILES = {"commonresponse.java"};

    @Override
    protected void outputCustomFile(@NotNull List<CustomFile> customFiles, @NotNull TableInfo tableInfo, @NotNull Map<String, Object> objectMap) {
        String entityName = tableInfo.getEntityName();
        String parentPath = getPathInfo(OutputFile.parent);

        customFiles.forEach(file -> {
            // 特定のユーティリティファイルを専用のパスに出力
            boolean isUtilityFile = false;
            for (String utilFile : UTILITY_FILES) {
                if (utilFile.equals(file.getFileName().toLowerCase(Locale.ENGLISH))) {
                    isUtilityFile = true;
                    break;
                }
            }

            if (isUtilityFile) {
                // 例: src/main/java/dev/sample/codegen/util に出力
                String fileName = String.format(parentPath + File.separator + "util" + File.separator + "%s", file.getFileName());
                outputFile(new File(fileName), objectMap, file.getTemplatePath(), file.isFileOverride());
            } else {
                // その他のカスタムファイルは通常のロジックで出力
                String filePath = StringUtils.isNotBlank(file.getFilePath()) ? file.getFilePath() : parentPath;
                if (StringUtils.isNotBlank(file.getPackageName())) {
                    filePath = filePath + File.separator + file.getPackageName().replaceAll("\\.", StringPool.BACK_SLASH + File.separator);
                }
                Function<TableInfo, String> formatNameFunction = file.getFormatNameFunction();
                String fileName = filePath + File.separator + (null != formatNameFunction ? formatNameFunction.apply(tableInfo) : entityName) + file.getFileName();
                outputFile(new File(fileName), objectMap, file.getTemplatePath(), file.isFileOverride());
            }
        });
    }
}

4. カスタムテンプレートコード

以下のテンプレートは、基本的なCRUD操作と共通レスポンスクラスに特化して変更されています。Controller層では、`CommonResponse`クラスを汎用的なAPIレスポンスとして利用します。SQLの新規挿入では、主キーを`id`とし、自動インクリメントを想定しています。

`apiController.java.ftl`

package ${package.Controller};

import ${package.Entity}.${entity};
import ${package.Service}.${table.serviceName};
import ${package.Parent}.util.CommonResponse; // カスタム共通レスポンンスクラス
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;

/**
 * <p>
 * ${table.comment!} APIコントローラ
 * </p>
 *
 * @author ${author}
 * @since ${date}
 */
@RestController
@RequestMapping("<#if package.ModuleName?? && package.ModuleName != "">/${package.ModuleName}/<#if controllerMappingHyphenStyle>${controllerMappingHyphen}<#else>${table.entityPath}")
@Tag(name = "${table.comment!}管理", description = "${table.comment!}関連API")
public class ${table.controllerName} {

    @Autowired
    private ${table.serviceName} ${table.serviceName?uncap_first}Impl;

    @Operation(summary = "${table.comment!}詳細取得")
    @GetMapping(value = "/{id}")
    public CommonResponse<${entity}> getDetail(@PathVariable Long id) {
        ${entity} data = ${table.serviceName?uncap_first}Impl.retrieveById(id);
        return CommonResponse.success(data);
    }

    @Operation(summary = "${table.comment!}新規登録")
    @PostMapping(value = "")
    public CommonResponse<Void> create(@Valid @RequestBody ${entity} param) {
        ${table.serviceName?uncap_first} register(param);
        return CommonResponse.success();
    }

    @Operation(summary = "${table.comment!}情報更新")
    @PutMapping(value = "")
    public CommonResponse<Void> update(@Valid @RequestBody ${entity} param) {
        ${table.serviceName?uncap_first}Impl.updateRecord(param);
        return CommonResponse.success();
    }

    @Operation(summary = "${table.comment!}削除")
    @DeleteMapping(value = "/{id}")
    public CommonResponse<Void> delete(@PathVariable Long id) {
        ${table.serviceName?uncap_first}Impl.removeById(id);
        return CommonResponse.success();
    }
}

`domainModel.java.ftl`

package ${package.Entity};

<#list table.importPackages as pkg>
import ${pkg};

<#if springdoc>
import io.swagger.v3.oas.annotations.media.Schema;
<#elseif swagger>
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;

<#if entityLombokModel>
import lombok.Data;


/**
 * <p>
 * ${table.comment!} ドメインモデル
 * </p>
 *
 * @author ${author}
 * @since ${date}
 */
<#if entityLombokModel>
@Data

<#if table.convert>
@TableName("${schemaName}${table.name}")

<#if springdoc>
@Schema(name = "${entity}", description = "${table.comment!}")
<#elseif swagger>
@ApiModel(value = "${entity}オブジェクト", description = "${table.comment!}")

public class ${entity} <#if superEntityClass??>extends ${superEntityClass} {

    <#if entitySerialVersionUID>
    private static final long serialVersionUID = 1L;
    

<#list table.fields as field>
    <#if field.comment!?length gt 0>
        <#if springdoc>
    @Schema(description = "${field.comment}")
        <#elseif swagger>
    @ApiModelProperty("${field.comment}")
        <#else>
    /**
     * ${field.comment}
     */
        
    
    <#if field.keyFlag>
        <#if field.keyIdentityFlag>
    @TableId(value = "${field.annotationColumnName}", type = IdType.AUTO)
        <#elseif idType??>
    @TableId(value = "${field.annotationColumnName}", type = IdType.${idType})
        <#elseif field.convert>
    @TableId("${field.annotationColumnName}")
        
    <#elseif field.fill??>
        <#if field.convert>
    @TableField(value = "${field.annotationColumnName}", fill = FieldFill.${field.fill})
        <#else>
    @TableField(fill = FieldFill.${field.fill})
        
    <#elseif field.convert>
    @TableField("${field.annotationColumnName}")
    
    <#if field.versionField>
    @Version
    
    <#if field.logicDeleteField>
    @TableLogic
    
    private ${field.propertyType} ${field.propertyName};

}

`infraMapper.java.ftl`

package ${package.Mapper};

import ${package.Entity}.${entity};
<#if mapperAnnotationClass??>
import ${mapperAnnotationClass.name};

import com.baomidou.mybatisplus.core.mapper.BaseMapper;

/**
 * <p>
 * ${table.comment!} Mapperインターフェース
 * </p>
 *
 * @author ${author}
 * @since ${date}
 */
<#if mapperAnnotationClass??>
@${mapperAnnotationClass.simpleName}

public interface ${table.mapperName} extends BaseMapper<${entity}> {

    /**
     * 主キーで${table.comment!}を取得します。
     * @param id 主キーID
     * @return 該当するエンティティ
     */
    ${entity} selectRecordById(Long id);

    /**
     * ${table.comment!}を新規挿入します。
     * @param entity 挿入するエンティティデータ
     */
    void insertRecord(${entity} entity);

    /**
     * ${table.comment!}情報を更新します。
     * @param entity 更新するエンティティデータ
     */
    void updateRecord(${entity} entity);

    /**
     * 主キーで${table.comment!}を削除します。
     * @param id 削除する主キーID
     */
    void deleteRecordById(Long id);
}

`mapperXml.xml.ftl`

<?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="${package.Mapper}.${table.mapperName}">

<#if enableCache>
    <!-- 二次キャッシュを有効化 -->
    <cache type="${cacheClassName}"/>


    <!-- ${entity}の基本結果マッピング -->
    <resultMap id="${entity}ResultMap" type="${package.Entity}.${entity}">
<#list table.fields as field>
<#if field.keyFlag>
        <id column="${field.name}" property="${field.propertyName}" />


<#list table.fields as field>
<#if !field.keyFlag>
        <result column="${field.name}" property="${field.propertyName}" />


    </resultMap>

    <!-- 全カラムのリスト -->
    <sql id="All_Columns">
<#list table.fields as field>
        ${r"`"}${field.columnName}${r"`"}<#if field_has_next>,

    </sql>

    <select id="selectRecordById" resultMap="${entity}ResultMap">
        SELECT
            <include refid="All_Columns"/>
        FROM ${table.name}
        WHERE id = ${r"#{id}"}
    </select>

    <insert id="insertRecord" useGeneratedKeys="true" keyProperty="id">
        INSERT INTO ${table.name}
        (
        <#list table.fields as field>
            <#if field.columnName == "id">
            <#else>
            ${r"`"}${field.columnName}${r"`"}<#if field_has_next>,
            
        
        )
        VALUES (
        <#list table.fields as field>
            <#if field.columnName == "id">
            <#elseif field.columnName == "created_at" || field.columnName == "updated_at">
            CURRENT_TIMESTAMP<#if field_has_next>,
            <#else>
            ${r"#{param."}${field.propertyName}${r"}"}<#if field_has_next>,
            
        
        )
    </insert>

    <update id="updateRecord">
        UPDATE ${table.name}
        <set>
        <#list table.fields as field>
            <#if field.columnName != "id" && field.columnName != "created_at">
            <if test="param.${field.propertyName} != null">
                ${r"`"}${field.columnName}${r"`"} = ${r"#{param."}${field.propertyName}${r"}"},
            </if>
            
        
            <#-- 更新日時を自動設定 -->
            ${r"`"}updated_at${r"`"} = CURRENT_TIMESTAMP
        </set>
        WHERE id = ${r"#{param.id}"}
    </update>

    <delete id="deleteRecordById">
        DELETE FROM ${table.name} WHERE id = ${r"#{id}"}
    </delete>
</mapper>

`commonResponse.java.ftl`

package ${package.Parent}.util;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

/**
 * <p>
 * API共通レスポンスユーティリティクラス
 * </p>
 *
 * @author ${author}
 * @since ${date}
 */
@Getter
@Setter
@ToString
public class CommonResponse<T> {
    public static final int CODE_SUCCESS = 200;
    public static final int CODE_FAILURE = 500;

    /** HTTPステータスコード */
    private int status;
    /** メッセージ */
    private String message;
    /** レスポンスデータ */
    private T payload;

    public CommonResponse() {
        this.status = CODE_SUCCESS;
        this.message = "処理成功";
    }

    public CommonResponse(T payload) {
        this.status = CODE_SUCCESS;
        this.message = "処理成功";
        this.payload = payload;
    }

    public CommonResponse(int status, String message) {
        this.status = status;
        this.message = message;
    }

    /**
     * 成功レスポンスを生成します。
     */
    public static <T> CommonResponse<T> success(){
        return new CommonResponse<>();
    }

    /**
     * データを含む成功レスポンスを生成します。
     * @param data レスポンスに含めるデータ
     */
    public static <T> CommonResponse<T> success(T data){
        return new CommonResponse<>(data);
    }

    /**
     * 失敗レスポンスを生成します。
     * @param msg エラーメッセージ
     */
    public static <T> CommonResponse<T> failure(String msg){
        return new CommonResponse<>(CODE_FAILURE, msg);
    }
}

`appService.java.ftl`

package ${package.Service};

import ${package.Entity}.${entity};
import ${superServiceClassPackage};

/**
 * <p>
 * ${table.comment!} アプリケーションサービスインターフェース
 * </p>
 *
 * @author ${author}
 * @since ${date}
 */
public interface ${table.serviceName} {
    /**
     * 主キーで${table.comment!}の詳細を取得します。
     * @param id 主キー
     * @return ${entity}オブジェクト
     */
    ${entity} retrieveById(Long id);

    /**
     * ${table.comment!}を新規登録します。
     * @param param 登録パラメータ
     */
    void register(${entity} param);

    /**
     * ${table.comment!}情報を更新します。
     * @param param 更新パラメータ
     */
    void updateRecord(${entity} param);

    /**
     * 主キーで${table.comment!}を削除します。
     * @param id 削除対象の主キー
     */
    void removeById(Long id);
}

`appServiceImpl.java.ftl`

package ${package.ServiceImpl};

import ${package.Entity}.${entity};
import ${package.Mapper}.${table.mapperName};
<#if generateService>
import ${package.Service}.${table.serviceName};

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * <p>
 * ${table.comment!} サービス実装クラス
 * </p>
 *
 * @author ${author}
 * @since ${date}
 */
@Service
public class ${table.serviceImplName} <#if generateService> implements ${table.serviceName} {

    @Autowired
    private ${table.mapperName} ${table.mapperName?uncap_first};

    /**
     * 主キーで${table.comment!}の詳細を取得します。
     * @param id 主キー
     * @return ${entity}オブジェクト
     */
    @Override
    public ${entity} retrieveById(Long id) {
        return ${table.mapperName?uncap_first}.selectRecordById(id);
    }

    /**
     * ${table.comment!}を新規登録します。
     * @param param 登録パラメータ
     */
    @Override
    public void register(${entity} param) {
        ${table.mapperName?uncap_first}.insertRecord(param);
    }

    /**
     * ${table.comment!}情報を更新します。
     * @param param 更新パラメータ
     */
    @Override
    public void updateRecord(${entity} param) {
        ${table.mapperName?uncap_first}.updateRecord(param);
    }

    /**
     * 主キーで${table.comment!}を削除します。
     * @param id 削除対象の主キー
     */
    @Override
    public void removeById(Long id) {
        ${table.mapperName?uncap_first}.deleteRecordById(id);
    }
}

生成されるコード構造例:

上記の設定とテンプレートを使用すると、以下のようなディレクトリ構造とファイルが生成されます。

src/main/java
├── dev/sample/codegen
│   ├── api/controller
│   │   ├── OrderDetailApi.java
│   │   └── ProductItemApi.java
│   ├── app/service
│   │   ├── OrderDetailAppService.java
│   │   └── ProductItemAppService.java
│   │   └── impl
│   │       ├── OrderDetailAppServiceImpl.java
│   │       └── ProductItemAppServiceImpl.java
│   ├── domain/model
│   │   ├── OrderDetailEntity.java
│   │   └── ProductItemEntity.java
│   ├── infra/mapper
│   │   ├── OrderDetailRepository.java
│   │   └── ProductItemRepository.java
│   └── util
│       └── CommonResponse.java
└── resources
    └── mapper
        ├── OrderDetailMapper.xml
        └── ProductItemMapper.xml

タグ: Spring Boot MyBatis-Plus コード生成 Freemarker Java

9月16日 15:20 投稿