简介
MyBatis-Flex 是一个基于 MyBatis 的增强框架。
它的定位很直接:
1保留 MyBatis 写 SQL 的灵活性,同时补上通用 CRUD、链式查询、分页、逻辑删除、乐观锁等常用能力。 2
普通 MyBatis 项目里,常见代码结构是:
1Entity 2Mapper 接口 3Mapper XML 4Service 5Controller 6
如果只是做一张表的增删改查,也要写不少重复 SQL。
MyBatis-Flex 的思路是:
1简单 CRUD 交给 BaseMapper 2复杂条件交给 QueryWrapper 3特别复杂的 SQL 仍然可以回到 MyBatis 原生 XML 或注解 SQL 4
一句话概括:
1MyBatis-Flex 是一个轻量的 MyBatis 增强工具,适合需要 SQL 控制感,又想减少重复 CRUD 代码的项目。 2
MyBatis-Flex 解决什么问题
先看普通 MyBatis 的单表查询。
Mapper 接口:
1public interface UserMapper { 2 User selectById(Long id); 3} 4
XML:
1<select id="selectById" resultType="com.example.entity.User"> 2 select id, username, email, age, status, created_at 3 from tb_user 4 where id = #{id} 5</select> 6
新增、修改、删除、分页、条件查询都继续写 SQL。
这种方式很灵活,但简单操作会显得重复。
MyBatis-Flex 的写法是:
1public interface UserMapper extends BaseMapper<User> { 2} 3
然后直接使用:
1User user = userMapper.selectOneById(1L); 2
这类基础方法由 BaseMapper 提供。
如果需要条件查询:
1QueryWrapper query = QueryWrapper.create() 2 .where(USER.AGE.ge(18)) 3 .and(USER.STATUS.eq("ACTIVE")) 4 .orderBy(USER.ID.desc()); 5 6List<User> users = userMapper.selectListByQuery(query); 7
这就是 MyBatis-Flex 的核心体验:
1单表 CRUD 少写 SQL 2条件查询用链式 API 3复杂 SQL 仍然保留 MyBatis 原生能力 4
核心概念
学习 MyBatis-Flex 时,先抓住几个关键词。
| 名称 | 作用 |
|---|---|
| @Table | 标记实体类对应的数据库表 |
| @Id | 标记主键字段 |
| @Column | 配置字段映射、逻辑删除、忽略字段等 |
| BaseMapper<T> | 通用 Mapper,提供基础增删改查 |
| QueryWrapper | 链式 SQL 构造器 |
| Page<T> | 分页结果对象 |
| Db + Row | 不依赖实体类的数据库操作方式 |
| APT | 编译期生成表定义类,比如 USER、ACCOUNT |
其中最常用的是:
1@Table + @Id + BaseMapper + QueryWrapper 2
Maven 依赖
MyBatis-Flex 需要根据 Spring Boot 版本选择不同 starter。
Spring Boot 2.x:
1<dependency> 2 <groupId>com.mybatis-flex</groupId> 3 <artifactId>mybatis-flex-spring-boot-starter</artifactId> 4 <version>1.11.7</version> 5</dependency> 6
Spring Boot 3.x:
1<dependency> 2 <groupId>com.mybatis-flex</groupId> 3 <artifactId>mybatis-flex-spring-boot3-starter</artifactId> 4 <version>1.11.7</version> 5</dependency> 6
数据库驱动以 MySQL 为例:
1<dependency> 2 <groupId>com.mysql</groupId> 3 <artifactId>mysql-connector-j</artifactId> 4 <scope>runtime</scope> 5</dependency> 6
APT 处理器建议放到 annotationProcessorPaths 里:
1<plugin> 2 <groupId>org.apache.maven.plugins</groupId> 3 <artifactId>maven-compiler-plugin</artifactId> 4 <version>3.11.0</version> 5 <configuration> 6 <annotationProcessorPaths> 7 <path> 8 <groupId>com.mybatis-flex</groupId> 9 <artifactId>mybatis-flex-processor</artifactId> 10 <version>1.11.7</version> 11 </path> 12 </annotationProcessorPaths> 13 </configuration> 14</plugin> 15
APT 用来生成表定义类。
比如实体类叫 User,编译后会生成类似这样的类:
1UserTableDef 2
代码里就可以静态导入:
1import static com.example.entity.table.UserTableDef.USER; 2
然后写出:
1USER.ID.eq(1L) 2USER.USERNAME.like("张") 3
数据源配置
application.yml 示例:
1spring: 2 datasource: 3 url: jdbc:mysql://localhost:3306/flex_demo?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8 4 username: root 5 password: 123456 6 driver-class-name: com.mysql.cj.jdbc.Driver 7 8mybatis-flex: 9 print-banner: false 10 configuration: 11 log-impl: org.apache.ibatis.logging.stdout.StdOutImpl 12
开发环境可以打开 SQL 日志,便于观察生成的 SQL。
生产环境一般会交给日志系统统一管理。
启动类配置
启动类需要扫描 Mapper。
1import org.mybatis.spring.annotation.MapperScan; 2import org.springframework.boot.SpringApplication; 3import org.springframework.boot.autoconfigure.SpringBootApplication; 4 5@SpringBootApplication 6@MapperScan("com.example.demo.mapper") 7public class MyBatisFlexDemoApplication { 8 9 public static void main(String[] args) { 10 SpringApplication.run(MyBatisFlexDemoApplication.class, args); 11 } 12} 13
也可以在每个 Mapper 接口上加 @Mapper。
项目里 Mapper 较多时,@MapperScan 更省事。
准备演示表
下面用一个用户表做示例。
1DROP TABLE IF EXISTS tb_user; 2 3CREATE TABLE tb_user ( 4 id BIGINT PRIMARY KEY AUTO_INCREMENT, 5 username VARCHAR(50) NOT NULL, 6 email VARCHAR(100) NOT NULL, 7 age INT NOT NULL, 8 status VARCHAR(20) NOT NULL, 9 deleted TINYINT NOT NULL DEFAULT 0, 10 created_at DATETIME NOT NULL, 11 updated_at DATETIME NULL 12); 13 14INSERT INTO tb_user (username, email, age, status, deleted, created_at, updated_at) VALUES 15('张三', 'zhangsan@example.com', 20, 'ACTIVE', 0, '2026-01-01 10:00:00', NULL), 16('李四', 'lisi@example.com', 25, 'ACTIVE', 0, '2026-01-02 10:00:00', NULL), 17('王五', 'wangwu@example.com', 17, 'DISABLED', 0, '2026-01-03 10:00:00', NULL); 18
实体类
1package com.example.demo.entity; 2 3import com.mybatisflex.annotation.Column; 4import com.mybatisflex.annotation.Id; 5import com.mybatisflex.annotation.Table; 6import com.mybatisflex.core.keygen.KeyType; 7 8import java.time.LocalDateTime; 9 10@Table("tb_user") 11public class User { 12 13 @Id(keyType = KeyType.Auto) 14 private Long id; 15 16 private String username; 17 18 private String email; 19 20 private Integer age; 21 22 private String status; 23 24 @Column(isLogicDelete = true) 25 private Boolean deleted; 26 27 private LocalDateTime createdAt; 28 29 private LocalDateTime updatedAt; 30 31 public Long getId() { 32 return id; 33 } 34 35 public void setId(Long id) { 36 this.id = id; 37 } 38 39 public String getUsername() { 40 return username; 41 } 42 43 public void setUsername(String username) { 44 this.username = username; 45 } 46 47 public String getEmail() { 48 return email; 49 } 50 51 public void setEmail(String email) { 52 this.email = email; 53 } 54 55 public Integer getAge() { 56 return age; 57 } 58 59 public void setAge(Integer age) { 60 this.age = age; 61 } 62 63 public String getStatus() { 64 return status; 65 } 66 67 public void setStatus(String status) { 68 this.status = status; 69 } 70 71 public Boolean getDeleted() { 72 return deleted; 73 } 74 75 public void setDeleted(Boolean deleted) { 76 this.deleted = deleted; 77 } 78 79 public LocalDateTime getCreatedAt() { 80 return createdAt; 81 } 82 83 public void setCreatedAt(LocalDateTime createdAt) { 84 this.createdAt = createdAt; 85 } 86 87 public LocalDateTime getUpdatedAt() { 88 return updatedAt; 89 } 90 91 public void setUpdatedAt(LocalDateTime updatedAt) { 92 this.updatedAt = updatedAt; 93 } 94} 95
几个重点:
@Table("tb_user"):实体类对应tb_user表@Id(keyType = KeyType.Auto):主键由数据库自增生成@Column(isLogicDelete = true):标记逻辑删除字段created_at会自动映射到createdAt
Mapper 接口
1package com.example.demo.mapper; 2 3import com.example.demo.entity.User; 4import com.mybatisflex.core.BaseMapper; 5 6public interface UserMapper extends BaseMapper<User> { 7} 8
继承 BaseMapper<User> 后,常见 CRUD 方法就可以直接使用。
新增数据
1User user = new User(); 2user.setUsername("赵六"); 3user.setEmail("zhaoliu@example.com"); 4user.setAge(28); 5user.setStatus("ACTIVE"); 6user.setDeleted(false); 7user.setCreatedAt(LocalDateTime.now()); 8 9userMapper.insert(user); 10 11System.out.println(user.getId()); 12
如果主键配置为自增,插入后主键会回填到实体对象。
常见生成 SQL 大致是:
1insert into tb_user (username, email, age, status, deleted, created_at) 2values (?, ?, ?, ?, ?, ?) 3
根据 ID 查询
1User user = userMapper.selectOneById(1L); 2
对应场景:
1通过主键查一条数据。 2
如果实体配置了逻辑删除字段,查询会自动带上未删除条件。
查询全部
1List<User> users = userMapper.selectAll(); 2
实际项目中,selectAll 更适合数据量很小的字典表、配置表。
业务表数据量较大时,通常使用条件查询或分页查询。
修改数据
1User user = new User(); 2user.setId(1L); 3user.setEmail("new-zhangsan@example.com"); 4user.setUpdatedAt(LocalDateTime.now()); 5 6userMapper.update(user); 7
这类写法适合根据实体主键更新。
如果只想更新某些字段,实体里只设置需要修改的字段更清楚。
删除数据
1userMapper.deleteById(1L); 2
如果实体类没有配置逻辑删除,这就是物理删除。
如果实体类配置了:
1@Column(isLogicDelete = true) 2private Boolean deleted; 3
删除会变成更新逻辑删除字段。
大致 SQL 是:
1update tb_user 2set deleted = 1 3where id = ? 4 and deleted = 0 5
逻辑删除后的数据,普通查询会自动过滤。
QueryWrapper 是什么
QueryWrapper 是 MyBatis-Flex 里非常核心的查询构造器。
它用来在 Java 代码里组装 SQL。
普通 SQL:
1select * 2from tb_user 3where age >= 18 4 and status = 'ACTIVE' 5order by id desc 6
QueryWrapper 写法:
1QueryWrapper query = QueryWrapper.create() 2 .where(USER.AGE.ge(18)) 3 .and(USER.STATUS.eq("ACTIVE")) 4 .orderBy(USER.ID.desc()); 5 6List<User> users = userMapper.selectListByQuery(query); 7
这里的 USER 来自 APT 生成的表定义类。
常见静态导入:
1import static com.example.demo.entity.table.UserTableDef.USER; 2
常见条件写法
等于:
1USER.STATUS.eq("ACTIVE") 2
不等于:
1USER.STATUS.ne("DISABLED") 2
大于:
1USER.AGE.gt(18) 2
大于等于:
1USER.AGE.ge(18) 2
小于:
1USER.AGE.lt(60) 2
模糊查询:
1USER.USERNAME.like("张") 2
范围查询:
1USER.AGE.between(18, 30) 2
IN 查询:
1USER.ID.in(1L, 2L, 3L) 2
排序:
1orderBy(USER.ID.desc()) 2
动态条件查询
业务接口里经常会有可选筛选条件。
比如:
1username 可传可不传 2status 可传可不传 3minAge 可传可不传 4
可以这样写:
1public List<User> search(String username, String status, Integer minAge) { 2 QueryWrapper query = QueryWrapper.create() 3 .select() 4 .from(USER); 5 6 if (username != null && !username.isBlank()) { 7 query.and(USER.USERNAME.like(username)); 8 } 9 10 if (status != null && !status.isBlank()) { 11 query.and(USER.STATUS.eq(status)); 12 } 13 14 if (minAge != null) { 15 query.and(USER.AGE.ge(minAge)); 16 } 17 18 query.orderBy(USER.ID.desc()); 19 20 return userMapper.selectListByQuery(query); 21} 22
这种写法比手动拼 SQL 字符串更稳。
参数仍然会走预编译绑定,不需要把业务值直接拼到 SQL 里。
查询单个对象
按唯一字段查询:
1public User findByEmail(String email) { 2 QueryWrapper query = QueryWrapper.create() 3 .where(USER.EMAIL.eq(email)); 4 5 return userMapper.selectOneByQuery(query); 6} 7
如果数据可能不存在,业务层可以包一层 Optional:
1public Optional<User> findOptionalByEmail(String email) { 2 QueryWrapper query = QueryWrapper.create() 3 .where(USER.EMAIL.eq(email)); 4 5 User user = userMapper.selectOneByQuery(query); 6 return Optional.ofNullable(user); 7} 8
查询部分字段
有些接口不需要返回整张表的所有字段。
1QueryWrapper query = QueryWrapper.create() 2 .select(USER.ID, USER.USERNAME, USER.EMAIL) 3 .from(USER) 4 .where(USER.STATUS.eq("ACTIVE")); 5 6List<User> users = userMapper.selectListByQuery(query); 7
这段 SQL 只会查询指定字段。
字段越少,网络传输和对象映射压力越小。
查询单个字段
统计数量:
1QueryWrapper query = QueryWrapper.create() 2 .where(USER.STATUS.eq("ACTIVE")); 3 4long count = userMapper.selectCountByQuery(query); 5
查询第一列:
1QueryWrapper query = QueryWrapper.create() 2 .select(USER.EMAIL) 3 .from(USER) 4 .where(USER.ID.eq(1L)); 5 6String email = userMapper.selectObjectByQueryAs(query, String.class); 7
这类写法适合只取一个字段的场景。
分页查询
BaseMapper 提供了 paginate 方法。
1import com.mybatisflex.core.paginate.Page; 2 3public Page<User> pageActiveUsers(int pageNumber, int pageSize) { 4 QueryWrapper query = QueryWrapper.create() 5 .where(USER.STATUS.eq("ACTIVE")) 6 .orderBy(USER.ID.desc()); 7 8 return userMapper.paginate(pageNumber, pageSize, query); 9} 10
返回值是 Page<User>。
常用字段:
1Page<User> page = pageActiveUsers(1, 10); 2 3List<User> records = page.getRecords(); 4long totalRow = page.getTotalRow(); 5long totalPage = page.getTotalPage(); 6
分页参数含义:
1pageNumber:当前页,从 1 开始 2pageSize:每页条数 3
第二页以后复用总数
分页查询通常会查两次:
1一次查总数 2一次查当前页数据 3
如果第一页已经拿到总数,第二页以后可以把总数传进去,减少一次 count 查询。
1long totalRow = 120L; 2 3Page<User> page = userMapper.paginate(2, 10, totalRow, query); 4
这个写法适合对性能比较敏感的列表页。
多表查询
准备一张订单表:
1DROP TABLE IF EXISTS tb_order; 2 3CREATE TABLE tb_order ( 4 id BIGINT PRIMARY KEY AUTO_INCREMENT, 5 user_id BIGINT NOT NULL, 6 order_no VARCHAR(50) NOT NULL, 7 amount DECIMAL(10, 2) NOT NULL, 8 status VARCHAR(20) NOT NULL, 9 created_at DATETIME NOT NULL 10); 11
实体类:
1@Table("tb_order") 2public class Order { 3 4 @Id(keyType = KeyType.Auto) 5 private Long id; 6 7 private Long userId; 8 9 private String orderNo; 10 11 private BigDecimal amount; 12 13 private String status; 14 15 private LocalDateTime createdAt; 16 17 // getter setter 18} 19
Mapper:
1public interface OrderMapper extends BaseMapper<Order> { 2} 3
定义一个 DTO 接收结果:
1public class OrderUserDTO { 2 private Long orderId; 3 private String orderNo; 4 private BigDecimal amount; 5 private String orderStatus; 6 private Long userId; 7 private String username; 8 private String email; 9 10 // getter setter 11} 12
关联查询:
1import static com.example.demo.entity.table.OrderTableDef.ORDER; 2import static com.example.demo.entity.table.UserTableDef.USER; 3 4public List<OrderUserDTO> findOrderUsers(String orderStatus) { 5 QueryWrapper query = QueryWrapper.create() 6 .select( 7 ORDER.ID.as("orderId"), 8 ORDER.ORDER_NO.as("orderNo"), 9 ORDER.AMOUNT, 10 ORDER.STATUS.as("orderStatus"), 11 USER.ID.as("userId"), 12 USER.USERNAME.as("username"), 13 USER.EMAIL.as("email") 14 ) 15 .from(ORDER) 16 .leftJoin(USER).on(ORDER.USER_ID.eq(USER.ID)) 17 .where(ORDER.STATUS.eq(orderStatus)) 18 .orderBy(ORDER.ID.desc()); 19 20 return orderMapper.selectListByQueryAs(query, OrderUserDTO.class); 21} 22
这种写法适合:
1SQL 不算特别复杂,但又不是简单单表查询。 2
如果 SQL 已经非常复杂,写 XML 也很正常。
MyBatis-Flex 不限制继续使用 MyBatis 原生 XML。
批量插入
1List<User> users = new ArrayList<>(); 2 3User user1 = new User(); 4user1.setUsername("用户1"); 5user1.setEmail("user1@example.com"); 6user1.setAge(18); 7user1.setStatus("ACTIVE"); 8user1.setDeleted(false); 9user1.setCreatedAt(LocalDateTime.now()); 10users.add(user1); 11 12User user2 = new User(); 13user2.setUsername("用户2"); 14user2.setEmail("user2@example.com"); 15user2.setAge(19); 16user2.setStatus("ACTIVE"); 17user2.setDeleted(false); 18user2.setCreatedAt(LocalDateTime.now()); 19users.add(user2); 20 21userMapper.insertBatch(users); 22
批量操作适合一次写入多条数据。
数据量很大时,可以按固定大小拆批。
1例如每 500 条或 1000 条执行一次。 2
条件删除
删除禁用状态用户:
1QueryWrapper query = QueryWrapper.create() 2 .where(USER.STATUS.eq("DISABLED")); 3 4userMapper.deleteByQuery(query); 5
如果配置了逻辑删除,这里会执行逻辑删除。
如果没有配置逻辑删除,就会执行物理删除。
条件更新
一些更新不方便先查出来再改,可以直接按条件更新。
1User updateEntity = new User(); 2updateEntity.setStatus("DISABLED"); 3updateEntity.setUpdatedAt(LocalDateTime.now()); 4 5QueryWrapper query = QueryWrapper.create() 6 .where(USER.AGE.lt(18)); 7 8userMapper.updateByQuery(updateEntity, query); 9
含义是:
1把年龄小于 18 的用户状态改成 DISABLED。 2
Active Record
MyBatis-Flex 也支持 Active Record 风格。
实体类继承 Model 后,可以直接调用保存、更新等方法。
1import com.mybatisflex.core.activerecord.Model; 2 3@Table("tb_user") 4public class User extends Model<User> { 5 @Id(keyType = KeyType.Auto) 6 private Long id; 7 8 private String username; 9 10 // getter setter 11} 12
使用:
1User user = new User(); 2user.setUsername("张三"); 3user.save(); 4
这种写法很短。
但在分层清晰的业务系统里,更常见的是:
1Controller -> Service -> Mapper 2
Active Record 更适合简单模块、脚本工具、管理后台里的少量操作。
Db + Row
Db + Row 适合不想定义实体类的场景。
比如临时查询、后台工具、通用管理功能。
1import com.mybatisflex.core.row.Db; 2import com.mybatisflex.core.row.Row; 3 4Row row = Db.selectOneById("tb_user", "id", 1L); 5 6String username = row.getString("username"); 7Integer age = row.getInt("age"); 8
查询列表:
1List<Row> rows = Db.selectListBySql( 2 "select id, username, email from tb_user where status = ?", 3 "ACTIVE" 4); 5
Db + Row 很灵活,但类型约束弱。
核心业务代码里,实体类和 Mapper 更容易维护。
Service 层封装
下面是一个比较完整的用户服务示例。
1package com.example.demo.service; 2 3import com.example.demo.entity.User; 4import com.example.demo.mapper.UserMapper; 5import com.mybatisflex.core.paginate.Page; 6import com.mybatisflex.core.query.QueryWrapper; 7import org.springframework.stereotype.Service; 8import org.springframework.transaction.annotation.Transactional; 9 10import java.time.LocalDateTime; 11import java.util.List; 12import java.util.Optional; 13 14import static com.example.demo.entity.table.UserTableDef.USER; 15 16@Service 17public class UserService { 18 19 private final UserMapper userMapper; 20 21 public UserService(UserMapper userMapper) { 22 this.userMapper = userMapper; 23 } 24 25 @Transactional 26 public Long create(User user) { 27 user.setStatus("ACTIVE"); 28 user.setDeleted(false); 29 user.setCreatedAt(LocalDateTime.now()); 30 31 userMapper.insert(user); 32 33 return user.getId(); 34 } 35 36 public Optional<User> findById(Long id) { 37 return Optional.ofNullable(userMapper.selectOneById(id)); 38 } 39 40 public List<User> search(String username, String status, Integer minAge) { 41 QueryWrapper query = QueryWrapper.create() 42 .select() 43 .from(USER); 44 45 if (username != null && !username.isBlank()) { 46 query.and(USER.USERNAME.like(username)); 47 } 48 49 if (status != null && !status.isBlank()) { 50 query.and(USER.STATUS.eq(status)); 51 } 52 53 if (minAge != null) { 54 query.and(USER.AGE.ge(minAge)); 55 } 56 57 query.orderBy(USER.ID.desc()); 58 59 return userMapper.selectListByQuery(query); 60 } 61 62 public Page<User> page(String status, int pageNumber, int pageSize) { 63 QueryWrapper query = QueryWrapper.create() 64 .where(USER.STATUS.eq(status)) 65 .orderBy(USER.ID.desc()); 66 67 return userMapper.paginate(pageNumber, pageSize, query); 68 } 69 70 @Transactional 71 public void updateEmail(Long id, String email) { 72 User user = new User(); 73 user.setId(id); 74 user.setEmail(email); 75 user.setUpdatedAt(LocalDateTime.now()); 76 77 userMapper.update(user); 78 } 79 80 @Transactional 81 public void disable(Long id) { 82 User user = new User(); 83 user.setId(id); 84 user.setStatus("DISABLED"); 85 user.setUpdatedAt(LocalDateTime.now()); 86 87 userMapper.update(user); 88 } 89 90 @Transactional 91 public void remove(Long id) { 92 userMapper.deleteById(id); 93 } 94} 95
这里把事务放在 Service 层。
Mapper 只负责数据库操作。
Service 负责业务规则和事务边界。
Controller 示例
1package com.example.demo.controller; 2 3import com.example.demo.entity.User; 4import com.example.demo.service.UserService; 5import com.mybatisflex.core.paginate.Page; 6import org.springframework.web.bind.annotation.DeleteMapping; 7import org.springframework.web.bind.annotation.GetMapping; 8import org.springframework.web.bind.annotation.PathVariable; 9import org.springframework.web.bind.annotation.PostMapping; 10import org.springframework.web.bind.annotation.PutMapping; 11import org.springframework.web.bind.annotation.RequestBody; 12import org.springframework.web.bind.annotation.RequestMapping; 13import org.springframework.web.bind.annotation.RequestParam; 14import org.springframework.web.bind.annotation.RestController; 15 16import java.util.List; 17 18@RestController 19@RequestMapping("/users") 20public class UserController { 21 22 private final UserService userService; 23 24 public UserController(UserService userService) { 25 this.userService = userService; 26 } 27 28 @PostMapping 29 public Long create(@RequestBody User user) { 30 return userService.create(user); 31 } 32 33 @GetMapping("/{id}") 34 public User detail(@PathVariable Long id) { 35 return userService.findById(id) 36 .orElseThrow(() -> new IllegalArgumentException("用户不存在")); 37 } 38 39 @GetMapping("/search") 40 public List<User> search(@RequestParam(required = false) String username, 41 @RequestParam(required = false) String status, 42 @RequestParam(required = false) Integer minAge) { 43 return userService.search(username, status, minAge); 44 } 45 46 @GetMapping 47 public Page<User> page(@RequestParam(defaultValue = "ACTIVE") String status, 48 @RequestParam(defaultValue = "1") int pageNumber, 49 @RequestParam(defaultValue = "10") int pageSize) { 50 return userService.page(status, pageNumber, pageSize); 51 } 52 53 @PutMapping("/{id}/email") 54 public void updateEmail(@PathVariable Long id, @RequestParam String email) { 55 userService.updateEmail(id, email); 56 } 57 58 @PutMapping("/{id}/disable") 59 public void disable(@PathVariable Long id) { 60 userService.disable(id); 61 } 62 63 @DeleteMapping("/{id}") 64 public void remove(@PathVariable Long id) { 65 userService.remove(id); 66 } 67} 68
这组接口覆盖了:
- 新增用户
- 按 ID 查询
- 多条件查询
- 分页查询
- 修改邮箱
- 禁用用户
- 删除用户
APT 生成类找不到怎么办
QueryWrapper 里常见的:
1USER.ID.eq(1L) 2
依赖 APT 生成的表定义类。
如果 IDE 里找不到 UserTableDef 或 USER,一般检查这些地方:
mybatis-flex-processor是否配置到annotationProcessorPaths- Maven 是否执行过
compile或package - IDE 是否开启 Annotation Processing
target/generated-sources/annotations是否被识别为 generated sources- 静态导入路径是否正确
默认情况下,生成类通常在:
1target/generated-sources/annotations 2
实体类包名如果是:
1com.example.demo.entity 2
生成类包名通常类似:
1com.example.demo.entity.table 2
自定义 APT 配置
可以在项目根目录创建:
1mybatis-flex.config 2
示例:
1processor.tableDef.propertiesNameStyle=upperCase 2processor.tableDef.package=${entityPackage}.table 3processor.mapper.generateEnable=false 4
常用配置含义:
| 配置 | 作用 |
|---|---|
| processor.genPath | 指定生成代码目录 |
| processor.tableDef.package | 指定表定义类包名 |
| processor.tableDef.propertiesNameStyle | 指定字段风格 |
| processor.mapper.generateEnable | 是否生成 Mapper |
默认字段风格通常是大写下划线:
1USER.ID 2USER.USERNAME 3USER.CREATED_AT 4
如果项目希望使用驼峰字段,也可以调整配置。
逻辑删除
逻辑删除是把删除动作变成更新状态。
实体字段:
1@Column(isLogicDelete = true) 2private Boolean deleted; 3
执行:
1userMapper.deleteById(1L); 2
实际效果类似:
1update tb_user 2set deleted = 1 3where id = ? 4 and deleted = 0 5
查询时会自动过滤已删除数据。
也就是说:
1userMapper.selectOneById(1L); 2
会带上:
1deleted = 0 2
逻辑删除适合用户、订单、文章这类需要保留历史记录的数据。
对于日志、临时数据、中间表,是否需要逻辑删除要看业务要求。
乐观锁
乐观锁适合防止并发修改覆盖。
表里增加版本字段:
1ALTER TABLE tb_user ADD COLUMN version INT NOT NULL DEFAULT 0; 2
实体字段:
1@Column(version = true) 2private Integer version; 3
更新时会根据版本号判断数据是否被其他事务改过。
大致逻辑是:
1更新条件里带上旧 version 2更新成功后 version 增加 3如果影响行数为 0,说明版本不匹配 4
这种能力常用于库存、余额、配置修改等场景。
数据填充
新增和修改时,经常要处理这些字段:
1created_at 2updated_at 3created_by 4updated_by 5
可以在 Service 层手动设置:
1user.setCreatedAt(LocalDateTime.now()); 2user.setUpdatedAt(LocalDateTime.now()); 3
也可以使用 MyBatis-Flex 的数据填充能力统一处理。
项目较小时,手动设置足够清楚。
项目字段规范较统一时,统一填充更省维护成本。
自定义 SQL
MyBatis-Flex 不影响 MyBatis 原生写法。
Mapper 接口里仍然可以写自定义方法:
1public interface UserMapper extends BaseMapper<User> { 2 List<User> selectActiveUsersByKeyword(String keyword); 3} 4
XML:
1<select id="selectActiveUsersByKeyword" resultType="com.example.demo.entity.User"> 2 select id, username, email, age, status, deleted, created_at, updated_at 3 from tb_user 4 where deleted = 0 5 and status = 'ACTIVE' 6 and ( 7 username like concat('%', #{keyword}, '%') 8 or email like concat('%', #{keyword}, '%') 9 ) 10 order by id desc 11</select> 12
所以不是所有 SQL 都要改成 QueryWrapper。
简单条件用 QueryWrapper 很舒服。
复杂报表、复杂统计、多层子查询,用 XML 仍然很合适。
和 JdbcTemplate、MyBatis、MyBatis-Plus 的区别
| 对比项 | JdbcTemplate | MyBatis | MyBatis-Plus | MyBatis-Flex |
|---|---|---|---|---|
| SQL 控制 | 很直接 | 很直接 | 较直接 | 很直接 |
| 单表 CRUD | 手写 | 手写 | 内置 | 内置 |
| 链式查询 | 无 | 无 | 有 | 有 |
| 多表查询 | 手写 SQL | 手写 SQL | 通常手写 | QueryWrapper 支持较灵活 |
| XML 支持 | 无 | 支持 | 支持 | 支持 |
| 学习成本 | 低 | 中等 | 中等 | 中等 |
| 适合场景 | 少量 SQL、内部工具 | SQL 控制要求高 | 常规后台 CRUD | 需要轻量增强和灵活查询 |
粗略理解:
1JdbcTemplate 更接近原生 JDBC 2MyBatis 更像 SQL 映射工具 3MyBatis-Plus 更偏常规 CRUD 增强 4MyBatis-Flex 更偏轻量增强 + 灵活 QueryWrapper 5
常见使用建议
Mapper 只放数据库操作
Mapper 层适合放:
- 基础 CRUD
- 自定义查询方法
- 和数据库强相关的操作
业务判断、事务流程、多个 Mapper 的组合,更适合放在 Service 层。
QueryWrapper 适合中等复杂度查询
适合写成 QueryWrapper 的查询:
- 单表条件查询
- 简单多条件筛选
- 简单 join
- 分页列表
- 动态条件查询
更适合写 XML 的查询:
- 大量聚合统计
- 多层嵌套子查询
- 复杂报表
- 数据库特有语法较多的 SQL
表定义类生成失败先查 APT
如果 USER、ORDER 这类表定义对象不存在,优先检查 APT。
这类问题通常不是 Mapper 或 SQL 问题,而是编译期生成代码没有生效。
逻辑删除字段保持统一
逻辑删除字段最好在项目里统一命名。
比如统一使用:
1deleted 2
或者:
1is_delete 2
字段名统一后,实体配置、全局配置、SQL 阅读都会更清楚。
批量操作注意分批
insertBatch 很方便,但大批量数据仍然需要分批。
如果一次性插入几十万条,容易带来 SQL 太长、事务太大、锁持有时间过长等问题。
常见做法是:
1每 500 条或 1000 条拆成一批。 2
保留 MyBatis 原生能力
MyBatis-Flex 是增强,不是替换所有写法。
项目里可以同时存在:
BaseMapper通用 CRUDQueryWrapper条件查询- XML 自定义 SQL
- 注解 SQL
按场景选择即可。
常用方法汇总
| 方法 | 作用 | 常见场景 |
|---|---|---|
| insert(entity) | 新增数据 | 创建用户、创建订单 |
| insertBatch(list) | 批量新增 | 批量导入 |
| update(entity) | 根据实体主键更新 | 修改单条记录 |
| updateByQuery(entity, query) | 按条件更新 | 批量修改状态 |
| deleteById(id) | 按主键删除 | 删除单条记录 |
| deleteByQuery(query) | 按条件删除 | 批量删除或逻辑删除 |
| selectOneById(id) | 按主键查询 | 详情页 |
| selectOneByQuery(query) | 按条件查询一条 | 唯一字段查询 |
| selectListByQuery(query) | 按条件查询列表 | 列表页、筛选 |
| selectAll() | 查询全部 | 小字典表 |
| selectCountByQuery(query) | 查询数量 | 统计条数 |
| paginate(pageNumber, pageSize, query) | 分页查询 | 后台列表 |
| selectListByQueryAs(query, DTO.class) | 查询并映射 DTO | 多表关联查询 |
总结
MyBatis-Flex 的核心并不复杂。
最常用的组合就是:
1Entity 使用 @Table 和 @Id 2Mapper 继承 BaseMapper 3查询条件使用 QueryWrapper 4分页使用 paginate 5复杂 SQL 继续使用 MyBatis XML 6
它适合这些场景:
- 项目已经熟悉 MyBatis
- 希望保留 SQL 控制权
- 单表 CRUD 比较多
- 动态条件查询比较多
- 不想为简单查询写大量 XML
- 需要逻辑删除、分页、乐观锁等常用能力
落地时重点关注三件事:
- 依赖版本和 Spring Boot 版本要匹配
- APT 要正常生成表定义类
QueryWrapper和 XML 按复杂度分工
掌握这些内容后,MyBatis-Flex 已经可以覆盖大多数后台系统的数据访问层开发。
《Java MyBatis-Flex 实战指南:从 BaseMapper 到 QueryWrapper 的轻量 ORM 用法》 是转载文章,点击查看原文。