一、MyBatis-Plus 基础概述

MyBatis-Plus(简称 MP)是 MyBatis 的增强工具,由国人开发维护,在 MyBatis 的基础之上只做增强、不做改变。它的核心定位非常清晰:

只为简化开发而生,不对 MyBatis 原有能力做任何侵入式改动。

MP 的核心目标可以归纳为三点:

  1. 简化单表 CRUD:传统 MyBatis 中,即使是最简单的单表查询也需要手写 XML 映射文件或注解 SQL,而 MP 内置的 BaseMapper 直接提供了数十种常用方法,绝大多数单表操作可以做到零 SQL 编写。
  2. 提升开发效率:通过条件构造器(Wrapper)体系,用纯 Java 代码链式拼接 WHERE 条件,避免在 XML 中反复编写重复的条件判断逻辑。
  3. 完全兼容 MyBatis:MP 本质上是对 MyBatis 的一层封装,你在原生 MyBatis 中的所有用法——自定义 SQL、XML 映射、动态 SQL、ResultMap 等——全部保留,随时可以混合使用。

一句话概括:MP 让你用极少的代码完成单表 CRUD,同时完全保留了 MyBatis 的全部能力。


二、MyBatis-Plus 快速使用完整步骤

2.1 基础使用五步流程

下面通过一个完整的示例项目,演示从零搭建 MyBatis-Plus 并完成数据库操作的全过程。

第一步:引入 Maven 依赖

在 Spring Boot 项目的 pom.xml 中添加 MP Starter 依赖:

<!-- MyBatis-Plus Spring Boot Starter(适配 Spring Boot 3.x/4.x) -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-spring-boot4-starter</artifactId>
    <version>3.5.17</version>
</dependency>

<!-- MySQL 驱动 -->
<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <scope>runtime</scope>
</dependency>

选型说明:mybatis-plus-spring-boot4-starter 适配 Spring Boot 3.x 和 4.x 版本。如果你使用的是 Spring Boot 2.x,需要换成 mybatis-plus-boot-starter。版本号建议使用最新稳定版,截止本文编写时为 3.5.17。

第二步:配置数据库连接

在 application.yml 中完成 MySQL 连接配置:

spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/mybatis_test?characterEncoding=utf8&useSSL=false
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver

# MyBatis-Plus 扩展配置
mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl  # SQL 日志输出,方便调试
    map-underscore-to-camel-case: true                       # 开启驼峰命名自动映射

配置要点:

  • log-impl 开启后,每次执行的 SQL 语句和参数都会打印到控制台,强烈建议开发阶段开启,上线时可关闭。
  • map-underscore-to-camel-case 设为 true 后,数据库字段 user_name 会自动映射到实体类属性 userName,无需额外配置。
第三步:配置 Mapper 扫描路径

在启动类上使用 @MapperScan 注解,指定 Mapper 接口所在的包路径:

package com.zmt.mybatisplus;

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

@MapperScan("com.zmt.mybatisplus.mapper")  // 扫描 Mapper 接口所在包
@SpringBootApplication
public class MybatisPlusApplication {
    public static void main(String[] args) {
        SpringApplication.run(MybatisPlusApplication.class, args);
    }
}

两种方式标识 Mapper:

  • 方式一(推荐):在启动类上加 @MapperScan("包路径"),一次配置全局生效,无需在每个 Mapper 接口上逐个加注解。
  • 方式二:在每个 Mapper 接口上单独添加 @Mapper 注解。适合 Mapper 数量较少的项目,或需要在不同包中分散管理 Mapper 的场景。
第四步:编写实体类和 Mapper 接口

首先创建实体类,对应数据库中的 user_info 表:

package com.zmt.mybatisplus.model;

import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.util.Date;

@Data
@TableName("user_info")            // 显式指定映射的数据表名
public class Userinfo {
    @TableId(value = "id", type = IdType.AUTO)  // 主键字段,自增策略
    private Integer id;

    @TableField("user_name")       // 属性名与字段名不一致时,手动指定映射
    private String userName;

    private String password;

    @TableField(exist = false)     // 标记该属性在数据库表中不存在
    private Integer age;

    @TableField(exist = false)
    private Integer gender;

    @TableField(exist = false)
    private String phone;

    private Integer deleteFlag;
    private Date createTime;
    private Date updateTime;
}

然后编写 Mapper 接口,只需继承 BaseMapper<实体类> 即可获得全部内置方法:

package com.zmt.mybatisplus.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.zmt.mybatisplus.model.Userinfo;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface UserInfoMapper extends BaseMapper<Userinfo> {
    // 无需写任何方法,BaseMapper 已内置所有常用单表 CRUD 方法
}

BaseMapper 内置了哪些方法? 包括但不限于:insert、deleteById、deleteByMap、updateById、selectById、selectBatchIds、selectList、selectPage 等。覆盖了单表增删改查的绝大多数场景。

第五步:编写测试类验证
package com.zmt.mybatisplus.mapper;

import com.zmt.mybatisplus.model.Userinfo;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.List;

@SpringBootTest
class UserInfoMapperTest {

    @Autowired
    private UserInfoMapper userInfoMapper;   // 注入 Mapper 对象

    @Test
    public void testSelect() {
        System.out.println("----- selectAll method test ------");
        // selectList(null) 自动生成 SELECT * FROM user_info
        List<Userinfo> userList = userInfoMapper.selectList(null);
        userList.forEach(System.out::println);
    }

    @Test
    void testInsert() {
        Userinfo userInfo = new Userinfo();
        userInfo.setUserName("bit2");
        userInfo.setPassword("bit2");
        userInfo.setGender(1);
        userInfo.setAge(18);
        int insert = userInfoMapper.insert(userInfo);
        System.out.println("影响行数:" + insert);
    }

    @Test
    void testUpdate() {
        Userinfo userInfo = new Userinfo();
        userInfo.setId(14);
        userInfo.setUserName("bit33333");
        userInfo.setPassword("bit33333");
        userInfoMapper.updateById(userInfo);  // 根据主键更新
    }

    @Test
    void testDelete() {
        userInfoMapper.deleteById(14);         // 根据主键删除
    }
}

代码思路解析:

  • selectList(null):参数传 null 表示无条件,MP 会自动拼接 SELECT * FROM user_info 查询全表数据。
  • insert(userInfo):MP 会根据实体对象中不为 null 的属性自动生成 INSERT 语句。
  • updateById(userInfo):根据主键 id 进行更新,只更新对象中非 null 的字段。
  • deleteById(14):根据主键值删除对应记录。

至此,一个完整的 MP 项目就搭建完成了。你会发现:整个过程中没有写一行 SQL 语句,就已经完成了单表的增删改查操作。

2.2 MP 自动推断规则与实体类注解映射

MP 的核心优势之一是约定优于配置——它会根据实体类自动推断对应的数据库表信息,同时支持通过注解进行精细化映射。

表名推断规则

MP 默认将实体类名按驼峰命名转换成下划线命名来匹配数据表:

实体类名推断表名转换规则
UserInfouser_info大写字母前插入下划线,全小写
BookInfobook_info同上
OrderDetailorder_detail同上

如果表名和推断结果不一致,使用 @TableName("真实表名") 显式指定:

@TableName("user_info")   // 明确告诉 MP:这个实体类对应 user_info 表
public class Userinfo { ... }

为什么实体类叫 Userinfo 而不是 UserInfo? 这是有意为之的例子:如果类名是 Userinfo(小写的 i),MP 按驼峰规则推断出来的表名是 userinfo(全小写无下划线),而实际表名是 user_info(带下划线),两者不匹配。此时就必须用 @TableName 显式绑定,否则会报 “表不存在” 的错误。这里也提醒大家:类名尽量遵循规范的驼峰命名,可以减少不必要的注解配置。

主键推断规则

MP 默认将实体类中名为 id 的属性识别为主键。如果你的主键字段不叫 id,或需要指定主键生成策略,使用 @TableId 注解:

@TableId(value = "id", type = IdType.AUTO)
private Integer id;

常用主键策略 IdType 说明:

策略说明适用场景
AUTO数据库自增 IDMySQL 自增主键,最常用
ASSIGN_IDMP 自动分配 Snowflake 雪花算法 ID(Long 型)分布式系统,避免 ID 冲突
INPUT手动输入 ID需要自定义 ID 值的场景
NONE无策略,跟随全局设置默认行为
字段推断规则

MP 默认将实体类属性名按驼峰转下划线匹配数据库字段。当属性名和字段名不一致时,使用 @TableField 注解:

@TableField("user_name")   // 属性名为 userName,数据库字段为 user_name → 手动绑定
private String userName;

@TableField(exist = false) // 这个属性在数据库表中不存在,MP 不对其做字段映射
private Integer age;

@TableField 的常见用法:

