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

0 阅读15分钟

简介

MyBatis-Flex 是一个基于 MyBatis 的增强框架。

它的定位很直接:

保留 MyBatis 写 SQL 的灵活性,同时补上通用 CRUD、链式查询、分页、逻辑删除、乐观锁等常用能力。

普通 MyBatis 项目里,常见代码结构是:

Entity
Mapper 接口
Mapper XML
Service
Controller

如果只是做一张表的增删改查,也要写不少重复 SQL。

MyBatis-Flex 的思路是:

简单 CRUD 交给 BaseMapper
复杂条件交给 QueryWrapper
特别复杂的 SQL 仍然可以回到 MyBatis 原生 XML 或注解 SQL

一句话概括:

MyBatis-Flex 是一个轻量的 MyBatis 增强工具,适合需要 SQL 控制感,又想减少重复 CRUD 代码的项目。

MyBatis-Flex 解决什么问题

先看普通 MyBatis 的单表查询。

Mapper 接口:

public interface UserMapper {
    User selectById(Long id);
}

XML:

<select id="selectById" resultType="com.example.entity.User">
    select id, username, email, age, status, created_at
    from tb_user
    where id = #{id}
</select>

新增、修改、删除、分页、条件查询都继续写 SQL。

这种方式很灵活,但简单操作会显得重复。

MyBatis-Flex 的写法是:

public interface UserMapper extends BaseMapper<User> {
}

然后直接使用:

User user = userMapper.selectOneById(1L);

这类基础方法由 BaseMapper 提供。

如果需要条件查询:

QueryWrapper query = QueryWrapper.create()
        .where(USER.AGE.ge(18))
        .and(USER.STATUS.eq("ACTIVE"))
        .orderBy(USER.ID.desc());

List<User> users = userMapper.selectListByQuery(query);

这就是 MyBatis-Flex 的核心体验:

单表 CRUD 少写 SQL
条件查询用链式 API
复杂 SQL 仍然保留 MyBatis 原生能力

核心概念

学习 MyBatis-Flex 时,先抓住几个关键词。

名称作用
@Table标记实体类对应的数据库表
@Id标记主键字段
@Column配置字段映射、逻辑删除、忽略字段等
BaseMapper<T>通用 Mapper,提供基础增删改查
QueryWrapper链式 SQL 构造器
Page<T>分页结果对象
Db + Row不依赖实体类的数据库操作方式
APT编译期生成表定义类,比如 USERACCOUNT

其中最常用的是:

@Table + @Id + BaseMapper + QueryWrapper

Maven 依赖

MyBatis-Flex 需要根据 Spring Boot 版本选择不同 starter。

Spring Boot 2.x

<dependency>
    <groupId>com.mybatis-flex</groupId>
    <artifactId>mybatis-flex-spring-boot-starter</artifactId>
    <version>1.11.7</version>
</dependency>

Spring Boot 3.x

<dependency>
    <groupId>com.mybatis-flex</groupId>
    <artifactId>mybatis-flex-spring-boot3-starter</artifactId>
    <version>1.11.7</version>
</dependency>

数据库驱动以 MySQL 为例:

<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <scope>runtime</scope>
</dependency>

APT 处理器建议放到 annotationProcessorPaths 里:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.11.0</version>
    <configuration>
        <annotationProcessorPaths>
            <path>
                <groupId>com.mybatis-flex</groupId>
                <artifactId>mybatis-flex-processor</artifactId>
                <version>1.11.7</version>
            </path>
        </annotationProcessorPaths>
    </configuration>
</plugin>

APT 用来生成表定义类。

比如实体类叫 User,编译后会生成类似这样的类:

UserTableDef

代码里就可以静态导入:

import static com.example.entity.table.UserTableDef.USER;

然后写出:

USER.ID.eq(1L)
USER.USERNAME.like("张")

数据源配置

application.yml 示例:

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/flex_demo?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver

mybatis-flex:
  print-banner: false
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

开发环境可以打开 SQL 日志,便于观察生成的 SQL。

