MyBatis-Plus 单表开发与增强

本文介绍 MyBatis-Plus 在 Java 后端项目中的单表开发与增强能力,覆盖快速入门、BaseMapper、通用 CRUD、Wrapper 条件构造器、IService、分页插件、自动填充、逻辑删除、代码生成器和多表查询边界。文章帮助读者理解 MyBatis-Plus 如何提升常规数据库开发效率。

第一章 为什么需要 MyBatis-Plus?

在了解 MyBatis-Plus 之前,我们先回想一下使用原生 MyBatis 开发时的痛点:

  • 大量重复的 SQL:每张表的基础 CRUD(增删改查)都需要手写一遍 XML 或者注解,比如 insertselectByIdupdate,代码极其冗余。
  • 实体类属性映射繁琐:如果数据库字段和 Java 实体类属性命名不一致,还需要写大量的 @Result 或者 <resultMap> 映射。
  • 动态 SQL 编写易错:使用 <if><where> 拼接条件容易出现语法错误,而且不够面向对象。

MyBatis-Plus(简称 MP) 就是为了解决这些痛点而生的。它的宗旨是:只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑。 使用它,你甚至不需要写一行 SQL 就能完成单表的大部分操作。


第二章 快速入门

2.1 引入依赖

在 Spring Boot 项目中,只需要引入 mybatis-plus-boot-starter。注意:不要同时引入原生的 mybatis-spring-boot-starter,MP 已经包含了它。

XML
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.3</version> <!-- 请使用最新稳定版 -->
</dependency>

2.2 核心体验:继承 BaseMapper

我们定义一个 Mapper 接口,直接继承 MP 提供的 BaseMapper<T> 接口,其中 T 是实体类。

Java
@Mapper
public interface UserMapper extends BaseMapper<User> {
    // 已经自动拥有了所有的基础 CRUD 方法!
}

测试一下:

Java
@Autowired
private UserMapper userMapper;

@Test
public void testSelect() {
    // 根据 ID 查询
    User user = userMapper.selectById(1L);
    System.out.println(user);
    
    // 查询所有数据
    List<User> users = userMapper.selectList(null);
}

2.3 核心注解

如果实体类与数据库表结构没有完美对应,MP 提供了几个核心注解来解决映射问题:

  • @TableName:指定实体类对应的数据库表名。如果类名叫 User,表名叫 t_user,就需要加 @TableName("t_user")
  • @TableId:指定实体类的哪个属性是主键。可以指定生成策略:
  • IdType.AUTO:数据库自增(数据库必须设置了自增)。
  • IdType.ASSIGN_ID:雪花算法生成分布式唯一 ID(默认策略,适合大厂和分库分表)。
  • @TableField:解决普通字段的映射问题。
  • 属性名和字段名不一致:@TableField("db_col_name")
  • 实体类中有,但数据库没有的字段(排除非表字段):@TableField(exist = false)
Java
@TableName("t_user")
public class User {
    @TableId(type = IdType.AUTO)
    private Long id;
    
    @TableField("user_name")
    private String name;
    
    private Integer age;
    
    @TableField(exist = false)
    private String token; // 数据库中没有该字段
}

第三章 通用 CRUD 操作

继承 BaseMapper 后,你不仅拥有了 selectById,还有一系列强大的方法:

  • 新增int insert(T entity);
  • 删除
  • 根据 ID 删除:int deleteById(Serializable id);
  • 根据 ID 批量删除:int deleteBatchIds(Collection<?> idList);
  • 根据条件删除:int delete(Wrapper<T> queryWrapper);
  • 修改
  • 根据 ID 修改:int updateById(T entity); (只会更新不为 null 的字段)
  • 查询
  • 根据 ID 批量查询:List<T> selectBatchIds(Collection<?> idList);
  • 根据条件查询一条:T selectOne(Wrapper<T> queryWrapper);
  • 根据条件查询列表:List<T> selectList(Wrapper<T> queryWrapper);

第四章 核心武器:条件构造器 (Wrapper)

如何不写 SQL 也能实现类似 WHERE name = '张三' AND age > 18 的查询?MP 提供了条件构造器 Wrapper

4.1 QueryWrapper

传统的 Wrapper,通过传入数据库字段名(字符串)来构建条件。

Java
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("user_name", "张三")    // name = '张三'
       .gt("age", 18)             // age > 18
       .like("email", "test");    // email LIKE '%test%'

List<User> list = userMapper.selectList(wrapper);

缺点:硬编码字段名 "user_name",一旦数据库改名或者拼写错误,编译期不会报错,运行时才会抛异常。

