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>/<#if controllerMappingHyphenStyle>${controllerMappingHyphen}<#else>${table.entityPath}#if>")
@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};
#list>
<#if springdoc>
import io.swagger.v3.oas.annotations.media.Schema;
<#elseif swagger>
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
#if>
<#if entityLombokModel>
import lombok.Data;
#if>
/**
* <p>
* ${table.comment!} ドメインモデル
* </p>
*
* @author ${author}
* @since ${date}
*/
<#if entityLombokModel>
@Data
#if>
<#if table.convert>
@TableName("${schemaName}${table.name}")
#if>
<#if springdoc>
@Schema(name = "${entity}", description = "${table.comment!}")
<#elseif swagger>
@ApiModel(value = "${entity}オブジェクト", description = "${table.comment!}")
#if>
public class ${entity} <#if superEntityClass??>extends ${superEntityClass}#if> {
<#if entitySerialVersionUID>
private static final long serialVersionUID = 1L;
#if>
<#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>
#if>
<#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}")
#if>
<#elseif field.fill??>
<#if field.convert>
@TableField(value = "${field.annotationColumnName}", fill = FieldFill.${field.fill})
<#else>
@TableField(fill = FieldFill.${field.fill})
#if>
<#elseif field.convert>
@TableField("${field.annotationColumnName}")
#if>
<#if field.versionField>
@Version
#if>
<#if field.logicDeleteField>
@TableLogic
#if>
private ${field.propertyType} ${field.propertyName};
#list>
}
`infraMapper.java.ftl`
package ${package.Mapper};
import ${package.Entity}.${entity};
<#if mapperAnnotationClass??>
import ${mapperAnnotationClass.name};
#if>
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* <p>
* ${table.comment!} Mapperインターフェース
* </p>
*
* @author ${author}
* @since ${date}
*/
<#if mapperAnnotationClass??>
@${mapperAnnotationClass.simpleName}
#if>
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}"/>
#if>
<!-- ${entity}の基本結果マッピング -->
<resultMap id="${entity}ResultMap" type="${package.Entity}.${entity}">
<#list table.fields as field>
<#if field.keyFlag>
<id column="${field.name}" property="${field.propertyName}" />
#if>
#list>
<#list table.fields as field>
<#if !field.keyFlag>
<result column="${field.name}" property="${field.propertyName}" />
#if>
#list>
</resultMap>
<!-- 全カラムのリスト -->
<sql id="All_Columns">
<#list table.fields as field>
${r"`"}${field.columnName}${r"`"}<#if field_has_next>,#if>
#list>
</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>,#if>
#if>
#list>
)
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>,#if>
<#else>
${r"#{param."}${field.propertyName}${r"}"}<#if field_has_next>,#if>
#if>
#list>
)
</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>
#if>
#list>
<#-- 更新日時を自動設定 -->
${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};
#if>
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}#if> {
@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