生产环境一般会交给日志系统统一管理。

启动类配置

启动类需要扫描 Mapper。

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@MapperScan("com.example.demo.mapper")
public class MyBatisFlexDemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(MyBatisFlexDemoApplication.class, args);
    }
}

也可以在每个 Mapper 接口上加 @Mapper

项目里 Mapper 较多时,@MapperScan 更省事。

准备演示表

下面用一个用户表做示例。

DROP TABLE IF EXISTS tb_user;

CREATE TABLE tb_user (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  username VARCHAR(50) NOT NULL,
  email VARCHAR(100) NOT NULL,
  age INT NOT NULL,
  status VARCHAR(20) NOT NULL,
  deleted TINYINT NOT NULL DEFAULT 0,
  created_at DATETIME NOT NULL,
  updated_at DATETIME NULL
);

INSERT INTO tb_user (username, email, age, status, deleted, created_at, updated_at) VALUES
('张三', 'zhangsan@example.com', 20, 'ACTIVE', 0, '2026-01-01 10:00:00', NULL),
('李四', 'lisi@example.com', 25, 'ACTIVE', 0, '2026-01-02 10:00:00', NULL),
('王五', 'wangwu@example.com', 17, 'DISABLED', 0, '2026-01-03 10:00:00', NULL);

实体类

package com.example.demo.entity;

import com.mybatisflex.annotation.Column;
import com.mybatisflex.annotation.Id;
import com.mybatisflex.annotation.Table;
import com.mybatisflex.core.keygen.KeyType;

import java.time.LocalDateTime;

@Table("tb_user")
public class User {

    @Id(keyType = KeyType.Auto)
    private Long id;

    private String username;

    private String email;

    private Integer age;

    private String status;

    @Column(isLogicDelete = true)
    private Boolean deleted;

    private LocalDateTime createdAt;

    private LocalDateTime updatedAt;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public String getStatus() {
        return status;
    }

    public void setStatus(String status) {
        this.status = status;
    }

    public Boolean getDeleted() {
        return deleted;
    }

    public void setDeleted(Boolean deleted) {
        this.deleted = deleted;
    }

    public LocalDateTime getCreatedAt() {
        return createdAt;
    }

    public void setCreatedAt(LocalDateTime createdAt) {
        this.createdAt = createdAt;
    }

    public LocalDateTime getUpdatedAt() {
        return updatedAt;
    }

    public void setUpdatedAt(LocalDateTime updatedAt) {
        this.updatedAt = updatedAt;
    }
}

几个重点:

  • @Table("tb_user"):实体类对应 tb_user
  • @Id(keyType = KeyType.Auto):主键由数据库自增生成
  • @Column(isLogicDelete = true):标记逻辑删除字段
  • created_at 会自动映射到 createdAt

Mapper 接口

package com.example.demo.mapper;

import com.example.demo.entity.User;
import com.mybatisflex.core.BaseMapper;

public interface UserMapper extends BaseMapper<User> {
}

继承 BaseMapper<User> 后,常见 CRUD 方法就可以直接使用。

新增数据

User user = new User();
user.setUsername("赵六");
user.setEmail("zhaoliu@example.com");
user.setAge(28);
user.setStatus("ACTIVE");
user.setDeleted(false);
user.setCreatedAt(LocalDateTime.now());

userMapper.insert(user);

System.out.println(user.getId());

如果主键配置为自增,插入后主键会回填到实体对象。

常见生成 SQL 大致是:

insert into tb_user (username, email, age, status, deleted, created_at)
values (?, ?, ?, ?, ?, ?)

根据 ID 查询

User user = userMapper.selectOneById(1L);

对应场景:

通过主键查一条数据。

如果实体配置了逻辑删除字段,查询会自动带上未删除条件。

查询全部

List<User> users = userMapper.selectAll();

实际项目中,selectAll 更适合数据量很小的字典表、配置表。

业务表数据量较大时,通常使用条件查询或分页查询。

修改数据

