mybatis-plus-patterns について
mybatis-plus-patterns は、Java プロジェクトで MyBatis-Plus ORM を使うためのコーディングスキルです。型安全なクエリ、ページング、同時更新制御、論理削除、安全な更新パターンを扱います。
主な機能
- 01LambdaQueryWrapper による型安全なクエリ
- 02ページング設定と Page パターン
- 03楽観ロックと論理削除
- 04フィールドの自動入力
- 05ActiveRecord と Mapper の使い分け
- 06全件更新・削除の防止
活用シーン
- 01文字列のフィールド名を使わず型安全なクエリを作成
- 02ページングを設定し PageQuery の結果をラップ
- 03同時更新による上書きを回避
- 04条件なしの更新や削除を誤って実行するのを防止
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ full-stack-skills/ java-skills --skill mybatis-plus-patternsインストール後、Claude Code で/mybatis-plus-patternsを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
agents/openai.yamlexamples/mybatis-plus-realworld.mdreferences/mybatis-plus-resources.md詳細
MyBatis-Plus 拡張 ORM
出典:https://baomidou.com/
GitHub:https://github.com/baomidou/mybatis-plus
対応範囲
✅ 得意分野
- LambdaQueryWrapper — 型安全なクエリ(文字列のフィールド名を記述する必要はありません)
- ページネーションプラグイン — PaginationInnerInterceptor の設定と使用方法
- 楽観ロック — @Version + OptimisticLockerInnerInterceptor
- 論理削除 — @TableLogic のグローバル設定とテーブル単位の設定
- 自動入力 — @TableField(fill) + MetaObjectHandler の実装
- ActiveRecord と Mapper の比較 — Entity extends Model と独立した Mapper インターフェースの選択
- 全件操作の防止 — BlockAttackInnerInterceptor
- 汎用ページネーションのラッパー — PageQuery → PageDTO のジェネリックパターン
❌ 対象外
- MyBatis XML/ResultMap/動的 SQL → mybatis-patterns
- MyBatis-Plus Generator によるコード生成 → mybatis-plus-generator スキルがすでにあります
- MyBatis-Plus のデータ権限 → プラグイン体系であり、LLM でよくあるニーズではありません
最小限の設定
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor()); // ← 推荐
return interceptor;
}
}
mybatis-plus:
global-config:
db-config:
id-type: auto # 主键自增
logic-delete-field: deleted # 逻辑删除字段
logic-delete-value: 1
logic-not-delete-value: 0
insert-strategy: not_null
update-strategy: not_null
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 开发环境
LLM がよく犯す間違い
| # | 間違い | 正しい方法 |
|---|---|---|
| 1 | eq("user_name", username) のように文字列のフィールド名を使う | eq(User::getUsername, username) のように Lambda で型安全に指定する |
| 2 | PageHelper だけを使い、ページネーションしない | IPage + PaginationInnerInterceptor を使う |
| 3 | 楽観ロックを設定せず、同時更新で上書きが発生する | @Version + OptimisticLockerInnerInterceptor を使う |
| 4 | UPDATE SET is_deleted = 1 で手動削除する | @TableLogic により自動的に UPDATE に変換する |
| 5 | ActiveRecord モードを知らない | extends Model → entity.insert/updateById/selectById |
| 6 | 全件更新・削除を防止しない | BlockAttackInnerInterceptor が WHERE のない UPDATE/DELETE を阻止する |
| 7 | ページネーションクエリで total/pages を手動計算する | Page が total、pages、records を自動的にラップする |
| 8 | createTime/updateTime を手動設定する | MetaObjectHandler で自動入力する |
| 9 | Wrapper で文字列のフィールド名をハードコードする | Wrappers.lambdaQuery() + LambdaQueryWrapper を使う |
コアパターン
パターン 1: LambdaQueryWrapper(常に Lambda を使う)
// ✅ 推荐:LambdaQueryWrapper(类型安全)
LambdaQueryWrapper<User> wrapper = Wrappers.lambdaQuery();
wrapper.eq(User::getUsername, "张三")
.ge(User::getAge, 18)
.in(User::getStatus, List.of(1, 2))
.orderByDesc(User::getCreateTime)
.last("limit 10"); // 慎用 last——与分页冲突
List<User> list = userMapper.selectList(wrapper);
// ✅ 简化:lambdaQuery() 链式调用
List<User> adults = userService.lambdaQuery()
.ge(User::getAge, 18)
.in(User::getStatus, Arrays.asList(1, 2))
.orderByDesc(User::getCreateTime)
.list();
// ✅ 更新:lambdaUpdate()
userService.lambdaUpdate()
.eq(User::getUsername, "张三")
.set(User::getEmail, "[email protected]")
.update();
// ❌ 避免:QueryWrapper(字符串硬编码,重构时不易发现)
// QueryWrapper<User> wrapper = new QueryWrapper<>();
// wrapper.eq("user_name", "张三"); // 字段名写死,重构时断裂
パターン 2: 汎用ページネーションのラッパー
// 通用分页查询参数
@Data
public class PageQuery {
@Min(1) private Integer pageNo = 1;
@Min(1) @Max(5000) private Integer pageSize = 10;
private String sortBy;
private boolean isAsc = true;
public <T> Page<T> toPage() {
Page<T> page = Page.of(pageNo, pageSize);
if (StrUtil.isNotBlank(sortBy)) {
page.addOrder(new OrderItem(sortBy, isAsc));
}
return page;
}
}
// 通用分页返回
@Data
public class PageDTO<T> {
private Long total;
private Long pages;
private List<T> list;
public static <T> PageDTO<T> of(IPage<T> page) {
PageDTO<T> dto = new PageDTO<>();
dto.setTotal(page.getTotal());
dto.setPages(page.getPages());
dto.setList(page.getRecords());
return dto;
}
}
// 使用
public PageDTO<User> queryUsers(PageQuery query) {
Page<User> page = userService.page(query.toPage(),
Wrappers.lambdaQuery<User>().ge(User::getAge, 18));
return PageDTO.of(page);
}
パターン 3: 楽観ロック + 論理削除 + 自動入力 Entity
@Data
@TableName("sys_user")
public class User {
@TableId(type = IdType.AUTO)
private Long id;
private String username;
private String email;
@Version private Integer version; // ← 乐观锁(更新时自动+1)
@TableLogic private Integer deleted; // ← 逻辑删除(0=正常, 1=删除)
@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());
}
}
パターン 4: ActiveRecord と Mapper の比較
// ActiveRecord: Entity extends Model<T>
@Data
@EqualsAndHashCode(callSuper = true) // ← 必须加
@TableName("sys_user")
public class User extends Model<User> {
@TableId private Long id;
private String username;
}
// 使用
User user = new User();
user.setUsername("张三");
user.insert(); // 直接操作
User found = user.selectById(1L);
user.setEmail("[email protected]");
user.updateById();
user.deleteById();
// Mapper 模式: 独立 Mapper 接口 + IService
public interface UserMapper extends BaseMapper<User> {}
public interface UserService extends IService<User> {}
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {}
// 使用
userService.save(user);
userService.getById(1L);
userService.lambdaQuery().eq(User::getUsername, "张三").list();
パターン 5: 全件更新・削除の防止
// 配置 BlockAttackInnerInterceptor 后:
userService.update(new User().setEmail("[email protected]")); // ❌ 抛出异常
// 必须加 where 条件
userService.lambdaUpdate()
.eq(User::getStatus, "ACTIVE")
.set(User::getEmail, "[email protected]")
.update(); // ✅ 安全
注意点
- LambdaQueryWrapper には、メタデータを生成するアノテーションプロセッサーが必要です — そうでないと、Lambda 式からフィールド名を解決できません
- 楽観ロックの @Version フィールドでサポートされる型は int/Integer/long/Long/Date/LocalDateTime/Timestamp です
- 楽観ロックでは、先にクエリを実行してから更新する必要があります —
selectById → setXxx → updateByIdの順にしないと、version が null のままとなり機能しません - @TableLogic はすべての削除操作に影響します — deleteById/deleteBatchIds はすべて UPDATE に変換されます
- カスタム SQL に論理削除条件は自動追加されません — SQL を手書きする場合は
AND deleted = 0を手動で追加する必要があります - BlockAttackInnerInterceptor は全件更新を阻止します — 開発時は有効にすることを推奨します。本番環境では必要に応じて設定してください
- PageHelper があるとページネーションプラグインが競合する場合があります — どちらか一方を選び、MyBatis-Plus のページネーションを推奨します
- ActiveRecord モードでは Entity が Model を継承する必要があります — そうでないと insert/updateById などのメソッドがありません
- 論理削除後のクエリには deleted=0 が自動的に追加されます — 削除済みデータを検索する必要がある場合は、
eq(User::getDeleted, 1)を手動で指定してください - 自動入力を行う MetaObjectHandler は Spring Bean(@Component)である必要があります — そうでないと機能しません
- strictInsertFill/strictUpdateFill ではフィールドの型を正確に一致させる必要があります — 新しいバージョンの MyBatis-Plus では strict メソッドの使用が推奨されています
- Page のコンストラクター引数は pageNo と pageSize です — offset と limit ではありません
データプライバシー
このスキルはユーザーデータを収集、保存、送信しません。
コメント
Markdown 対応