场景配置说明
字段名不匹配@TableField("user_name")显式指定映射的数据库字段名
属性非表字段@TableField(exist = false)该属性不参与 SQL 拼接,常用于业务辅助字段
忽略空值更新@TableField(updateStrategy = FieldStrategy.IGNORED)控制更新策略

三、Wrapper 条件构造器体系

当查询不再是"查全部",而是需要加 WHERE 条件时,就需要用到 MP 的核心武器——Wrapper 条件构造器。它让你用纯 Java 代码链式拼接查询/更新条件,告别在 XML 中手动编写 <where> 和 <if> 标签的繁琐写法。

3.1 Wrapper 继承结构

MP 的条件构造器体系层次分明,所有实现类都继承自抽象父类 AbstractWrapper:

AbstractWrapper(抽象父类,定义通用条件方法)
├── QueryWrapper          → 用于构建 SELECT 的 WHERE 条件
├── LambdaQueryWrapper    → 基于 Lambda 表达式的查询条件构造器
├── UpdateWrapper         → 用于构建 UPDATE 的 SET + WHERE 条件
└── LambdaUpdateWrapper   → 基于 Lambda 表达式的更新条件构造器

四大实现类的职责分工:

构造器用途特点
QueryWrapper构建查询条件字段名以字符串形式书写
LambdaQueryWrapper构建查询条件通过实体类方法引用书写字段,编译期校验
UpdateWrapper构建更新条件支持 set() 设置字段值 + WHERE 条件
LambdaUpdateWrapper构建更新条件Lambda 写法 + Update 能力

3.2 两大核心 Wrapper 详解

QueryWrapper:查询条件构造器

QueryWrapper 专门用于拼接 SELECT 语句的 WHERE 条件,支持等于、不等于、大于、小于、模糊匹配等各类判断条件,并且支持链式调用,可自由组合 AND / OR 逻辑。

基础条件查询示例:

@Test
void testQueryWrapper() {
    // 1. 创建 QueryWrapper 对象
    QueryWrapper<Userinfo> queryWrapper = new QueryWrapper<>();

    // 2. 链式拼接查询条件:选择字段 + WHERE 条件
    queryWrapper
        .select("id", "user_name", "password", "delete_flag")  // 指定返回字段
        .eq("delete_flag", 18)                                   // WHERE delete_flag = 18
        .like("user_name", "min");                               // AND user_name LIKE '%min%'

    // 3. 调用 selectList 执行查询
    List<Userinfo> userinfos = userInfoMapper.selectList(queryWrapper);
    userinfos.forEach(System.out::println);
}

生成的 SQL(已开启日志时可见):

SELECT id, user_name, password, delete_flag
FROM user_info
WHERE delete_flag = 18 AND user_name LIKE '%min%'

代码思路解析:

  1. select("id", "user_name", ...):指定查询返回的字段,不调用则默认 SELECT *。
  2. eq("delete_flag", 18):拼接 WHERE delete_flag = 18,eq = equals。
  3. like("user_name", "min"):拼接 AND user_name LIKE '%min%',条件间默认用 AND 连接。
  4. 所有方法都返回 this,支持链式调用,代码简洁流畅。

QueryWrapper 常用条件方法速查:

方法SQL 等价说明
eq("字段", 值)字段 = 值等于
ne("字段", 值)字段 != 值不等于
gt("字段", 值)字段 > 值大于
lt("字段", 值)字段 < 值小于
ge("字段", 值)字段 >= 值大于等于
le("字段", 值)字段 <= 值小于等于
like("字段", 值)字段 LIKE '%值%'模糊匹配
in("字段", 集合)字段 IN (v1, v2, ...)范围查询
between("字段", v1, v2)字段 BETWEEN v1 AND v2区间查询
orderByAsc("字段")ORDER BY 字段 ASC升序排序
orderByDesc("字段")ORDER BY 字段 DESC降序排序

注意:QueryWrapper 中的字段名是数据库字段名(如 delete_flag),而不是实体类的属性名(如 deleteFlag)。这是新手最容易踩的坑。

UpdateWrapper:更新条件构造器

UpdateWrapper 用于构造更新条件,和 QueryWrapper 用法类似,同样支持链式调用与多条件逻辑组合。它的核心优势是:无需先创建实体对象,直接通过 set() 方法指定要更新的字段和值。