User user = new User();
user.setId(1L);
user.setEmail("new-zhangsan@example.com");
user.setUpdatedAt(LocalDateTime.now());

userMapper.update(user);

这类写法适合根据实体主键更新。

如果只想更新某些字段,实体里只设置需要修改的字段更清楚。

删除数据

userMapper.deleteById(1L);

如果实体类没有配置逻辑删除,这就是物理删除。

如果实体类配置了:

@Column(isLogicDelete = true)
private Boolean deleted;

删除会变成更新逻辑删除字段。

大致 SQL 是:

update tb_user
set deleted = 1
where id = ?
  and deleted = 0

逻辑删除后的数据,普通查询会自动过滤。

QueryWrapper 是什么

QueryWrapperMyBatis-Flex 里非常核心的查询构造器。

它用来在 Java 代码里组装 SQL。

普通 SQL:

select *
from tb_user
where age >= 18
  and status = 'ACTIVE'
order by id desc

QueryWrapper 写法:

QueryWrapper query = QueryWrapper.create()
        .where(USER.AGE.ge(18))
        .and(USER.STATUS.eq("ACTIVE"))
        .orderBy(USER.ID.desc());

List<User> users = userMapper.selectListByQuery(query);

这里的 USER 来自 APT 生成的表定义类。

常见静态导入:

import static com.example.demo.entity.table.UserTableDef.USER;

常见条件写法

等于:

USER.STATUS.eq("ACTIVE")

不等于:

USER.STATUS.ne("DISABLED")

大于:

USER.AGE.gt(18)

大于等于:

USER.AGE.ge(18)

小于:

USER.AGE.lt(60)

模糊查询:

USER.USERNAME.like("张")

范围查询:

USER.AGE.between(18, 30)

IN 查询:

USER.ID.in(1L, 2L, 3L)

排序:

orderBy(USER.ID.desc())

动态条件查询

业务接口里经常会有可选筛选条件。

比如:

username 可传可不传
status 可传可不传
minAge 可传可不传

可以这样写:

public List<User> search(String username, String status, Integer minAge) {
    QueryWrapper query = QueryWrapper.create()
            .select()
            .from(USER);

    if (username != null && !username.isBlank()) {
        query.and(USER.USERNAME.like(username));
    }

    if (status != null && !status.isBlank()) {
        query.and(USER.STATUS.eq(status));
    }

    if (minAge != null) {
        query.and(USER.AGE.ge(minAge));
    }

    query.orderBy(USER.ID.desc());

    return userMapper.selectListByQuery(query);
}

这种写法比手动拼 SQL 字符串更稳。

参数仍然会走预编译绑定,不需要把业务值直接拼到 SQL 里。

查询单个对象

按唯一字段查询:

public User findByEmail(String email) {
    QueryWrapper query = QueryWrapper.create()
            .where(USER.EMAIL.eq(email));

    return userMapper.selectOneByQuery(query);
}

如果数据可能不存在,业务层可以包一层 Optional

public Optional<User> findOptionalByEmail(String email) {
    QueryWrapper query = QueryWrapper.create()
            .where(USER.EMAIL.eq(email));

    User user = userMapper.selectOneByQuery(query);
    return Optional.ofNullable(user);
}

查询部分字段

有些接口不需要返回整张表的所有字段。

QueryWrapper query = QueryWrapper.create()
        .select(USER.ID, USER.USERNAME, USER.EMAIL)
        .from(USER)
        .where(USER.STATUS.eq("ACTIVE"));

List<User> users = userMapper.selectListByQuery(query);

这段 SQL 只会查询指定字段。

字段越少,网络传输和对象映射压力越小。

查询单个字段

统计数量:

QueryWrapper query = QueryWrapper.create()
        .where(USER.STATUS.eq("ACTIVE"));

long count = userMapper.selectCountByQuery(query);

查询第一列:

QueryWrapper query = QueryWrapper.create()
        .select(USER.EMAIL)
        .from(USER)
        .where(USER.ID.eq(1L));

