Java(13):13 Mybatis-Plus -ORM框架 MyBatis 最好的搭档


1. [基础] 快速入门与环境配置

摘要: 本章的目标是用最快的速度搭建一个可以运行 Mybatis-Plus 的最小化 Spring Boot 3 项目。我们将聚焦于核心的依赖配置、数据源连接以及实体类和 Mapper 接口的基础定义,为后续所有章节的学习提供一个简洁、稳定的开发环境。


1.1. Mybatis-Plus 简介与核心优势

对于一位已经熟练掌握 Spring Boot 和原生 MyBatis 的开发者而言,MyBatis 的优点——完全掌控 SQL 的灵活性——毋庸置疑。但与此同时,其固有的开发痛点也同样突出。

“痛点回顾”: 大量的样板代码(Boilerplate Code)充斥在项目中。即便是一个最基础的单表 CRUD 操作,我们依然需要一步步地完成从 Mapper 接口定义到 XML 文件编写的全过程。这种重复性劳动在项目初期和快速迭代中,会显著拖慢开发效率。

为了更直观地展示原生 MyBatis 与 Mybatis-Plus (下文简称 MP) 在开发流程上的天壤之别,我们可以通过一个简单的“根据ID查询用户”功能进行对比:

  1. Mapper 层:在 UserMapper 接口中定义
    1
    User selectById(Long id);
  2. XML 层:在 UserMapper.xml 中手写
    1
    2
    3
    <select id="selectById" resultType="com.demo.User">
    SELECT * FROM user WHERE id = #{id}
    </select>
  3. Service 层:在 UserServiceImpl 中注入 UserMapper 并调用
    1
    User user = userMapper.selectById(id);
  1. Mapper 层:让 UserMapper 接口继承 BaseMapper<User>,无需额外方法。
    1
    public interface UserMapper extends BaseMapper<User> {}
  2. XML 层:无需任何 XML 或 SQL。
  3. Service 层:直接调用继承自 IService 的现成方法。
    1
    User user = userService.getById(id);

通过对比,MP 的核心价值主张显而易见:“只做增强,不做改变”。它完美继承了 MyBatis 的所有功能,并通过内置通用 MapperService,将我们从繁琐、重复的 CRUD 代码中彻底解放出来。这使得我们能更专注于复杂的业务逻辑,也正是我们称之为 MyBatis “最佳搭档” 的根本原因。


1.1.1. [面试题] MP、MyBatis 与 JPA 的技术选型对比

技术选型讨论
2025-08-21 22:30

在项目中,当面临持久层框架选型时,你是如何看待 Mybatis-Plus、MyBatis 和 JPA (如 Hibernate) 这三者的?它们的优缺点和适用场景分别是什么?

好的面试官。这三者是 Java 持久化领域的代表,我的理解如下:

JPA以Hibernate为代表,是一个全自动ORM框架。它的优点是自动化程度高、开发效率快,缺点是SQL黑盒、难以优化,因此最适用于业务简单的中后台系统。

MyBatis是一个半自动SQL映射框架。它的优点是对SQL有绝对控制权、便于性能优化,缺点是样板代码多、开发效率较低,因此非常适用于SQL逻辑复杂、性能要求高的互联网应用。

Mybatis-Plus是MyBatis的增强工具。它的优点是结合了JPA的便利和MyBatis的灵活,既能快速开发也能精细优化,缺点是学习曲线稍高,因此它适用于绝大多数需要兼顾开发效率和性能的现代Java项目。

总结得很好。


1.2. 项目环境搭建

为了让学习过程聚焦于 Mybatis-Plus 本身,我们将采用最简洁的单模块 Spring Boot 项目结构

1.2.1. 技术栈版本说明

本教程将基于 2025 年的主流稳定技术栈进行构建,具体版本如下:

技术栈版本说明
JDK21Long-Term Support (LTS) 长期支持版
Spring Boot3.4.x现代 Java 应用开发的事实标准
Mybatis-Plus3.5.7+适配 Spring Boot 3 的最新稳定版
MySQL Driver8.0.33官方推荐的 MySQL 8+ 驱动
Maven3.8+项目构建与依赖管理工具

image-20250822090636590


1.2.2. Maven 依赖配置 (pom.xml)

首先,创建一个标准的 Spring Boot Maven 项目,并在 pom.xml 中配置好我们的核心依赖。

文件路径: pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.4</version>
</parent>