4.2 LambdaQueryWrapper(强烈推荐)

为了解决硬编码问题,MP 引入了 Lambda 条件构造器,通过方法引用来指定字段,类型安全,防误写。

Java
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
// User::getName 直接获取了属性,MP 底层会自动转换为列名
wrapper.eq(User::getName, "张三")
       .gt(User::getAge, 18)
       .like(User::getEmail, "test")
       .orderByDesc(User::getCreateTime); // ORDER BY create_time DESC

List<User> list = userMapper.selectList(wrapper);

4.3 常见条件方法速查

  • eq:等于 =
  • ne:不等于 <>
  • gt:大于 >
  • ge:大于等于 >=
  • lt:小于 <
  • le:小于等于 <=
  • between:BETWEEN 值1 AND 值2
  • like:LIKE '%值%'
  • likeLeft:LIKE '%值'
  • likeRight:LIKE '值%'
  • isNull / isNotNull:字段 IS NULL / IS NOT NULL
  • in / notIn:IN (值1, 值2...)

第五章 IService 接口增强

除了 Mapper 层的 BaseMapper,MP 还提供了 Service 层的通用接口 IService 和它的实现类 ServiceImpl

5.1 为什么要用 IService?

BaseMapper 只能单条插入,如果我有 1 万条数据要入库怎么办?循环调 insert 性能极差。IService 提供了 批量插入 等更高级的功能。

5.2 如何使用?

1. 定义 Service 接口,继承 IService<T>

Java
public interface UserService extends IService<User> {
    // 可以在这里写自定义业务方法
}

2. 编写实现类,继承 ServiceImpl<M, T> 并实现自定义接口

Java
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
}

5.3 IService 带来的超能力

Java
@Autowired
private UserService userService;

// 1. 批量保存 (底层会自动分批处理,性能极高)
List<User> users = buildUserList();
userService.saveBatch(users);

// 2. 批量保存或更新 (有 ID 且存在则更新,否则新增)
userService.saveOrUpdateBatch(users);

// 3. 链式查询 (更加优雅)
List<User> result = userService.lambdaQuery()
        .like(User::getName, "李")
        .ge(User::getAge, 20)
        .list();

第六章 进阶高级特性

6.1 分页插件

分页是极高频的需求。MP 提供了拦截器插件,配置后即可使用 Page 对象进行物理分页(底层自动拼接 LIMIT)。

配置拦截器

Java
@Configuration
public class MybatisPlusConfig {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // 添加分页插件,指定数据库类型为 MySQL
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }
}

使用分页

Java
// 第 1 页,每页 10 条
Page<User> page = new Page<>(1, 10);
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.ge(User::getAge, 18);

// 执行分页查询,结果会塞回 page 对象中
Page<User> resultPage = userMapper.selectPage(page, wrapper);

System.out.println("总条数: " + resultPage.getTotal());
System.out.println("总页数: " + resultPage.getPages());
System.out.println("当前页数据: " + resultPage.getRecords());

6.2 自动填充功能

很多表都有 create_timeupdate_time,每次 insertupdate 手动设值很麻烦。

  1. 实体类加注解声明填充策略:
Java
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;

@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
  1. 编写 MetaObjectHandler 处理器:
Java
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
    @Override
    public void insertFill(MetaObject metaObject) {
        this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
        this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
    }

    @Override
    public void updateFill(MetaObject metaObject) {
        this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
    }
}

6.3 逻辑删除

真实项目中,数据通常不真删(物理删除),而是把一个状态字段标记为已删除(逻辑删除)。 MP 原生支持逻辑删除:

在实体类对应的字段加上 @TableLogic

Java
@TableLogic
private Integer deleted; // 默认 0 未删除,1 已删除

加上之后:

  • 调用 deleteById 时,底层自动变成:UPDATE user SET deleted = 1 WHERE id = ?
  • 调用 selectList 等查询方法时,底层自动追加:WHERE deleted = 0

完全对业务层透明,极其省心!


第七章 代码生成器 (Generator)

面对几十张表,手动建实体类、Mapper、Service 也是体力活。MyBatis-Plus 提供了代码生成器(MyBatis-Plus Generator)或者第三方 IDEA 插件(如 MyBatisX),连上数据库,一键生成所有的基础代码。

7.1 使用 MyBatisX 插件(推荐)

在现代开发中,通常推荐直接使用 IDEA 的 MyBatisX 插件,图形化界面,鼠标点两下即可生成整套模板代码。只需在 IDEA 插件市场搜索 MyBatisX 安装,然后在 Database 工具窗口中右键表名,选择 MybatisX-Generator 即可。