String email = userMapper.selectObjectByQueryAs(query, String.class);

这类写法适合只取一个字段的场景。

分页查询

BaseMapper 提供了 paginate 方法。

import com.mybatisflex.core.paginate.Page;

public Page<User> pageActiveUsers(int pageNumber, int pageSize) {
    QueryWrapper query = QueryWrapper.create()
            .where(USER.STATUS.eq("ACTIVE"))
            .orderBy(USER.ID.desc());

    return userMapper.paginate(pageNumber, pageSize, query);
}

返回值是 Page<User>

常用字段:

Page<User> page = pageActiveUsers(1, 10);

List<User> records = page.getRecords();
long totalRow = page.getTotalRow();
long totalPage = page.getTotalPage();

分页参数含义:

pageNumber:当前页,从 1 开始
pageSize:每页条数

第二页以后复用总数

分页查询通常会查两次:

一次查总数
一次查当前页数据

如果第一页已经拿到总数,第二页以后可以把总数传进去,减少一次 count 查询。

long totalRow = 120L;

Page<User> page = userMapper.paginate(2, 10, totalRow, query);

这个写法适合对性能比较敏感的列表页。

多表查询

准备一张订单表:

DROP TABLE IF EXISTS tb_order;

CREATE TABLE tb_order (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  user_id BIGINT NOT NULL,
  order_no VARCHAR(50) NOT NULL,
  amount DECIMAL(10, 2) NOT NULL,
  status VARCHAR(20) NOT NULL,
  created_at DATETIME NOT NULL
);

实体类:

@Table("tb_order")
public class Order {

    @Id(keyType = KeyType.Auto)
    private Long id;

    private Long userId;

    private String orderNo;

    private BigDecimal amount;

    private String status;

    private LocalDateTime createdAt;

    // getter setter
}

Mapper:

public interface OrderMapper extends BaseMapper<Order> {
}

定义一个 DTO 接收结果:

public class OrderUserDTO {
    private Long orderId;
    private String orderNo;
    private BigDecimal amount;
    private String orderStatus;
    private Long userId;
    private String username;
    private String email;

    // getter setter
}

关联查询:

import static com.example.demo.entity.table.OrderTableDef.ORDER;
import static com.example.demo.entity.table.UserTableDef.USER;

public List<OrderUserDTO> findOrderUsers(String orderStatus) {
    QueryWrapper query = QueryWrapper.create()
            .select(
                    ORDER.ID.as("orderId"),
                    ORDER.ORDER_NO.as("orderNo"),
                    ORDER.AMOUNT,
                    ORDER.STATUS.as("orderStatus"),
                    USER.ID.as("userId"),
                    USER.USERNAME.as("username"),
                    USER.EMAIL.as("email")
            )
            .from(ORDER)
            .leftJoin(USER).on(ORDER.USER_ID.eq(USER.ID))
            .where(ORDER.STATUS.eq(orderStatus))
            .orderBy(ORDER.ID.desc());

    return orderMapper.selectListByQueryAs(query, OrderUserDTO.class);
}

这种写法适合:

SQL 不算特别复杂,但又不是简单单表查询。

如果 SQL 已经非常复杂,写 XML 也很正常。

MyBatis-Flex 不限制继续使用 MyBatis 原生 XML。

批量插入

List<User> users = new ArrayList<>();

User user1 = new User();
user1.setUsername("用户1");
user1.setEmail("user1@example.com");
user1.setAge(18);
user1.setStatus("ACTIVE");
user1.setDeleted(false);
user1.setCreatedAt(LocalDateTime.now());
users.add(user1);

User user2 = new User();
user2.setUsername("用户2");
user2.setEmail("user2@example.com");
user2.setAge(19);
user2.setStatus("ACTIVE");
user2.setDeleted(false);
user2.setCreatedAt(LocalDateTime.now());
users.add(user2);

userMapper.insertBatch(users);

批量操作适合一次写入多条数据。

数据量很大时,可以按固定大小拆批。

例如每 500 条或 1000 条执行一次。