<groupId>com.example</groupId>
<artifactId>mybatis-plus-tutorial</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>mybatis-plus-tutorial</name>
<description>Mybatis-Plus Tutorial</description>

<properties>
<java.version>21</java.version>
<mybatis-plus.version>3.5.7</mybatis-plus.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>

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

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>

<!-- 引入Hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.20</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>

1.2.3. 数据源与 MP 基础配置 (application.yml)

我们推荐使用 .yml 格式进行配置,因为它层级清晰,更具可读性。请在 src/main/resources/ 目录下创建 application.yml 文件。

文件路径: src/main/resources/application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 服务器端口配置
server:
port: 8080

# Spring Boot 核心配置
spring:
# 数据库数据源配置
datasource:
url: jdbc:mysql://127.0.0.1:3306/mybatis_plus_notes?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: root # 替换为您的数据库密码
driver-class-name: com.mysql.cj.jdbc.Driver

# [新特性] 开启虚拟线程,提升I/O密集型应用吞吐量 (需要 JDK 21+)
threads:
virtual:
enabled: true

# Mybatis-Plus 特定配置
mybatis-plus:
# 全局配置
global-config:
# 关闭启动时输出的 Mybatis-Plus Banner信息, 保持控制台清爽
banner: false
# MyBatis原生配置
configuration:
# 开启驼峰命名自动映射,如数据库的:user_name -> Java实体的:userName
map-underscore-to-camel-case: true
# 配置日志实现为标准输出,方便在开发阶段直接于控制台查看执行的SQL语句
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

请注意: 在 application.yml 中,spring.datasource.password 字段需要替换为您自己本地 MySQL 数据库的真实密码。


1.3. 核心文件创建

项目的基础框架和配置已经就绪。现在,我们需要创建与数据库交互的核心文件,包括数据表结构、实体类(Entity)、数据访问接口(Mapper)以及配置启动类。

1.3.1. 数据库表结构 (tb_user)

请在您的 MySQL 数据库中执行以下 SQL 脚本。这份脚本将创建我们项目所需的数据库和 tb_user 表。

设计说明:此表结构严格遵循了《阿里巴巴 Java 开发手册》的规约。我们预先定义了乐观锁 (version)、逻辑删除 (is_deleted) 以及审计字段 (gmt_create, gmt_modified),这体现了企业级表结构设计的专业性与前瞻性。我们将在后续章节中详细讲解这些字段的应用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
-- 创建数据库(如果不存在)
CREATE DATABASE IF NOT EXISTS `mybatis_plus_notes` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

-- 切换到目标数据库
USE `mybatis_plus_notes`;

