MyBatis-Flex实战:极简CRUD+高性能分页,吊打传统MyBatis的新一代持久层框架
在Java持久层框架生态中,MyBatis以灵活的SQL控制占据半壁江山,但原生配置繁琐;MyBatis-Plus简化了CRUD却在复杂查询场景下略显厚重。而MyBatis-Flex的出现,完美平衡了"灵活性"与"简洁性",兼具MyBatis的SQL自由度和MyBatis-Plus的CRUD便捷性,更支持动态表名、多租户、数据脱敏等企业级特性,性能更优。
一、环境搭建:5分钟初始化MyBatis-Flex项目
1.1 核心依赖(Maven)
选用最新稳定版本,适配Spring Boot 3.2.5(当前最新稳定版),核心依赖如下,包含MyBatis-Flex、数据源、Lombok、Swagger3等必需组件:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.5</version>
<relativePath/>
</parent>
<dependencies>
<!-- Spring Boot核心 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- MyBatis-Flex核心依赖 -->
<dependency>
<groupId>com.mybatis-flex</groupId>
<artifactId>mybatis-flex-spring-boot-starter</artifactId>
<version>1.9.6</version> <!-- 最新稳定版 -->
</dependency>
<!-- MyBatis-Plus(持久层辅助) -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.5</version> <!-- 最新稳定版 -->
</dependency>
<!-- MySQL驱动 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.3.0</version>
<scope>runtime</scope>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
<!-- Swagger3 -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.2.20</version>
</dependency>
<!-- FastJSON2 -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.47</version>
</dependency>
<!-- Google Guava集合工具 -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>33.2.1-jre</version>
</dependency>
<!-- 单元测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>3.2.5</version>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
<!-- JDK17编译插件 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>17</source>
<target>17</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
</build>
1.2 核心配置(application.yml)
配置数据源、MyBatis-Flex映射路径、Swagger3等,确保配置简洁且符合生产规范:
spring:
# 数据源配置
datasource:
url: jdbc:mysql://localhost:3306/mybatis_flex_demo?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&rewriteBatchedStatements=true
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
# MyBatis-Flex配置
mybatis-flex:
# mapper.xml文件路径
mapper-locations: classpath:mapper/*.xml
# 实体类别名包
type-aliases-package: com.jam.demo.entity
# 全局配置
global-config:
db-config:
# 主键生成策略(MySQL自增)
id-type: auto
# 表名前缀(若有)
table-prefix: t_
# 日志配置(打印SQL)
log:
enable: true
level: debug
# MyBatis-Plus配置(辅助分页)
mybatis-plus:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.jam.demo.entity
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# Swagger3配置
springdoc:
api-docs:
path: /api-docs
swagger-ui:
path: /swagger-ui.html
operationsSorter: method
packages-to-scan: com.jam.demo.controller
model-packages: com.jam.demo.entity
1.3 启动类配置
添加MyBatis-Flex和MyBatis-Plus的 mapper 扫描注解,开启Swagger3:
package com.jam.demo;
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.info.Info;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* 应用启动类
*
* @author ken
*/
@SpringBootApplication
@MapperScan({"com.jam.demo.mapper"}) // 扫描mapper接口
@OpenAPIDefinition(
info = @Info(
title = "MyBatis-Flex实战API",
description = "基于MyBatis-Flex的CRUD、分页、事务等核心功能实战接口",
version = "1.0.0"
)
)
public class MyBatisFlexDemoApplication {
public static void main(String[] args) {
SpringApplication.run(MyBatisFlexDemoApplication.class, args);
}
}
二、核心概念速通:MyBatis-Flex的核心优势
MyBatis-Flex是基于MyBatis开发的增强工具,并非替代MyBatis,而是通过"零XML配置"、"动态SQL生成"、"链式查询"等特性简化开发,核心优势如下:
- 极简CRUD:无需编写XML和Mapper方法,通过BaseMapper和Entity即可完成单表CRUD;
- 灵活的动态SQL:通过QueryWrapper链式构建SQL,支持复杂条件查询,比MyBatis-Plus更简洁;
- 高性能:内置SQL优化机制,支持批量操作、分页优化,性能优于传统MyBatis;
- 轻量无侵入:依赖极少,可与MyBatis、MyBatis-Plus共存,迁移成本低;
- 企业级特性:原生支持多租户、数据脱敏、动态表名、逻辑删除等功能。
核心执行流程如下:
三、实战核心功能:从单表CRUD到分页查询
3.1 准备数据库表(MySQL 8.0)
创建用户表t_user,包含基础字段,SQL可直接执行:
CREATE TABLE `t_user` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`username` varchar(50) NOT NULL COMMENT '用户名',
`password` varchar(100) NOT NULL COMMENT '密码(加密后)',
`nickname` varchar(50) DEFAULT NULL COMMENT '昵称',
`age` int DEFAULT NULL COMMENT '年龄',
`email` varchar(100) DEFAULT NULL COMMENT '邮箱',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`is_deleted` tinyint NOT NULL DEFAULT '0' COMMENT '逻辑删除(0-未删,1-已删)',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
3.2 实体类定义(Entity)
使用MyBatis-Flex注解映射表结构,结合Swagger3注解,字段命名遵循驼峰命名法:
package com.jam.demo.entity;
import com.mybatis.flex.core.BaseEntity;
import com.mybatis.flex.annotation.*;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
/**
* 用户实体类
*
* @author ken
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(description = "用户实体")
@Table(value = "t_user", comment = "用户表")
public class User extends BaseEntity {
/**
* 主键ID
*/
@Schema(description = "主键ID")
@Id(keyType = KeyType.AUTO)
private Long id;
/**
* 用户名
*/
@Schema(description = "用户名", requiredMode = Schema.RequiredMode.REQUIRED)
@Column(comment = "用户名", isUnique = true, isNullable = false)
private String username;
/**
* 密码(加密后)
*/
@Schema(description = "密码(加密后)", requiredMode = Schema.RequiredMode.REQUIRED)
@Column(comment = "密码(加密后)", isNullable = false)
private String password;
/**
* 昵称
*/
@Schema(description = "昵称")
@Column(comment = "昵称")
private String nickname;
/**
* 年龄
*/
@Schema(description = "年龄")
@Column(comment = "年龄")
private Integer age;
/**
* 邮箱
*/
@Schema(description = "邮箱")
@Column(comment = "邮箱")
private String email;
/**
* 创建时间
*/
@Schema(description = "创建时间")
@Column(comment = "创建时间", isInsertFill = true)
@InsertFill(InsertFillStrategy.NOW)
private LocalDateTime createTime;
/**
* 更新时间
*/
@Schema(description = "更新时间")
@Column(comment = "更新时间", isInsertFill = true, isUpdateFill = true)
@InsertFill(InsertFillStrategy.NOW)
@UpdateFill(UpdateFillStrategy.NOW)
private LocalDateTime updateTime;
/**
* 逻辑删除(0-未删,1-已删)
*/
@Schema(description = "逻辑删除(0-未删,1-已删)")
@Column(comment = "逻辑删除(0-未删,1-已删)")
@LogicDelete
private Integer isDeleted;
}
说明:
- 继承
BaseEntity可获得默认的CRUD方法支持; @Table指定表名和注释,@Column映射字段属性,@Id指定主键;@InsertFill和@UpdateFill实现创建时间、更新时间的自动填充;@LogicDelete开启逻辑删除,默认值0为未删除。
3.3 Mapper接口定义
无需编写方法,直接继承MyBatis-Flex的BaseMapper即可:
package com.jam.demo.mapper;
import com.mybatis.flex.core.BaseMapper;
import com.jam.demo.entity.User;
import org.springframework.stereotype.Repository;
/**
* 用户Mapper接口
*
* @author ken
*/
@Repository
public interface UserMapper extends BaseMapper<User> {
}
3.4 Service层实现(含事务处理)
采用编程式事务,实现用户的增删改查核心功能,结合日志打印:
package com.jam.demo.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.google.common.collect.Lists;
import com.mybatis.flex.core.query.QueryWrapper;
import com.jam.demo.entity.User;
import com.jam.demo.mapper.UserMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import java.util.List;
import static com.jam.demo.entity.table.UserTableDef.USER;
/**
* 用户服务实现类
*
* @author ken
*/
@Slf4j
@Service
public class UserService {
private final UserMapper userMapper;
private final PlatformTransactionManager transactionManager;
@Autowired
public UserService(UserMapper userMapper, PlatformTransactionManager transactionManager) {
this.userMapper = userMapper;
this.transactionManager = transactionManager;
}
/**
* 新增用户
*
* @param user 用户实体
* @return 新增成功的用户ID
*/
public Long addUser(User user) {
// 参数校验
validateUser(user, true);
log.info("新增用户:{}", user.getUsername());
// 编程式事务
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
userMapper.insert(user);
transactionManager.commit(status);
log.info("新增用户成功,用户ID:{}", user.getId());
return user.getId();
} catch (Exception e) {
transactionManager.rollback(status);
log.error("新增用户失败", e);
throw new RuntimeException("新增用户失败", e);
}
}
/**
* 根据ID删除用户(逻辑删除)
*
* @param id 用户ID
* @return 删除成功标识
*/
public boolean deleteUserById(Long id) {
if (ObjectUtils.isEmpty(id)) {
throw new IllegalArgumentException("用户ID不能为空");
}
log.info("删除用户,用户ID:{}", id);
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
int rows = userMapper.deleteById(id);
transactionManager.commit(status);
boolean success = rows > 0;
log.info("删除用户{},用户ID:{}", success ? "成功" : "失败", id);
return success;
} catch (Exception e) {
transactionManager.rollback(status);
log.error("删除用户失败", e);
throw new RuntimeException("删除用户失败", e);
}
}
/**
* 更新用户信息
*
* @param user 用户实体(含ID)
* @return 更新成功标识
*/
public boolean updateUser(User user) {
// 参数校验
validateUser(user, false);
if (ObjectUtils.isEmpty(user.getId())) {
throw new IllegalArgumentException("用户ID不能为空");
}
log.info("更新用户,用户ID:{}", user.getId());
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
int rows = userMapper.updateById(user);
transactionManager.commit(status);
boolean success = rows > 0;
log.info("更新用户{},用户ID:{}", success ? "成功" : "失败", user.getId());
return success;
} catch (Exception e) {
transactionManager.rollback(status);
log.error("更新用户失败", e);
throw new RuntimeException("更新用户失败", e);
}
}
/**
* 根据ID查询用户
*
* @param id 用户ID
* @return 用户实体
*/
public User getUserById(Long id) {
if (ObjectUtils.isEmpty(id)) {
throw new IllegalArgumentException("用户ID不能为空");
}
log.info("查询用户,用户ID:{}", id);
return userMapper.selectById(id);
}
/**
* 条件查询用户列表
*
* @param username 用户名(模糊匹配)
* @param age 年龄
* @return 用户列表
*/
public List<User> listUser(String username, Integer age) {
log.info("条件查询用户,用户名:{},年龄:{}", username, age);
// 构建查询条件
QueryWrapper queryWrapper = QueryWrapper.create()
.where(USER.IS_DELETED.eq(0));
if (StringUtils.hasText(username)) {
queryWrapper.and(USER.USERNAME.like("%" + username + "%"));
}
if (!ObjectUtils.isEmpty(age)) {
queryWrapper.and(USER.AGE.eq(age));
}
// 按创建时间倒序
queryWrapper.orderBy(USER.CREATE_TIME.desc());
return userMapper.selectList(queryWrapper);
}
/**
* 分页查询用户
*
* @param pageNum 页码(从1开始)
* @param pageSize 每页条数
* @param username 用户名(模糊匹配)
* @return 分页结果
*/
public Page<User> pageUser(Integer pageNum, Integer pageSize, String username) {
// 参数校验
if (ObjectUtils.isEmpty(pageNum) || pageNum < 1) {
throw new IllegalArgumentException("页码必须大于等于1");
}
if (ObjectUtils.isEmpty(pageSize) || pageSize < 1 || pageSize > 100) {
throw new IllegalArgumentException("每页条数必须在1-100之间");
}
log.info("分页查询用户,页码:{},每页条数:{},用户名:{}", pageNum, pageSize, username);
// 构建分页对象
Page<User> page = new Page<>(pageNum, pageSize);
// 构建查询条件
QueryWrapper queryWrapper = QueryWrapper.create()
.where(USER.IS_DELETED.eq(0));
if (StringUtils.hasText(username)) {
queryWrapper.and(USER.USERNAME.like("%" + username + "%"));
}
queryWrapper.orderBy(USER.CREATE_TIME.desc());
// MyBatis-Flex分页查询(结合MyBatis-Plus分页插件)
return userMapper.selectPage(page, queryWrapper);
}
/**
* 批量新增用户
*
* @param userList 用户列表
* @return 新增成功条数
*/
public int batchAddUser(List<User> userList) {
if (CollectionUtils.isEmpty(userList)) {
throw new IllegalArgumentException("用户列表不能为空");
}
// 批量参数校验
userList.forEach(user -> validateUser(user, true));
log.info("批量新增用户,数量:{}", userList.size());
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
int rows = userMapper.insertBatch(userList);
transactionManager.commit(status);
log.info("批量新增用户成功,新增条数:{}", rows);
return rows;
} catch (Exception e) {
transactionManager.rollback(status);
log.error("批量新增用户失败", e);
throw new RuntimeException("批量新增用户失败", e);
}
}
/**
* 用户参数校验
*
* @param user 用户实体
* @param isInsert 是否为新增场景(新增需校验必填字段,更新可不校验)
*/
private void validateUser(User user, boolean isInsert) {
if (ObjectUtils.isEmpty(user)) {
throw new IllegalArgumentException("用户实体不能为空");
}
if (isInsert) {
StringUtils.hasText(user.getUsername(), "用户名不能为空");
StringUtils.hasText(user.getPassword(), "密码不能为空");
}
// 年龄校验(若存在)
if (!ObjectUtils.isEmpty(user.getAge()) && (user.getAge() < 0 || user.getAge() > 150)) {
throw new IllegalArgumentException("年龄必须在0-150之间");
}
// 邮箱校验(若存在)
if (StringUtils.hasText(user.getEmail()) && !user.getEmail().matches("^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)+$")) {
throw new IllegalArgumentException("邮箱格式不正确");
}
}
}
说明:
- 编程式事务通过
PlatformTransactionManager实现,手动控制提交和回滚,适合复杂事务场景; - 使用
QueryWrapper链式构建查询条件,结合UserTableDef.USER(MyBatis-Flex自动生成的表定义),避免硬编码字段名; - 参数校验使用
org.springframework.util包下的工具类(ObjectUtils、StringUtils、CollectionUtils),符合规范; - 批量新增使用
insertBatch方法,性能优于循环单条插入。
3.5 Controller层实现(Swagger3接口)
暴露RESTful接口,结合Swagger3注解,方便接口调试:
package com.jam.demo.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.jam.demo.entity.User;
import com.jam.demo.service.UserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 用户控制器
*
* @author ken
*/
@Slf4j
@RestController
@RequestMapping("/api/user")
@Tag(name = "用户管理接口", description = "用户的增删改查、分页查询、批量新增接口")
public class UserController {
private final UserService userService;
@Autowired
public UserController(UserService userService) {
this.userService = userService;
}
/**
* 新增用户
*/
@PostMapping
@Operation(
summary = "新增用户",
description = "新增用户,用户名和密码为必填项",
responses = {
@ApiResponse(responseCode = "200", description = "新增成功", content = @Content(schema = @Schema(implementation = Long.class))),
@ApiResponse(responseCode = "500", description = "新增失败")
}
)
public ResponseEntity<Long> addUser(@Parameter(description = "用户实体", required = true) @RequestBody User user) {
Long userId = userService.addUser(user);
return new ResponseEntity<>(userId, HttpStatus.OK);
}
/**
* 根据ID删除用户
*/
@DeleteMapping("/{id}")
@Operation(
summary = "根据ID删除用户",
description = "逻辑删除用户,并非物理删除",
responses = {
@ApiResponse(responseCode = "200", description = "删除成功", content = @Content(schema = @Schema(implementation = Boolean.class))),
@ApiResponse(responseCode = "500", description = "删除失败")
}
)
public ResponseEntity<Boolean> deleteUserById(@Parameter(description = "用户ID", required = true) @PathVariable Long id) {
boolean success = userService.deleteUserById(id);
return new ResponseEntity<>(success, HttpStatus.OK);
}
/**
* 更新用户信息
*/
@PutMapping
@Operation(
summary = "更新用户信息",
description = "更新用户信息,需传入用户ID",
responses = {
@ApiResponse(responseCode = "200", description = "更新成功", content = @Content(schema = @Schema(implementation = Boolean.class))),
@ApiResponse(responseCode = "500", description = "更新失败")
}
)
public ResponseEntity<Boolean> updateUser(@Parameter(description = "用户实体(含ID)", required = true) @RequestBody User user) {
boolean success = userService.updateUser(user);
return new ResponseEntity<>(success, HttpStatus.OK);
}
/**
* 根据ID查询用户
*/
@GetMapping("/{id}")
@Operation(
summary = "根据ID查询用户",
responses = {
@ApiResponse(responseCode = "200", description = "查询成功", content = @Content(schema = @Schema(implementation = User.class))),
@ApiResponse(responseCode = "500", description = "查询失败")
}
)
public ResponseEntity<User> getUserById(@Parameter(description = "用户ID", required = true) @PathVariable Long id) {
User user = userService.getUserById(id);
return new ResponseEntity<>(user, HttpStatus.OK);
}
/**
* 条件查询用户列表
*/
@GetMapping("/list")
@Operation(
summary = "条件查询用户列表",
description = "根据用户名(模糊)和年龄查询用户列表",
responses = {
@ApiResponse(responseCode = "200", description = "查询成功", content = @Content(schema = @Schema(implementation = List.class))),
@ApiResponse(responseCode = "500", description = "查询失败")
}
)
public ResponseEntity<List<User>> listUser(
@Parameter(description = "用户名(模糊匹配)") @RequestParam(required = false) String username,
@Parameter(description = "年龄") @RequestParam(required = false) Integer age) {
List<User> userList = userService.listUser(username, age);
return new ResponseEntity<>(userList, HttpStatus.OK);
}
/**
* 分页查询用户
*/
@GetMapping("/page")
@Operation(
summary = "分页查询用户",
description = "分页查询用户,支持用户名模糊匹配",
responses = {
@ApiResponse(responseCode = "200", description = "查询成功", content = @Content(schema = @Schema(implementation = Page.class))),
@ApiResponse(responseCode = "500", description = "查询失败")
}
)
public ResponseEntity<Page<User>> pageUser(
@Parameter(description = "页码(从1开始)", required = true) @RequestParam Integer pageNum,
@Parameter(description = "每页条数(1-100)", required = true) @RequestParam Integer pageSize,
@Parameter(description = "用户名(模糊匹配)") @RequestParam(required = false) String username) {
Page<User> page = userService.pageUser(pageNum, pageSize, username);
return new ResponseEntity<>(page, HttpStatus.OK);
}
/**
* 批量新增用户
*/
@PostMapping("/batch")
@Operation(
summary = "批量新增用户",
description = "批量新增用户,每个用户的用户名和密码为必填项",
responses = {
@ApiResponse(responseCode = "200", description = "新增成功", content = @Content(schema = @Schema(implementation = Integer.class))),
@ApiResponse(responseCode = "500", description = "新增失败")
}
)
public ResponseEntity<Integer> batchAddUser(@Parameter(description = "用户列表", required = true) @RequestBody List<User> userList) {
int rows = userService.batchAddUser(userList);
return new ResponseEntity<>(rows, HttpStatus.OK);
}
}
3.6 分页插件配置
MyBatis-Flex结合MyBatis-Plus的分页插件,实现高性能分页:
package com.jam.demo.config;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* MyBatis-Plus配置(分页插件)
*
* @author ken
*/
@Configuration
public class MyBatisPlusConfig {
/**
* 分页插件
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加MySQL分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(com.baomidou.mybatisplus.annotation.DbType.MYSQL));
return interceptor;
}
}
四、高级特性:动态SQL与关联查询
4.1 动态SQL实战
MyBatis-Flex的QueryWrapper支持复杂的动态SQL构建,例如多条件组合、in查询、区间查询等:
/**
* 复杂条件查询用户
*
* @param userQuery 查询条件封装
* @return 用户列表
*/
public List<User> listUserByComplexCondition(UserQuery userQuery) {
QueryWrapper queryWrapper = QueryWrapper.create()
.where(USER.IS_DELETED.eq(0));
// 用户名模糊查询
if (StringUtils.hasText(userQuery.getUsername())) {
queryWrapper.and(USER.USERNAME.like("%" + userQuery.getUsername() + "%"));
}
// 年龄区间查询
if (!ObjectUtils.isEmpty(userQuery.getMinAge()) && !ObjectUtils.isEmpty(userQuery.getMaxAge())) {
queryWrapper.and(USER.AGE.between(userQuery.getMinAge(), userQuery.getMaxAge()));
} else if (!ObjectUtils.isEmpty(userQuery.getMinAge())) {
queryWrapper.and(USER.AGE.ge(userQuery.getMinAge()));
} else if (!ObjectUtils.isEmpty(userQuery.getMaxAge())) {
queryWrapper.and(USER.AGE.le(userQuery.getMaxAge()));
}
// 邮箱后缀匹配
if (StringUtils.hasText(userQuery.getEmailSuffix())) {
queryWrapper.and(USER.EMAIL.like("%" + userQuery.getEmailSuffix()));
}
// 批量ID查询
if (!CollectionUtils.isEmpty(userQuery.getIdList())) {
queryWrapper.and(USER.ID.in(userQuery.getIdList()));
}
// 时间区间查询(创建时间)
if (!ObjectUtils.isEmpty(userQuery.getStartTime()) && !ObjectUtils.isEmpty(userQuery.getEndTime())) {
queryWrapper.and(USER.CREATE_TIME.between(userQuery.getStartTime(), userQuery.getEndTime()));
}
// 排序:先按年龄倒序,再按创建时间倒序
queryWrapper.orderBy(USER.AGE.desc())
.orderBy(USER.CREATE_TIME.desc());
return userMapper.selectList(queryWrapper);
}
// 查询条件封装类
@Data
@Schema(description = "用户查询条件")
public class UserQuery {
@Schema(description = "用户名(模糊)")
private String username;
@Schema(description = "最小年龄")
private Integer minAge;
@Schema(description = "最大年龄")
private Integer maxAge;
@Schema(description = "邮箱后缀")
private String emailSuffix;
@Schema(description = "用户ID列表")
private List<Long> idList;
@Schema(description = "创建开始时间")
private LocalDateTime startTime;
@Schema(description = "创建结束时间")
private LocalDateTime endTime;
}
4.2 关联查询(一对一/一对多)
MyBatis-Flex支持关联查询,以用户和订单的一对多关系为例(新增订单表t_order):
4.2.1 订单表SQL
CREATE TABLE `t_order` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '订单ID',
`user_id` bigint NOT NULL COMMENT '用户ID',
`order_no` varchar(50) NOT NULL COMMENT '订单编号',
`amount` decimal(10,2) NOT NULL COMMENT '订单金额',
`status` tinyint NOT NULL COMMENT '订单状态(0-待支付,1-已支付,2-已取消)',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`is_deleted` tinyint NOT NULL DEFAULT '0' COMMENT '逻辑删除(0-未删,1-已删)',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_order_no` (`order_no`),
KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';
4.2.2 订单实体类
package com.jam.demo.entity;
import com.mybatis.flex.core.BaseEntity;
import com.mybatis.flex.annotation.*;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 订单实体类
*
* @author ken
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(description = "订单实体")
@Table(value = "t_order", comment = "订单表")
public class Order extends BaseEntity {
/**
* 订单ID
*/
@Schema(description = "订单ID")
@Id(keyType = KeyType.AUTO)
private Long id;
/**
* 用户ID
*/
@Schema(description = "用户ID", requiredMode = Schema.RequiredMode.REQUIRED)
@Column(comment = "用户ID", isNullable = false)
private Long userId;
/**
* 订单编号
*/
@Schema(description = "订单编号", requiredMode = Schema.RequiredMode.REQUIRED)
@Column(comment = "订单编号", isUnique = true, isNullable = false)
private String orderNo;
/**
* 订单金额
*/
@Schema(description = "订单金额", requiredMode = Schema.RequiredMode.REQUIRED)
@Column(comment = "订单金额", isNullable = false)
private BigDecimal amount;
/**
* 订单状态(0-待支付,1-已支付,2-已取消)
*/
@Schema(description = "订单状态(0-待支付,1-已支付,2-已取消)", requiredMode = Schema.RequiredMode.REQUIRED)
@Column(comment = "订单状态(0-待支付,1-已支付,2-已取消)", isNullable = false)
private Integer status;
/**
* 创建时间
*/
@Schema(description = "创建时间")
@Column(comment = "创建时间", isInsertFill = true)
@InsertFill(InsertFillStrategy.NOW)
private LocalDateTime createTime;
/**
* 更新时间
*/
@Schema(description = "更新时间")
@Column(comment = "更新时间", isInsertFill = true, isUpdateFill = true)
@InsertFill(InsertFillStrategy.NOW)
@UpdateFill(UpdateFillStrategy.NOW)
private LocalDateTime updateTime;
/**
* 逻辑删除(0-未删,1-已删)
*/
@Schema(description = "逻辑删除(0-未删,1-已删)")
@Column(comment = "逻辑删除(0-未删,1-已删)")
@LogicDelete
private Integer isDeleted;
}
4.2.3 关联查询实现(用户关联订单列表)
package com.jam.demo.service;
import com.mybatis.flex.core.query.QueryWrapper;
import com.jam.demo.entity.Order;
import com.jam.demo.entity.User;
import com.jam.demo.mapper.OrderMapper;
import com.jam.demo.mapper.UserMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;
import java.util.List;
import static com.jam.demo.entity.table.OrderTableDef.ORDER;
import static com.jam.demo.entity.table.UserTableDef.USER;
/**
* 关联查询服务
*
* @author ken
*/
@Slf4j
@Service
public class RelationService {
private final UserMapper userMapper;
private final OrderMapper orderMapper;
@Autowired
public RelationService(UserMapper userMapper, OrderMapper orderMapper) {
this.userMapper = userMapper;
this.orderMapper = orderMapper;
}
/**
* 根据用户ID查询用户及关联的订单列表
*
* @param userId 用户ID
* @return 用户(含订单列表)
*/
public User getUserWithOrders(Long userId) {
if (ObjectUtils.isEmpty(userId)) {
throw new IllegalArgumentException("用户ID不能为空");
}
log.info("查询用户及订单列表,用户ID:{}", userId);
// 1. 查询用户信息
User user = userMapper.selectById(userId);
if (ObjectUtils.isEmpty(user)) {
log.info("用户不存在,用户ID:{}", userId);
return null;
}
// 2. 查询该用户的订单列表
QueryWrapper orderQueryWrapper = QueryWrapper.create()
.where(ORDER.IS_DELETED.eq(0))
.and(ORDER.USER_ID.eq(userId))
.orderBy(ORDER.CREATE_TIME.desc());
List<Order> orderList = orderMapper.selectList(orderQueryWrapper);
// 3. 关联订单列表(可通过DTO封装返回,避免修改Entity)
user.setOrders(orderList);
return user;
}
}
说明:实际开发中建议使用DTO封装关联结果,避免直接在Entity中添加关联字段,保证Entity的纯粹性。
五、MyBatis-Flex vs MyBatis-Plus:核心差异解析
很多开发者会混淆两者,其实它们的设计理念和侧重点不同,核心差异如下:
| 特性 | MyBatis-Flex | MyBatis-Plus |
| 设计理念 | 极简、灵活,专注于SQL生成的简洁性 | 功能全面,提供一站式解决方案 |
| SQL构建方式 | QueryWrapper链式API更简洁,支持表定义常量 | 支持QueryWrapper和LambdaQueryWrapper |
| 性能 | 更优,内置SQL优化,减少冗余操作 | 性能优秀,功能多导致轻微开销 |
| 侵入性 | 极低,可与MyBatis无缝集成 | 中等,需继承特定基类 |
| 企业级特性支持 | 原生支持多租户、数据脱敏等,配置简单 | 支持,但部分特性需额外配置 |
| 学习成本 | 低,API简洁直观 | 中,功能多需记忆更多API |
架构差异对比:
总结:
- 若追求极简开发、灵活的SQL控制、低侵入性,优先选MyBatis-Flex;
- 若需要丰富的开箱即用功能(如代码生成、乐观锁、逻辑删除的更多配置),优先选MyBatis-Plus;
- 两者可共存,可根据场景灵活搭配使用。
六、底层逻辑简析:MyBatis-Flex的SQL生成原理
MyBatis-Flex的核心优势在于"零XML动态SQL生成",其底层原理可概括为"注解解析+语法树构建":
- 注解解析:启动时扫描Entity上的
@Table、@Column等注解,解析表名、字段名、主键、填充规则等信息,生成表定义常量(如UserTableDef.USER); - QueryWrapper构建:开发者通过链式API构建查询条件时,QueryWrapper会将条件转换为SQL语法树节点;
- SQL生成:根据语法树节点,结合表定义信息,生成标准的SQL语句,同时处理参数绑定、防SQL注入;
- 委托执行:将生成的SQL委托给MyBatis执行,利用MyBatis的结果集映射功能将查询结果转换为Entity对象。
SQL生成流程细节:
七、实战避坑指南:常见问题及解决方案
7.1 表名/字段名映射错误
- 问题:查询时提示表名或字段名不存在;
- 原因:Entity注解配置错误,或未开启驼峰命名转换;
- 解决方案:
- 确保
@Table的value属性正确对应表名; - 确保
@Column的value属性正确对应字段名(若字段名与属性名一致可省略); - 在配置文件中开启驼峰命名转换(MyBatis-Flex默认开启)。
7.2 分页查询返回空数据
- 问题:分页查询时total正确,但records为空;
- 原因:表定义常量使用错误,或查询条件有误;
- 解决方案:
- 检查QueryWrapper中使用的表定义常量是否正确(如是否引用了其他表的常量);
- 检查查询条件是否过滤了所有数据(如逻辑删除条件是否正确);
- 直接打印SQL,在数据库中执行验证条件是否正确。
7.3 事务不生效
- 问题:编程式事务未提交或回滚;
- 原因:事务管理器配置错误,或未捕获异常;
- 解决方案:
- 确保注入的是
PlatformTransactionManager,而非其他事务管理器; - 确保异常被正确捕获,并在异常时调用
rollback; - 检查数据源配置是否正确,事务需基于同一数据源。
八、总结
MyBatis-Flex作为新一代持久层框架,以"极简、灵活、高性能"的特点,完美平衡了MyBatis的灵活性和MyBatis-Plus的便捷性。本文通过完整的实战案例,覆盖了从环境搭建、单表CRUD、分页查询到关联查询的核心场景,所有代码均可直接编译运行。
核心要点回顾:
- 环境搭建只需引入核心依赖,配置简单;
- 基于注解和表定义常量,实现零XML的动态SQL生成;
- 编程式事务保证数据一致性,符合企业级开发规范;
- 与MyBatis-Plus各有侧重,可根据场景选择或共存。