@Test
void testUpdateWrapper() {
    // 创建 UpdateWrapper,直接设置更新字段和条件
    UpdateWrapper<Userinfo> updateWrapper = new UpdateWrapper<>();
    updateWrapper
        .lt("delete_flag", 20)           // WHERE delete_flag < 20
        .set("delete_flag", 1);          // SET delete_flag = 1

    // 执行更新——无需传入实体对象
    userInfoMapper.update(null, updateWrapper);
}

生成的 SQL:

UPDATE user_info SET delete_flag = 1 WHERE delete_flag < 20

与更新实体方式对比:

方式代码量灵活性适用场景
updateById(实体)少低,只能按主键更新固定字段简单的单条记录全量更新
UpdateWrapper + set()中高,支持任意条件 + 任意字段组合按条件批量更新指定字段

高级场景——SQL 片段拼接:

@Test
void testUpdateWrapper2() {
    UpdateWrapper<Userinfo> updateWrapper = new UpdateWrapper<>();
    updateWrapper
        .setSql("delete_flag = delete_flag + 10")  // 直接拼接 SQL 片段
        .in("id", List.of(1, 13, 15));              // WHERE id IN (1, 13, 15)

    userInfoMapper.update(updateWrapper);
}

生成的 SQL 为 UPDATE user_info SET delete_flag = delete_flag + 10 WHERE id IN (1, 13, 15)。setSql() 方法让你可以在更新中编写原生 SQL 表达式,满足字段自增、运算等特殊需求。

3.3 Lambda 系列 Wrapper 的优势

QueryWrapper 和 UpdateWrapper 虽然功能强大,但有一个明显的痛点:字段名以硬编码字符串形式书写,容易拼写错误,且项目重构属性名时,这些字符串无法被 IDE 自动关联修改。

LambdaQueryWrapper 和 LambdaUpdateWrapper 正是为解决这个问题而生——它们通过实体类方法引用(类名::get属性名)来书写条件,完全消除硬编码字符串。

对比示例:

// 传统写法:字段名为字符串,拼写错误在编译期无法发现
QueryWrapper<Userinfo> queryWrapper = new QueryWrapper<>();
queryWrapper
    .select("id", "user_name", "password")   // 字符串,拼错不会报错
    .eq("delete_flag", 15);                   // 同上

// Lambda 写法:通过方法引用,编译期校验,属性名改动能自动同步
queryWrapper.lambda()
    .select(Userinfo::getId, Userinfo::getUserName, Userinfo::getPassword)
    .eq(Userinfo::getDeleteFlag, 15);

Lambda 写法的三大优势:

  1. 编译期校验:方法引用如果不合法,编译阶段就会报错,杜绝字段名拼写问题。
  2. 重构友好:实体类属性名修改后,IDE 会自动同步所有方法引用,不会遗漏。
  3. 代码可读性更高:Userinfo::getDeleteFlag 比字符串 "delete_flag" 语义更清晰。

UpdateWrapper 同样支持 Lambda 写法:

@Test
void testLambdaUpdateWrapper() {
    UpdateWrapper<Userinfo> updateWrapper = new UpdateWrapper<>();
    updateWrapper.lambda()
        .set(Userinfo::getDeleteFlag, 0)          // SET delete_flag = 0
        .set(Userinfo::getDeleteFlag, 5)          // SET delete_flag = 5(覆盖上一步)
        .in(Userinfo::getId, List.of(1, 13, 15)); // WHERE id IN (1, 13, 15)

    userInfoMapper.update(updateWrapper);
}

使用建议:日常开发优先选择 Lambda 系列 Wrapper。只有当需要处理动态字段名(如按用户选择的列排序)的极少数场景时,再降级使用字符串方式的 QueryWrapper / UpdateWrapper。


四、补充重要总结知识点

4.1 MP 的最大优势

回顾整个使用流程,MP 的核心价值可以浓缩为:

对简单 CRUD 操作,几乎零 SQL 代码;对复杂查询,完全保留 MyBatis 原生能力,想怎么写就怎么写。

对比维度原生 MyBatisMyBatis-Plus
简单查询需手写 SQL / XMLselectList() 一行搞定
条件查询XML 中写 <if> + <where>Wrapper 链式调用
分页查询手写 LIMIT + COUNT 查询Page + selectPage() 内置支持
主键策略手动处理@TableId 一行配置
复杂联表查询XML 手写 SQL完全兼容,仍可手写 SQL + XML

一句话总结:MP 让开发者把精力聚焦在核心业务逻辑和复杂 SQL 上,而不是浪费在重复的 CRUD 模板代码中。

4.2 自定义 SQL 兼容规则