-- 创建 user 表
DROP TABLE IF EXISTS `tb_user`;
CREATE TABLE `tb_user` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`name` varchar(30) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '姓名',
`age` int unsigned DEFAULT NULL COMMENT '年龄',
`email` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '邮箱',
`version` int unsigned NOT NULL DEFAULT '1' COMMENT '乐观锁版本号',
`is_deleted` tinyint unsigned NOT NULL DEFAULT '0' COMMENT '逻辑删除标志(0-未删除;1-已删除)',
`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户表';

-- 插入初始数据
INSERT INTO `tb_user` (`id`, `name`, `age`, `email`) VALUES
(1, 'Jone', 18, 'test1@baomidou.com'),
(2, 'Jack', 20, 'test2@baomidou.com'),
(3, 'Tom', 28, 'test3@baomidou.com'),
(4, 'Sandy', 21, 'test4@baomidou.com'),
(5, 'Billie', 24, 'test5@baomidou.com');

1.3.2. 实体类 (UserDO.java)

实体类是数据库表在 Java 世界中的映射。按照规约,与数据库表直接对应的对象我们称之为 DO (Data Object)。

文件路径: src/main/java/com/example/mpstudy/domain/UserDO.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
package com.example.mpstudy.domain;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.experimental.Accessors;

import java.time.LocalDateTime;

@Data
@Accessors(chain = true) // 支持链式调用
// `@TableName("tb_user")`: Mybatis-Plus 注解
// 用于将 `UserDO` 类与数据库中的 `tb_user` 表进行显式绑定。
@TableName("tb_user")
public class UserDO {
// 注意:我们在这里提前设置ID,因为数据库表中的id字段是主键,MP会使用默认的雪花算法生成一个全局唯一的ID,不方便我们的测试
@TableId(type = IdType.AUTO)
private Long id;

/**
* 姓名
*/
private String name;

/**
* 年龄
*/
private Integer age;

/**
* 邮箱
*/
private String email;

/**
* 乐观锁版本号
*/
private Integer version;

/**
* 逻辑删除标志(0-未删除;1-已删除)
*/
private Integer isDeleted;

/**
* 创建时间
*/
private LocalDateTime gmtCreate;

/**
* 修改时间
*/
private LocalDateTime gmtModified;
}

1.3.3. Mapper 接口 (UserMapper.java)

Mapper 接口是数据访问层(DAO)的核心,它充当了 Java 代码与数据库 SQL 之间的桥梁。

文件路径: src/main/java/com/example/mpstudy/mapper/UserMapper.java

1
2
3
4
5
6
7
8
9
package com.example.mpstudy.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.mpstudy.domain.UserDO;
// `extends BaseMapper<UserDO>`: 这是 Mybatis-Plus 的强大所在
// 通过继承 `BaseMapper` 并指定泛型为 `UserDO`,我们的 `UserMapper` 接口瞬间拥有了一整套强大且经过性能优化的 CRUD 方法
public interface UserMapper extends BaseMapper<UserDO> {
// 目前无需编写任何方法
}

1.3.4. 启动类配置 (@MapperScan)

最后一步,我们需要告诉 Spring Boot 在哪里可以找到我们刚刚创建的 Mapper 接口,以便为它们创建代理实现并纳入 IoC 容器管理。

文件路径: src/main/java/com/example/mpstudy/MpStudyApplication.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.example.mpstudy;

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

@SpringBootApplication
@MapperScan("com.example.mpstudy.mapper") // <-- 添加此行注解
public class MpStudyApplication {

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

}

@MapperScan("com.example.mpstudy.mapper"): 这个注解的作用是扫描指定的包(com.example.mpstudy.mapper),并将其中所有被 Mybatis 识别为 Mapper 的接口(通常是继承了 BaseMapper 的接口)自动注册为 Spring Bean。这样,我们就可以在 Service 层或其他地方通过 @Autowired 直接注入并使用它们了。

至此,我们的项目已完全准备就绪,所有基础配置和核心文件均已创建完毕。在下一章,我们将正式开始体验 Mybatis-Plus 强大而便捷的 CRUD 功能。


2. [核心] 通用 CRUD 与 Service 接口

摘要: 本章将讲解 Mybatis-Plus 效率革命的核心:通用 CRUD 功能。我们将学习如何通过继承 BaseMapperServiceImpl 接口,在不写一行 SQL 的情况下,实现单表的增、删、改、查及批量操作。


2.1. BaseMapper 内置方法详解

BaseMapper 是 Mybatis-Plus 实现通用 CRUD 的基石。我们在上一章让 UserMapper 接口继承了 BaseMapper<UserDO>,这使得 UserMapper 立刻拥有了十几个功能强大的、无需任何SQL编写的数据库操作方法。

方法名称描述需要传入的参数
insert(T entity)插入一条数据entity:需要插入的实体类对象
deleteById(Serializable id)根据 ID 删除数据id:要删除的记录的主键 ID
updateById(T entity)根据 ID 更新数据entity:包含更新字段的实体类对象
selectById(Serializable id)根据 ID 查询数据id:要查询的记录的主键 ID
selectList(Wrapper<T> query)根据条件查询数据query:查询条件,通常使用 QueryWrapper
delete(Wrapper<T> query)根据条件删除数据query:删除条件,通常使用 QueryWrapper
update(Wrapper<T> updateWrapper)根据条件更新数据updateWrapper:更新条件,通常使用 UpdateWrapper
selectCount(Wrapper<T> query)根据条件统计数据query:查询条件,通常使用 QueryWrapper
selectOne(Wrapper<T> query)根据条件查询一条数据query:查询条件,通常使用 QueryWrapper
  • T:表示一个实体类对象类型。例如,UserOrder 等。
  • Serializable:表示可以序列化的类型,通常是主键 ID。
  • Wrapper<T>:这是 MyBatis-Plus 提供的一个条件构造器类,常用的有 QueryWrapper(用于查询条件)和 UpdateWrapper(用于更新条件)。通过 Wrapper,你可以构建更加复杂的查询或更新条件,我们后续会详细讲解这个

为了验证这些方法的实际效果,我们将通过单元测试来进行演示。

首先,在 src/test/java/com/example/mpstudy/mapper/ 目录下,创建一个测试类 UserMapperTest

文件路径: src/test/java/com/example/mpstudy/mapper/UserMapperTest.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.example.mpstudy.mapper;


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

// @SpringBootTest: 标记这是一个Spring Boot的测试类,会加载整个Spring应用上下文
@SpringBootTest
class UserMapperTest {

// @Autowired: 从Spring容器中自动注入UserMapper的实例
@Autowired
private UserMapper userMapper;

// 后续的所有测试方法都将写在这个类中
}

2.1.1. 插入 (insert)

insert 方法用于向数据库中插入一条新的记录。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// UserMapperTest.java

@Test
void testInsert() {
System.out.println("----- 开始执行 insert 测试 -----");
UserDO user = new UserDO();
user.setName("Prorise");
user.setAge(30);
user.setEmail("prorise@example.com");

int result = userMapper.insert(user);
System.out.println("受影响的行数: " + result);
System.out.println("插入后的用户ID: " + user.getId());
System.out.println("----- insert 测试执行完毕 -----");
}
1
2
3
4
5
6
7
8
9
10
11
12
----- 开始执行 insert 测试 -----
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@68a426c3] was not registered for synchronization because synchronization is not active
2025-08-22T09:22:23.605+08:00 INFO 9020 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...
2025-08-22T09:22:23.805+08:00 INFO 9020 --- [ main] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Added connection com.mysql.cj.jdbc.ConnectionImpl@51141f64
2025-08-22T09:22:23.809+08:00 INFO 9020 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
JDBC Connection [HikariProxyConnection@1010480754 wrapping com.mysql.cj.jdbc.ConnectionImpl@51141f64] will not be managed by Spring
==> Preparing: INSERT INTO tb_user ( id, name, age, email ) VALUES ( ?, ?, ?, ? )
==> Parameters: 1958701250124349442(Long), Prorise(String), 18(Integer), prorise@163.com(String)
<== Updates: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@68a426c3]
受影响的行数: 1

2.1.2. 删除 (deleteById, deleteByMap, deleteBatchIds)

Mybatis-Plus 提供了多种删除数据的方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Test
void testDelete() {
System.out.println("----- 开始执行 delete 测试 -----");
int resultById = userMapper.deleteById(1L);
System.out.println("deleteById 受影响行数: " + resultById);

// 2. 根据多个ID批量删除
List<Long> idsToDelete = Arrays.asList(4L, 5L);
int resultBatch = userMapper.deleteBatchIds(idsToDelete);
System.out.println("deleteBatchIds 受影响行数: " + resultBatch);


// 3. 根据Map中的条件删除 (多个条件之间是 AND 关系)
// 删除 name = 'Tom' AND age = 28 的记录
HashMap<String, Object> columnMap = new HashMap<>();
columnMap.put("name", "Tom"); // key是数据库中的列名,不是Java属性名
columnMap.put("age", 28);
int resultMap = userMapper.deleteByMap(columnMap);
System.out.println("deleteByMap 受影响行数: " + resultMap);

}

由于我们测试删除了部分数据,建议再重新向我们的数据库插入新数据回来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
-- 创建数据库(如果不存在)
CREATE DATABASE IF NOT EXISTS `mybatis_plus_notes` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

-- 切换到目标数据库
USE `mybatis_plus_notes`;

-- 创建 user 表
DROP TABLE IF EXISTS `tb_user`;
CREATE TABLE `tb_user` (
`id` bigint unsigned NOT NULL COMMENT '主键ID',
`name` varchar(30) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '姓名',
`age` int unsigned DEFAULT NULL COMMENT '年龄',
`email` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '邮箱',
`version` int unsigned NOT NULL DEFAULT '1' COMMENT '乐观锁版本号',
`is_deleted` tinyint unsigned NOT NULL DEFAULT '0' COMMENT '逻辑删除标志(0-未删除;1-已删除)',
`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户表';