条件删除

删除禁用状态用户:

QueryWrapper query = QueryWrapper.create()
        .where(USER.STATUS.eq("DISABLED"));

userMapper.deleteByQuery(query);

如果配置了逻辑删除,这里会执行逻辑删除。

如果没有配置逻辑删除,就会执行物理删除。

条件更新

一些更新不方便先查出来再改,可以直接按条件更新。

User updateEntity = new User();
updateEntity.setStatus("DISABLED");
updateEntity.setUpdatedAt(LocalDateTime.now());

QueryWrapper query = QueryWrapper.create()
        .where(USER.AGE.lt(18));

userMapper.updateByQuery(updateEntity, query);

含义是:

把年龄小于 18 的用户状态改成 DISABLED。

Active Record

MyBatis-Flex 也支持 Active Record 风格。

实体类继承 Model 后,可以直接调用保存、更新等方法。

import com.mybatisflex.core.activerecord.Model;

@Table("tb_user")
public class User extends Model<User> {
    @Id(keyType = KeyType.Auto)
    private Long id;

    private String username;

    // getter setter
}

使用:

User user = new User();
user.setUsername("张三");
user.save();

这种写法很短。

但在分层清晰的业务系统里,更常见的是:

Controller -> Service -> Mapper

Active Record 更适合简单模块、脚本工具、管理后台里的少量操作。

Db + Row

Db + Row 适合不想定义实体类的场景。

比如临时查询、后台工具、通用管理功能。

import com.mybatisflex.core.row.Db;
import com.mybatisflex.core.row.Row;

Row row = Db.selectOneById("tb_user", "id", 1L);

String username = row.getString("username");
Integer age = row.getInt("age");

查询列表:

List<Row> rows = Db.selectListBySql(
        "select id, username, email from tb_user where status = ?",
        "ACTIVE"
);

Db + Row 很灵活,但类型约束弱。

核心业务代码里,实体类和 Mapper 更容易维护。

Service 层封装

下面是一个比较完整的用户服务示例。

package com.example.demo.service;

import com.example.demo.entity.User;
import com.example.demo.mapper.UserMapper;
import com.mybatisflex.core.paginate.Page;
import com.mybatisflex.core.query.QueryWrapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;

import static com.example.demo.entity.table.UserTableDef.USER;

@Service
public class UserService {

    private final UserMapper userMapper;

    public UserService(UserMapper userMapper) {
        this.userMapper = userMapper;
    }

    @Transactional
    public Long create(User user) {
        user.setStatus("ACTIVE");
        user.setDeleted(false);
        user.setCreatedAt(LocalDateTime.now());

        userMapper.insert(user);

        return user.getId();
    }

    public Optional<User> findById(Long id) {
        return Optional.ofNullable(userMapper.selectOneById(id));
    }

    public List<User> search(String username, String status, Integer minAge) {
        QueryWrapper query = QueryWrapper.create()
                .select()
                .from(USER);

        if (username != null && !username.isBlank()) {
            query.and(USER.USERNAME.like(username));
        }

        if (status != null && !status.isBlank()) {
            query.and(USER.STATUS.eq(status));
        }

        if (minAge != null) {
            query.and(USER.AGE.ge(minAge));
        }

        query.orderBy(USER.ID.desc());

        return userMapper.selectListByQuery(query);
    }

    public Page<User> page(String status, int pageNumber, int pageSize) {
        QueryWrapper query = QueryWrapper.create()
                .where(USER.STATUS.eq(status))
                .orderBy(USER.ID.desc());

        return userMapper.paginate(pageNumber, pageSize, query);
    }

    @Transactional
    public void updateEmail(Long id, String email) {
        User user = new User();
        user.setId(id);
        user.setEmail(email);
        user.setUpdatedAt(LocalDateTime.now());

        userMapper.update(user);
    }

    @Transactional
    public void disable(Long id) {
        User user = new User();
        user.setId(id);
        user.setStatus("DISABLED");
        user.setUpdatedAt(LocalDateTime.now());

        userMapper.update(user);
    }