MP 并不强制你只使用内置方法——它完全兼容 MyBatis 的自定义 SQL 写法。当内置方法无法满足需求时,你可以自由编写自定义 SQL,并且可以将 Wrapper 构造好的条件传入自定义 SQL 中复用。

核心规则(重要):

MP 版本 ≥ 3.0.7 时,自定义 Mapper 方法中如果接收 Wrapper 对象作为参数,该参数的命名必须为 ew(对应 MP 内部常量 Constants.WRAPPER)。在自定义 SQL 中通过 ${ew.customSqlSegment} 即可引用 Wrapper 拼接好的 WHERE 条件片段。

实战示例——Mapper 接口中定义自定义方法:

@Mapper
public interface UserInfoMapper extends BaseMapper<Userinfo> {

    // 纯自定义 SQL(不使用 Wrapper 条件)
    @Select("select id, user_name, password from user_info where user_name = #{user_name}")
    List<Userinfo> selectListByCustom(String user_name);

    // 自定义 SQL + Wrapper 动态条件(注解方式)
    @Select("select id, user_name, password from user_info ${ew.customSqlSegment}")
    List<Userinfo> selectListByCustom2(@Param(Constants.WRAPPER) Wrapper<Userinfo> wrapper);

    // 自定义 SQL + Wrapper 动态条件(XML 方式)
    List<Userinfo> selectListByCustom3(@Param(Constants.WRAPPER) Wrapper<Userinfo> wrapper);

    // 自定义更新 + Wrapper 动态条件
    @Update("update user_info set delete_flag = delete_flag + #{age} ${ew.customSqlSegment}")
    Integer updateByCustom(@Param("age") Integer age,
                           @Param(Constants.WRAPPER) Wrapper<Userinfo> wrapper);
}

对应的 XML 映射文件(mapper/UserInfoMapper.xml):

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zmt.mybatisplus.mapper.UserInfoMapper">

    <select id="selectListByCustom3"
            resultType="com.zmt.mybatisplus.model.Userinfo">
        select id, userName, password, age from user_info ${ew.customSqlSegment}
    </select>

</mapper>

测试调用——Wrapper 条件与自定义 SQL 联动:

@Test
void testSelectListByCustom2() {
    // 用 QueryWrapper 构造条件
    QueryWrapper<Userinfo> queryWrapper = new QueryWrapper<>();
    queryWrapper
        .eq("user_name", "admin")
        .eq("delete_flag", 15);

    // 传入自定义方法,${ew.customSqlSegment} 自动替换为 WHERE 子句
    List<Userinfo> userinfos = userInfoMapper.selectListByCustom2(queryWrapper);
    userinfos.forEach(System.out::println);
}

实际执行的 SQL:

SELECT id, user_name, password FROM user_info WHERE user_name = 'admin' AND delete_flag = 15

代码思路解析:

  1. @Param(Constants.WRAPPER) 是固定的写法,Constants.WRAPPER 的值就是 "ew"。也可以直接写 @Param("ew"),但使用常量更规范。
  2. ${ew.customSqlSegment} 中的 $ 不是 #,这意味着它直接拼接 SQL 字符串而非使用预编译占位符。实际 WHERE 内部的参数值仍然是通过 # 预编译安全的——这里 ${} 拼接的是整个 WHERE 子句框架。
  3. 这种设计让你既享受 Wrapper 的动态条件构造能力,又保留了自定义 SQL 的灵活性,是 MP 中最实用的高级特性之一。

全文总结

本文从 MP 的核心定位出发,通过一个完整的示例项目,系统讲解了 MyBatis-Plus 的四大核心模块:

  1. 基础概述:MP 是 MyBatis 的增强工具,只做增强不做改变,核心目标是简化单表 CRUD 开发,同时完全兼容 MyBatis 原生能力。
  2. 五步快速上手:引入依赖 → 配置数据源 → 配置 Mapper 扫描 → 继承 BaseMapper → 直接调用内置方法。全程零 SQL 编写即可完成单表增删改查。
  3. 注解映射机制:通过 @TableName、@TableId、@TableField 三大注解灵活控制实体类与数据库表的映射关系,兼顾约定优于配置和精细化控制。
  4. Wrapper 条件构造器:QueryWrapper / UpdateWrapper 提供了链式拼接条件的能力,Lambda 系列 Wrapper 进一步消除了字符串硬编码带来的维护隐患。
  5. 自定义 SQL 兼容:通过 ${ew.customSqlSegment} 将 Wrapper 条件注入自定义 SQL,实现动态条件 + 手写 SQL 的完美结合。