-- 插入初始数据
INSERT INTO `tb_user` (`id`, `name`, `age`, `email`) VALUES
(1, 'Jone', 18, 'test1@baomidou.com'),
(2, 'Jack', 20, 'test2@baomidou.com'),
(3, 'Tom', 28, 'test3@baomidou.com'),
(4, 'Sandy', 21, 'test4@baomidou.com'),
(5, 'Billie', 24, 'test5@baomidou.com');

2.1.3. 修改 (updateById)

updateById 会根据传入实体的ID去更新记录。

重要: updateById 方法默认会更新实体中所有字段,即使字段值为 null。这意味着如果您只想更新某个字段,需要先查询出完整记录,修改后再更新,否则其他字段可能被 null 覆盖。后续章节会讲解如何实现“部分更新”。

1
2
3
4
5
6
7
8
9
10
11
12
13
// UserMapperTest.java

@Test
void testUpdate() {
// 确保ID为 4 的用户存在
UserDO user = new UserDO();
user.setId(4L); // 指定要更新的记录ID
user.setAge(22); // 只设置age,其他字段为null

// 执行更新
int result = userMapper.updateById(user);
System.out.println("updateById 受影响行数: " + result);
}

2.1.4. 查询 (selectById, selectList, selectBatchIds, selectByMap)