    @Transactional
    public void remove(Long id) {
        userMapper.deleteById(id);
    }
}

这里把事务放在 Service 层。

Mapper 只负责数据库操作。

Service 负责业务规则和事务边界。

Controller 示例

package com.example.demo.controller;

import com.example.demo.entity.User;
import com.example.demo.service.UserService;
import com.mybatisflex.core.paginate.Page;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequestMapping("/users")
public class UserController {

    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    @PostMapping
    public Long create(@RequestBody User user) {
        return userService.create(user);
    }

    @GetMapping("/{id}")
    public User detail(@PathVariable Long id) {
        return userService.findById(id)
                .orElseThrow(() -> new IllegalArgumentException("用户不存在"));
    }

    @GetMapping("/search")
    public List<User> search(@RequestParam(required = false) String username,
                             @RequestParam(required = false) String status,
                             @RequestParam(required = false) Integer minAge) {
        return userService.search(username, status, minAge);
    }

    @GetMapping
    public Page<User> page(@RequestParam(defaultValue = "ACTIVE") String status,
                           @RequestParam(defaultValue = "1") int pageNumber,
                           @RequestParam(defaultValue = "10") int pageSize) {
        return userService.page(status, pageNumber, pageSize);
    }

    @PutMapping("/{id}/email")
    public void updateEmail(@PathVariable Long id, @RequestParam String email) {
        userService.updateEmail(id, email);
    }

    @PutMapping("/{id}/disable")
    public void disable(@PathVariable Long id) {
        userService.disable(id);
    }

    @DeleteMapping("/{id}")
    public void remove(@PathVariable Long id) {
        userService.remove(id);
    }
}

这组接口覆盖了:

  • 新增用户
  • 按 ID 查询
  • 多条件查询
  • 分页查询
  • 修改邮箱
  • 禁用用户
  • 删除用户

APT 生成类找不到怎么办

QueryWrapper 里常见的:

USER.ID.eq(1L)

依赖 APT 生成的表定义类。

如果 IDE 里找不到 UserTableDefUSER,一般检查这些地方:

  • mybatis-flex-processor 是否配置到 annotationProcessorPaths
  • Maven 是否执行过 compilepackage
  • IDE 是否开启 Annotation Processing
  • target/generated-sources/annotations 是否被识别为 generated sources
  • 静态导入路径是否正确

默认情况下,生成类通常在:

target/generated-sources/annotations

实体类包名如果是:

com.example.demo.entity

生成类包名通常类似:

com.example.demo.entity.table

自定义 APT 配置

可以在项目根目录创建:

mybatis-flex.config

示例:

processor.tableDef.propertiesNameStyle=upperCase
processor.tableDef.package=${entityPackage}.table
processor.mapper.generateEnable=false

常用配置含义:

配置作用
processor.genPath指定生成代码目录
processor.tableDef.package指定表定义类包名
processor.tableDef.propertiesNameStyle指定字段风格
processor.mapper.generateEnable是否生成 Mapper

默认字段风格通常是大写下划线:

USER.ID
USER.USERNAME
USER.CREATED_AT

如果项目希望使用驼峰字段,也可以调整配置。

逻辑删除

逻辑删除是把删除动作变成更新状态。

实体字段:

@Column(isLogicDelete = true)
private Boolean deleted;

执行:

userMapper.deleteById(1L);

实际效果类似:

update tb_user
set deleted = 1
where id = ?
  and deleted = 0

查询时会自动过滤已删除数据。

也就是说:

userMapper.selectOneById(1L);

会带上:

deleted = 0

逻辑删除适合用户、订单、文章这类需要保留历史记录的数据。

对于日志、临时数据、中间表,是否需要逻辑删除要看业务要求。

乐观锁

乐观锁适合防止并发修改覆盖。

表里增加版本字段:

ALTER TABLE tb_user ADD COLUMN version INT NOT NULL DEFAULT 0;

实体字段:

@Column(version = true)
private Integer version;