核心知识点复盘

知识点要点
MP 定位增强而非替代 MyBatis,完全兼容原生功能
依赖选型Spring Boot 3.x/4.x 用 mybatis-plus-spring-boot4-starter
Mapper 扫描@MapperScan 一次性配置优于逐接口加 @Mapper
BaseMapper继承即获得全套单表 CRUD 方法,无需编写任何实现
表名推断实体类名驼峰转下划线,不一致时用 @TableName
主键策略IdType.AUTO(自增)和 IdType.ASSIGN_ID(雪花算法)最常用
字段映射@TableField 解决属性名与字段名不匹配、非表字段标记
QueryWrapper链式拼接 SELECT 的 WHERE 条件,字段名用数据库字段名
UpdateWrapper无需实体对象,set() + 条件链式构造 UPDATE 语句
Lambda Wrapper通过方法引用消除字符串硬编码,编译期安全,重构友好
自定义 SQL + Wrapper参数用 @Param(Constants.WRAPPER),SQL 中用 ${ew.customSqlSegment}

常见问题 / 避坑指南

1. 表名或字段名不匹配导致 SQL 报错

现象:执行查询时报 Table 'xxx' doesn't exist 或 Unknown column 'xxx'。

原因:MP 按驼峰转下划线规则推断表名/字段名,与数据库实际命名不一致。

解决:使用 @TableName("实际表名") 和 @TableField("实际字段名") 显式指定映射关系。

2. QueryWrapper 中写错字段名

现象:SQL 执行正常但查询结果不符合预期,或直接报字段不存在。

原因:QueryWrapper 的 eq()、like() 等方法参数是数据库字段名(如 delete_flag),不是实体类的属性名(如 deleteFlag)。

解决:

  • 优先使用 LambdaQueryWrapper,通过方法引用书写,从源头杜绝此类错误。
  • 如果必须用 QueryWrapper,确保传入的是数据库字段名而非 Java 属性名。

3. 设置了 map-underscore-to-camel-case 但结果映射失败

现象:SQL 执行成功、控制台显示有数据返回,但 Java 对象的属性全是 null。

原因:可能是在 XML 自定义 SQL 中 SELECT 的列别名使用了驼峰命名,与 MP 的自动映射机制产生了冲突。

解决:统一命名风格——如果开启了驼峰自动映射,数据库查询返回的列名使用下划线格式(如 user_name),让 MP 自动转换为 userName。

4. 自定义 SQL 中 ${ew.customSqlSegment} 不生效

现象:调用自定义方法时,Wrapper 中的条件没有被拼接到 SQL 中。

排查步骤:

  • 确认 Mapper 方法参数中使用了 @Param(Constants.WRAPPER) 或 @Param("ew"),这是硬性要求。
  • 确认 SQL 中写的是 ${ew.customSqlSegment}($ 符号),而不是 #{ew.customSqlSegment}(# 符号)。# 是预编译占位符,会把整个 WHERE 片段当作字符串参数处理,导致拼接失败。
  • 确认 MP 版本 ≥ 3.0.7,ew 参数名是在这个版本之后才固定下来的。

5. @TableField(exist = false) 字段在自定义 SQL 中使用

现象:标记了 exist = false 的属性,在某些场景下能正常赋值,某些场景下不行。

说明:exist = false 告诉 MP “这个字段在数据库表中不存在”,因此 MP 在自动生成 INSERT / UPDATE SQL 时会忽略该字段。但如果你在自定义 SQL(XML 或 @Select)中手动 SELECT 了这个字段,并且查询结果中包含该列,MyBatis 仍然会将值映射到该属性上。这是 MyBatis 底层行为,不是 MP 的问题——关键在于你自定义 SQL 中 SELECT 了什么列。

6. 依赖版本与 Spring Boot 版本不匹配

现象:项目启动时报 ClassNotFoundException 或 NoSuchMethodError 等异常。

解决:

  • Spring Boot 2.x → 使用 mybatis-plus-boot-starter
  • Spring Boot 3.x / 4.x → 使用 mybatis-plus-spring-boot4-starter
  • 如果项目使用的是 MyBatis-Plus 3.5.x 以下的老版本,建议升级到最新稳定版,以获取更好的 Spring Boot 高版本兼容性。

本文所有代码均基于实际可运行的 Spring Boot + MyBatis-Plus 示例项目编写,读者可参照上述步骤搭建自己的项目进行练习。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