查询是最高频的操作,BaseMapper 同样提供了丰富的查询方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// UserMapperTest.java

@Test
void testSelect() {
// 1. 根据主键ID查询
UserDO user = userMapper.selectById(5L);
System.out.println("selectById 查询结果: " + user);

// 2. 查询所有记录
// selectList(null) 传入null作为查询条件,代表无条件查询
List<UserDO> userList = userMapper.selectList(null);
System.out.println("selectList 查询到的总数: " + userList.size());
userList.forEach(System.out::println);

// 3. 根据多个ID批量查询
List<Long> idsToSelect = Arrays.asList(4L, 5L);
List<UserDO> usersByIds = userMapper.selectBatchIds(idsToSelect);
System.out.println("selectBatchIds 查询到的结果:");
usersByIds.forEach(System.out::println);

// 4. 根据Map中的条件查询
// 查询 name = 'Sandy' 的记录
Map<String, Object> columnMap = new HashMap<>();
columnMap.put("name", "Sandy"); // key是数据库中的列名
List<UserDO> usersByMap = userMapper.selectByMap(columnMap);
System.out.println("selectByMap 查询到的结果:");
usersByMap.forEach(System.out::println);
}

2.2. IServiceServiceImpl 的应用

直接在业务逻辑中注入 Mapper 进行数据库操作是可行的,但这是一种不良实践。专业的开发模式要求在 Controller (或业务逻辑) 与 Mapper (数据访问) 之间设立一个 Service 层

Service 层的职责:

  • 封装业务逻辑:处理复杂的业务规则。
  • 事务管理:确保多个数据库操作的原子性。
  • 解耦:隔离上层应用与底层数据访问的细节。

Mybatis-Plus 同样为 Service 层提供了强大的代码简化方案:IService 接口和 ServiceImpl 实现类。


2.2.1. 业务层接口 (UserService) 继承 IService

我们首先定义 UserService 接口,它继承 IService<UserDO>

文件路径: src/main/java/com/example/mpstudy/service/UserService.java

1
2
3
4
5
6
7
8
9
10
11
12
package com.example.mpstudy.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.example.mpstudy.domain.UserDO;

public interface UserService extends IService<UserDO> {
// IService<UserDO> 泛型接口,提供了大量便捷的业务层方法,如 save, list, page 等。
// 通过继承它,UserService 立刻就拥有了这些通用的业务方法。

// 后续我们可以在这里定义 User 相关的、IService 中没有的特定业务方法。
// 例如:void lockUser(Long userId);
}

2.2.2. 业务类实现 (UserServiceImpl) 继承 ServiceImpl

接著,我們創建 UserService 的實現類,它需要繼承 ServiceImpl

文件路径: src/main/java/com/example/mpstudy/service/impl/UserServiceImpl.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.example.mpstudy.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.mpstudy.domain.UserDO;
import com.example.mpstudy.mapper.UserMapper;
import com.example.mpstudy.service.UserService;
import org.springframework.stereotype.Service;

// @Service: 将该类标记为Spring的Service组件,交由IoC容器管理。
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, UserDO> implements UserService {
// ServiceImpl 是 IService 的官方实现类,封装了对BaseMapper的调用。
// 继承 ServiceImpl<M, T> 需要提供两个泛型:
// 1. M: 当前Service对应的Mapper接口类型,这里是 UserMapper。
// 2. T: 当前Service对应的实体类型,这里是 UserDO。

// 继承之后,所有IService中定义的方法都已自动实现,无需我们手动编写。
// 我们只需要在这里实现 UserService 中自定义的业务方法即可。
}