7.2 使用代码生成器依赖

如果你希望通过代码来灵活控制生成逻辑,可以引入官方的代码生成器依赖:

XML
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-generator</artifactId>
    <version>3.5.3</version>
</dependency>
<!-- 模板引擎,推荐 freemarker -->
<dependency>
    <groupId>org.freemarker</groupId>
    <artifactId>freemarker</artifactId>
</dependency>

快速生成代码示例

Java
public class CodeGenerator {
    public static void main(String[] args) {
        FastAutoGenerator.create("jdbc:mysql://localhost:3306/mydb", "root", "123456")
            .globalConfig(builder -> {
                builder.author("YourName") // 设置作者
                    .outputDir("D://output"); // 输出目录
            })
            .packageConfig(builder -> {
                builder.parent("com.example.demo") // 设置父包名
                    .moduleName("system"); // 设置父包模块名
            })
            .strategyConfig(builder -> {
                builder.addInclude("t_user", "t_order") // 设置需要生成的表名
                    .addTablePrefix("t_", "c_"); // 设置过滤表前缀
            })
            .templateEngine(new FreemarkerTemplateEngine()) // 使用Freemarker引擎模板,默认的是Velocity引擎模板
            .execute();
    }
}

第八章 多表查询解决方案

MyBatis-Plus 的核心设计理念是“专注于单表操作”。官方提供的 Wrapper 条件构造器并不直接支持多表联查(如 JOIN)。面对多表查询,通常有以下几种解决方案:

8.1 自定义 XML 编写 SQL(最正宗、最推荐)

对于复杂的报表、多表关联,最稳妥和好维护的方式还是回归 MyBatis 的本源,写 XML。

Mapper 接口

Java
public interface UserMapper extends BaseMapper<User> {
    List<UserVO> getUserAndOrder(@Param("userId") Long userId);
}

XML 映射文件

XML
<select id="getUserAndOrder" resultType="com.example.demo.vo.UserVO">
    SELECT u.id, u.user_name, o.order_no, o.amount
    FROM t_user u
    LEFT JOIN t_order o ON u.id = o.user_id
    WHERE u.id = #{userId}
</select>

8.2 使用注解方式

如果 SQL 相对简单,可以直接写在 Mapper 接口的方法注解上,省去 XML 文件:

Java
@Select("SELECT u.id, u.user_name, o.order_no, o.amount FROM t_user u LEFT JOIN t_order o ON u.id = o.user_id WHERE u.id = #{userId}")
List<UserVO> getUserAndOrder(@Param("userId") Long userId);

8.3 使用 MyBatis-Plus-Join (MPJ) 插件

如果你非常喜欢 MP 的 Wrapper 风格,并且不想写 SQL,可以引入社区维护的扩展包 MyBatis-Plus-Join

引入依赖

XML
<dependency>
    <groupId>com.github.yulichang</groupId>
    <artifactId>mybatis-plus-join</artifactId>
    <version>1.4.5</version> <!-- 请使用最新版 -->
</dependency>

让 Mapper 继承 MPJBaseMapper

Java
public interface UserMapper extends MPJBaseMapper<User> {
}

使用连表 Wrapper

Java
MPJLambdaWrapper<User> wrapper = new MPJLambdaWrapper<User>()
    .selectAll(User.class)
    .select(Order::getOrderNo, Order::getAmount)
    .leftJoin(Order.class, Order::getUserId, User::getId)
    .eq(User::getId, 1L);

List<UserVO> list = userMapper.selectJoinList(UserVO.class, wrapper);

这种方式适合不太复杂的 JOIN 查询,能够保持代码的全 Java 化和类型安全。


第九章 总结

MyBatis-Plus 将我们从无聊的单表 CRUD 和冗长的 XML 映射中解放了出来:

  • BaseMapper 搞定单表增删改查。
  • LambdaQueryWrapper 优雅、安全地拼接查询条件。
  • IService 轻松实现批量操作。
  • 借助 分页插件自动填充逻辑删除 解决高频通用需求。

建议: 虽然 MP 强大,但它的定位是辅助。对于多表联查、极其复杂的报表 SQL,不要强行用 Wrapper 拼接,那会导致代码可读性极差。复杂的 SQL,老老实实写在 Mapper 的 XML 文件里,这才是最正宗、最容易维护的做法!

配合阅读: MyBatis-Plus 的批量操作、多表写入同样依赖 Spring 事务。请参阅 《Java 数据库事务实战》,了解 @TransactionalBaseMapper / ServiceImpl 如何共用同一事务。