Java MyBatis-Flex 实战指南:从 BaseMapper 到 QueryWrapper 的轻量 ORM 用法

作者:唐青枫日期:2026/6/3

简介

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 是什么

QueryWrapperMyBatis-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 里找不到 UserTableDefUSER,一般检查这些地方:

  • mybatis-flex-processor 是否配置到 annotationProcessorPaths
  • Maven 是否执行过 compilepackage
  • 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 的区别

对比项JdbcTemplateMyBatisMyBatis-PlusMyBatis-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

如果 USERORDER 这类表定义对象不存在,优先检查 APT。

这类问题通常不是 Mapper 或 SQL 问题,而是编译期生成代码没有生效。

逻辑删除字段保持统一

逻辑删除字段最好在项目里统一命名。

比如统一使用:

1deleted
2

或者:

1is_delete
2

字段名统一后,实体配置、全局配置、SQL 阅读都会更清楚。

批量操作注意分批

insertBatch 很方便,但大批量数据仍然需要分批。

如果一次性插入几十万条,容易带来 SQL 太长、事务太大、锁持有时间过长等问题。

常见做法是:

1 500 条或 1000 条拆成一批。
2

保留 MyBatis 原生能力

MyBatis-Flex 是增强,不是替换所有写法。

项目里可以同时存在:

  • BaseMapper 通用 CRUD
  • QueryWrapper 条件查询
  • 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 用法》 是转载文章,点击查看原文


相关推荐


我用AI一小时撸了个单词学习站,每天自动生成5个单词
修己xj2026/5/26

AI编程热度居高不下,我也一直在用CodeBuddy辅助开发。最近发现了一个有意思的功能——Skill(技能包),灵机一动:不如写个技能,每天自动生成英文单词并部署成网页,这样打开浏览器就能学。 说干就干,全程AI辅助,一个多小时就搞定了。过程分享给大家。 以下是项目地址及在线地址 在线访问:xiuji008.github.io/everyday-en… 项目完全开源:github.com/xiuji008/ev… 灵感来源 CodeBuddy的Skill功能可以定义一套自动化流程。比如我说“


claudeCode+DeepSeekV4pro[1m]
冰镇生鲜2026/5/5