2.2.3. 常用 Service 方法 (save, remove, update, get, list)

IService 提供了比 BaseMapper 更符合业务语义的方法名,如 save 对应 insertgetById 对应 selectByIdlist 对应 selectList

方法名称描述需要传入的参数
save(T entity)插入单条数据entity:需要插入的实体类对象
saveBatch(Collection<T> list)批量插入数据list:需要插入的实体类对象集合
removeById(Serializable id)根据 ID 删除数据id:要删除的记录的主键 ID
remove(Wrapper<T> query)根据条件删除数据query:删除条件,通常使用 QueryWrapper
updateById(T entity)根据 ID 更新数据entity:包含更新字段的实体类对象
update(Wrapper<T> updateWrapper)根据条件更新数据updateWrapper:更新条件,通常使用 UpdateWrapper
list()查询所有数据无(无需参数,查询所有记录)
list(Wrapper<T> query)根据条件查询数据query:查询条件,通常使用 QueryWrapper
getById(Serializable id)根据 ID 查询数据id:要查询的记录的主键 ID
count()查询数据条数无(返回数据库中记录的总数)
count(Wrapper<T> query)根据条件查询数据条数query:查询条件,通常使用 QueryWrapper
getOne(Wrapper<T> query)根据条件查询一条数据query:查询条件,通常使用 QueryWrapper
  • T:表示一个实体类对象类型。例如,UserOrder 等。
  • Serializable:表示可以序列化的类型,通常是主键 ID。
  • Wrapper<T>:这是 MyBatis-Plus 提供的一个条件构造器类,常用的有 QueryWrapper(用于查询条件)和 UpdateWrapper(用于更新条件)。通过 Wrapper,你可以构建更加复杂的查询或更新条件,我们后续会详细讲解

为了测试 Service 层的功能,我们创建一个新的测试类 UserServiceTest

文件路径: src/test/java/com/example/mpstudy/service/UserServiceTest.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.example.mpstudy.service;

import com.example.mpstudy.domain.UserDO;
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 UserServiceTest {

@Autowired
private UserService userService;

@Test
void testGetAndList() {
// 1. 根据ID查询 (等同于 userMapper.selectById)
UserDO user = userService.getById(5L);
System.out.println("getById 查询结果: " + user);

// 2. 查询所有 (等同于 userMapper.selectList(null))
List<UserDO> list = userService.list();
System.out.println("list 查询到的总数: " + list.size());
}
}

2.2.4. 批量操作 (saveBatch, updateBatchById)

IService 也提供了高效的批量操作方法,它会在底层优化 SQL 的执行(例如,通过 Batch 模式),远比我们自己循环调用 saveupdate 要高效。

1
2
3
4
5
6
7
8
9
10
@Test
void testBatchOperations() {
// 1. 批量新增
List<UserDO> userList = List.of(
new UserDO().setName("BatchUser1").setAge(25).setEmail("b1@example.com"),
new UserDO().setName("BatchUser2").setAge(26).setEmail("b2@example.com")
);
boolean saveResult = userService.saveBatch(userList);
System.out.println("批量新增是否成功: " + saveResult);
}

2.2.5. saveOrUpdate 方法详解

saveOrUpdate 是一个非常智能的方法,它可以根据实体对象的主键(ID)是否存在来自动判断是执行插入还是更新操作。

  • 如果实体对象的 ID 为 null,则执行 insert
  • 如果实体对象的 ID 不为 null,则执行 updateById
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// UserServiceTest.java

@Test
void testSaveOrUpdate() {
// 1. ID为null,执行插入操作
UserDO newUser = new UserDO().setName("NewOrUpdateUser").setAge(40).setEmail("nou@example.com");
boolean insertSuccess = userService.saveOrUpdate(newUser);
System.out.println("插入操作是否成功: " + insertSuccess + ", 用户ID: " + newUser.getId());

// 2. ID不为null,执行更新操作
// 使用上一步插入的ID
UserDO existingUser = new UserDO().setId(newUser.getId()).setAge(41);
boolean updateSuccess = userService.saveOrUpdate(existingUser);
System.out.println("更新操作是否成功: " + updateSuccess);
}

2.3. 自定义接口方法