更新时会根据版本号判断数据是否被其他事务改过。

大致逻辑是:

更新条件里带上旧 version
更新成功后 version 增加
如果影响行数为 0,说明版本不匹配

这种能力常用于库存、余额、配置修改等场景。

数据填充

新增和修改时,经常要处理这些字段:

created_at
updated_at
created_by
updated_by

可以在 Service 层手动设置:

user.setCreatedAt(LocalDateTime.now());
user.setUpdatedAt(LocalDateTime.now());

也可以使用 MyBatis-Flex 的数据填充能力统一处理。

项目较小时,手动设置足够清楚。

项目字段规范较统一时,统一填充更省维护成本。

自定义 SQL

MyBatis-Flex 不影响 MyBatis 原生写法。

Mapper 接口里仍然可以写自定义方法:

public interface UserMapper extends BaseMapper<User> {
    List<User> selectActiveUsersByKeyword(String keyword);
}

XML:

<select id="selectActiveUsersByKeyword" resultType="com.example.demo.entity.User">
    select id, username, email, age, status, deleted, created_at, updated_at
    from tb_user
    where deleted = 0
      and status = 'ACTIVE'
      and (
        username like concat('%', #{keyword}, '%')
        or email like concat('%', #{keyword}, '%')
      )
    order by id desc
</select>

所以不是所有 SQL 都要改成 QueryWrapper

简单条件用 QueryWrapper 很舒服。

复杂报表、复杂统计、多层子查询,用 XML 仍然很合适。

和 JdbcTemplate、MyBatis、MyBatis-Plus 的区别

对比项JdbcTemplateMyBatisMyBatis-PlusMyBatis-Flex
SQL 控制很直接很直接较直接很直接
单表 CRUD手写手写内置内置
链式查询
多表查询手写 SQL手写 SQL通常手写QueryWrapper 支持较灵活
XML 支持支持支持支持
学习成本中等中等中等
适合场景少量 SQL、内部工具SQL 控制要求高常规后台 CRUD需要轻量增强和灵活查询

粗略理解:

JdbcTemplate 更接近原生 JDBC
MyBatis 更像 SQL 映射工具
MyBatis-Plus 更偏常规 CRUD 增强
MyBatis-Flex 更偏轻量增强 + 灵活 QueryWrapper

常见使用建议

Mapper 只放数据库操作

Mapper 层适合放:

  • 基础 CRUD
  • 自定义查询方法
  • 和数据库强相关的操作

业务判断、事务流程、多个 Mapper 的组合,更适合放在 Service 层。

QueryWrapper 适合中等复杂度查询

适合写成 QueryWrapper 的查询:

  • 单表条件查询
  • 简单多条件筛选
  • 简单 join
  • 分页列表
  • 动态条件查询

更适合写 XML 的查询:

  • 大量聚合统计
  • 多层嵌套子查询
  • 复杂报表
  • 数据库特有语法较多的 SQL

表定义类生成失败先查 APT

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

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

逻辑删除字段保持统一

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

比如统一使用:

deleted

或者:

is_delete

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

批量操作注意分批

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

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

常见做法是:

每 500 条或 1000 条拆成一批。

保留 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 的核心并不复杂。

最常用的组合就是:

Entity 使用 @Table 和 @Id
Mapper 继承 BaseMapper
查询条件使用 QueryWrapper
分页使用 paginate
复杂 SQL 继续使用 MyBatis XML

它适合这些场景:

  • 项目已经熟悉 MyBatis
  • 希望保留 SQL 控制权
  • 单表 CRUD 比较多
  • 动态条件查询比较多
  • 不想为简单查询写大量 XML
  • 需要逻辑删除、分页、乐观锁等常用能力

落地时重点关注三件事:

  • 依赖版本和 Spring Boot 版本要匹配
  • APT 要正常生成表定义类
  • QueryWrapper 和 XML 按复杂度分工

掌握这些内容后,MyBatis-Flex 已经可以覆盖大多数后台系统的数据访问层开发。