Claude Code 完美调和配置 这是经过多轮磨合后的全局配置,可以直接复制到新电脑的 ~/.claude/settings.json 使用。 也可以直接让 AI 处理一切。 完整配置文件 ~/.claude/settings.json { "env": { "ANTHROPIC_AUTH_TOKEN": "sk-你的token", "ANTHROPIC_BASE_URL": "https://api.deepseek.com/anthropic", "AN


【AI Agent | 第七篇】Skill的使用:将经验沉淀成可复用工作流
程序员夏末2026/4/25

前言   此文是我近期学习Agent中关于Skill使用的一点心得,结合AI整理总结而来,分享给大家。一是为了记录,二是用于日后的回顾,三也是希望能给其他初学者带来一点点帮助。 目录 1. 为什么我会开始关注 Skill?2. Skill 到底解决了什么问题?3. 从写博客这个例子理解 Skill 的价值4. 一个 Skill 通常包含哪些内容?5. Skill 使用时会加载哪些上下文?6. 写 Skill 时我踩到的几个细节7. 什么样的工作流适合沉淀成 Skill?8. 总结


【AI工具篇】Windows 安装 WSL 全攻略:wsl --install 一键部署 + VSCode 搭配使用好处详解
*星星之火*2026/4/16

Windows 安装 WSL 全攻略:wsl --install 一键部署 + VSCode 搭配使用好处详解 前言 在 Windows 上做开发,尤其是后端、C/C++、Python、Docker、机器学习等开发时,经常会遇到环境不一致、命令不兼容、依赖难装等问题。 传统虚拟机笨重卡顿,双系统切换麻烦,而 WSL(Windows Subsystem for Linux) 完美解决了这些痛点。 本文详细介绍: Windows 安装 WSL 的好处一条命令 wsl --install 完成安装V


【docker】Ubuntu22使用skopeo离线推送镜像
s9123601012026/4/8

1,准备安装skopeo # 安装skopeo apt install skopeo --fix-missing # 创建目录 mkdir -p skopeo_bundle # 拷贝主要的 cp /usr/bin/skopeo skopeo_bundle/ # 下载依赖库 ldd /usr/bin/skopeo | awk '{print $3}' | grep '/' | xargs -I '{}' cp -v '{}' skopeo_bundle/ # 压缩 tar czf skopeo


极速上手:Puppeteer + 原生代理IP 突破无头检测(金融与突发新闻抓取 Cheat Sheet)
亿牛云爬虫专家2026/3/31

在金融量化分析、宏观经济数据追踪或突发新闻监控等场景中,数据价值随时间呈指数级衰减。高频并发抓取极易触发目标网站的反爬策略(如 Cloudflare 盾、无头浏览器指纹识别)以及严苛的 IP 封禁。 终极解法: 使用 puppeteer-extra-plugin-stealth 抹平自动化指纹,配合 爬虫原生代理IP 进行高匿 IP 轮换。本文提供可直接用于生产环境的配置清单与核心业务代码。 核心优势:为什么金融与突发新闻需要“即时采集”? 在讲技术实现之前,我们需要明确高频即时采集的不可


【Web】使用Vue3+PlayCanvas开发3D游戏(五)3D模型鼠标交互控制
沙振宇2026/3/23

文章目录 一、效果二、简介三、知识点3.1、核心交互需求分析3.2、基础准备:变量定义3.3、工具函数封装3.3.1、角度转弧度(原生实现)3.3.2、相机位置更新函数3.3.3、视角重置函数(双击触发) 3.4、鼠标交互核心逻辑实现3.4.1、初始化鼠标事件监听3.4.2、关键逻辑说明 3.5、集成到初始化流程3.6、生命周期清理 四、完整源码 一、效果 二、简介 在《【Web】使用 Vue3+PlayCanvas 开发 3D 游戏(四)3D 障碍物躲避游戏 2


pycharm创建桌面时间控件小程序
chushiyunen不懂代码的小白2026/3/15

文章目录 步骤 主要是为了走一遍python创建exe的流程。 步骤 1、新建一个项目,名称为:desktop_widget 2、创建一个python文件,名称为:desktop_widget.py,内容如下: import tkinter as tk from datetime import datetime class DesktopWidget: def __init__(self): self.root = tk.Tk()


Vue3开发 First Internship
午安~婉2026/3/6

#记录在2025.12-2026.3,从一个Vue新手到能独立开发项目的成长历程,包含大量实战中遇到的问题和解决方案。第一段实习总结。 #时间:2026.3.3 目录 一.项目环境与工具 二.Git版本控制实战 三.Vue3核心知识 四、路由与状态管理 五、CSS与布局技巧 六、性能优化 七、移动端开发 八、调试与部署 九、开发工具与插件 十、常见问题与解决方案 一.项目环境与工具 1.1 开发模式与生产模式的区别 开发流程:pnpm run dev→ vite


【分布式组件雪花ID】
老友記2026/2/26

分布式组件雪花ID 组成时钟回拨解决方案汇总方案一:等待后重试(阻塞等待)方案二:预留回拨位(占用序列号位)1. "预留回拨位"的核心思想2. 位分配对比图3. 具体工作场景模拟正常情况(时间向前走):发生时钟回拨(时间从1000跳回999): 4. 这种方案的优缺点5. 位运算代码示意(Java) 方案三:采用"未生成ID最大上限"自动漂移方案四:外部存储兜底(依赖Redis/ZooKeeper) 组成 雪花ID(Snowflake ID)的生成规则,核心

首页编辑器站点地图

本站内容在 CC BY-SA 4.0 协议下发布

Copyright © 2026 聚合阅读