尽管 Mybatis-Plus 提供的通用 BaseMapperIService 已经能覆盖绝大多数单表操作,但在复杂的业务场景中,我们仍然需要编写自定义的 SQL,例如多表 JOIN 查询、复杂的统计或调用数据库函数等。

Mybatis-Plus 的一个核心优势在于它**“只做增强,不做改变”**。这意味着,我们可以无缝地回归到原生 MyBatis 的开发模式,定义自己的 Mapper 方法,并通过 XML 文件或注解来编写对应的 SQL 语句。


2.3.1. [实践] 自定义 Mapper 接口方法

接下来,我们将为 UserMapper 添加一个自定义方法 selectByName,用于根据姓名查询用户信息,并为其编写对应的 XML 实现。

第一步:在 UserMapper 接口中定义抽象方法

文件路径: src/main/java/com/example/mpstudy/mapper/UserMapper.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.example.mpstudy.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.mpstudy.domain.UserDO;
import org.apache.ibatis.annotations.Param; // 引入MyBatis的Param注解

public interface UserMapper extends BaseMapper<UserDO> {
// 继承了 BaseMapper 的同时,我们可以在这里定义任何自定义的方法。

/**
* 根据姓名查询用户信息
* @param name 姓名
* @return 用户信息
*/
UserDO selectByName(@Param("name") String name);
}

最佳实践: 当 Mapper 方法有多个参数时,强烈建议使用 MyBatis 提供的 @Param("...") 注解为每个参数命名。这能让 XML 文件中的 SQL 通过名称(如 #{name})清晰地引用到参数,避免因参数顺序问题导致的错误。

第二步:创建 Mapper XML 映射文件

我们需要在 resources 目录下创建一个与 UserMapper 接口对应的 XML 文件来存放我们的 SQL 语句。

文件路径: src/main/resources/mapper/UserMapper.xml

1
2
3
4
5
6
7
8
9
10
11
<?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.example.mpstudy.mapper.UserMapper">

<select id="selectByName" resultType="com.example.mpstudy.domain.UserDO">
SELECT * FROM tb_user WHERE name = #{name}
</select>

</mapper>

检查配置: 请确保您的 application.yml 文件中配置了 mybatis-plus.mapper-locations 属性,以便 Mybatis-Plus 能够找到您编写的 XML 文件。
mybatis-plus.mapper-locations: classpath*:/mapper/**/*.xml

第三步:编写单元测试进行验证

现在,我们可以在 UserMapperTest 中调用这个新的自定义方法。

文件路径: src/test/java/com/example/mpstudy/mapper/UserMapperTest.java

1
2
3
4
5
6
7
8
9
// UserMapperTest.java (添加新的测试方法)

@Test
void testCustomMethod() {
System.out.println("----- 开始执行自定义方法测试 -----");
UserDO user = userMapper.selectByName("Tom");
System.out.println("查询到的 Tom 的信息: " + user);
System.out.println("----- 自定义方法测试执行完毕 -----");
}

通过以上步骤,我们成功地在 Mybatis-Plus 的体系中集成了自定义的 SQL 查询,这证明了 MP 的高度灵活性和兼容性。对于任何通用方法无法满足的复杂需求,您都可以放心地使用这种方式来解决。


3. [进阶] 从实体映射到复杂关联查询

摘要: 本章是 Mybatis-Plus 从入门到精通的关键。我们将首先掌握如何通过注解精准控制实体与表的映射关系;随后,深入学习 MP 的灵魂——条件构造器(Wrapper),用纯 Java 代码构建任意复杂的单表动态查询;最后,我们将回归 MyBatis 的 XML 精髓,解决 Wrapper 难以处理的多表 JOIN 及一对多、多对多等复杂关联查询场景。


3.1. 实体与表映射注解

在深入学习查询之前,我们必须先打好地基——确保我们的 Java 实体类能够精准地与数据库表结构对应起来。虽然 Mybatis-Plus 提供了强大的自动映射能力,但在实际项目中,类名与表名、属性名与字段名不一致的情况非常普遍。本节将深入讲解如何通过注解来解决这些映射问题。

3.1.1. 自动映射规则回顾

Mybatis-Plus 默认遵循“驼峰与下划线”的自动映射规则,这得益于其内置的 map-underscore-to-camel-case 配置默认为 true

  • 表名映射: 实体类名 UserDO 会被自动映射为表名 user_do
  • 字段映射: 属性名 gmtCreate 会被自动映射为字段名 gmt_create

正是因为有此规则,在前面的章节中,我们的 UserDO 即使不加任何注解也能正常工作(如果我们把表名和字段名都改成下划线格式)。但当默认规则不满足需求时,就需要手动配置了。


3.1.2. 表映射: @TableName

当实体类名与表名的映射不符合默认规则时(例如,类名为 UserDO,而表名为 tb_user),就需要使用 @TableName 注解来手动指定。

文件路径: src/main/java/com/example/mpstudy/domain/UserDO.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.example.mpstudy.domain;

import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.experimental.Accessors;

@Data
@Accessors(chain = true)
// 使用 @TableName("tb_user") 明确告诉MP,此类对应数据库中的 "tb_user" 表。
// 这是最常见也是最重要的映射注解之一。
@TableName("tb_user")
public class UserDO {
// ... 属性 ...
}

3.1.3. 字段映射: @TableField

@TableField 是一个功能强大的注解,用于处理实体属性与表字段之间的各种映射问题。

1. 字段名不匹配

当属性名和字段名的映射关系不符合默认规则时(例如,属性为 email,但数据库字段为 user_email),可以使用其 value 属性来指定。

1
2
3
4
5
6
7
8
9
10
11
// UserDO.java
import com.baomidou.mybatisplus.annotation.TableField;

// ...

/**
* 邮箱
*/
// @TableField 的 value 属性用于指定数据库中对应的列名
@TableField("user_email")
private String email;

2. 属性在表中不存在 (非表字段)

有时我们希望在实体类中定义一些不与数据库表字段对应的属性,例如用于临时计算或前端展示。可以使用 exist = false 来标记,告诉 MP 忽略这个属性。

1
2
3
4
5
6
7
8
9
10
11
12
// UserDO.java
import com.baomidou.mybatisplus.annotation.TableField;

// ...

/**
* 用户的角色描述,这个字段在 tb_user 表中不存在。
*/
// @TableField(exist = false) 告诉MP,在执行任何数据库操作时(如INSERT, UPDATE, SELECT),
// 都要完全忽略这个 'userRole' 属性,它只是一个普通的Java类成员。
@TableField(exist = false)
private String userRole;

3. 控制字段是否参与查询

对于一些敏感信息(如密码)或大字段(如文章内容),我们可能希望在常规列表查询中默认不返回它们,以提高性能和安全性。可以使用 select = false 来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
// UserDO.java
import com.baomidou.mybatisplus.annotation.TableField;

// ...

/**
* 假设我们有一个密码字段
*/
// @TableField(select = false) 表示此字段在数据库中存在,
// 但在使用 MP 内置的查询方法(如 selectList)时,不会被包含在 SELECT 的字段列表中。
// 注意:INSERT 和 UPDATE 操作仍然会包含此字段。
@TableField(select = false)
private String password;

3.1.4. 主键映射: @TableId 与主键生成策略 (IdType)

@TableId 注解专门用于标识实体类中的主键属性。MP 默认会将名为 id 的属性视为主键,但显式使用 @TableId 是更规范的做法。它最重要的功能是可以通过 type 属性指定主键的生成策略。

IdType 枚举值描述适用场景
AUTO数据库ID自增。将主键生成交由数据库的自增列处理。本项目选用,兼容性好,便于测试。
ASSIGN_ID雪花算法。MP 默认策略,生成一个全局唯一的 Long 类型ID。分布式系统,需要全局唯一ID的场景。
INPUT用户手动输入。ID需要由开发者在插入前手动设置。业务主键明确,或由其他服务生成ID的场景。
ASSIGN_UUIDUUID。生成一个随机的32位字符串ID,无序。主键为 String 类型,需要唯一性的场景。
NONE无策略。未设置主键类型,会跟随全局配置。不推荐单独使用。

在我们的 UserDO 中,已经根据您的要求配置为了 IdType.AUTO

文件路径: src/main/java/com/example/mpstudy/domain/UserDO.java

1
2
3
4
5
6
7
8
9
10
11
// UserDO.java
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;

// ...

// @TableId 用于标识主键字段
// type = IdType.AUTO: 指定主键生成策略为数据库自增。
// 当我们执行 insert 操作时, MP不会为ID赋值,而是依赖数据库生成,并在插入后将生成的值回填到实体对象中。
@TableId(type = IdType.AUTO)
private Long id;