MyBatis 从入门到精通:手把手教你玩转持久层框架 🗂️

MyBatis 是 Java 领域最受欢迎的持久层框架之一,它解决了 JDBC 操作数据库时的繁琐冗长问题,让数据库操作变得优雅而高效。本文将从零开始,系统讲解 MyBatis 的核心概念、使用方法、高级特性以及最佳实践,帮你真正掌握这门实用技术!💪


📚 目录导航


一、MyBatis 概述:为什么选择 MyBatis?

1.1 JDBC 的痛点

在 MyBatis 诞生之前,Java 操作数据库主要依赖 JDBC。JDBC 虽然强大,但使用起来非常繁琐:

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
// JDBC 方式:查询用户信息
public User findUserById(Long id) {
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
User user = null;

try {
// 1. 加载驱动
Class.forName("com.mysql.cj.jdbc.Driver");

// 2. 建立连接
conn = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/mybatis_demo",
"root", "password"
);

// 3. 编写 SQL
String sql = "SELECT * FROM users WHERE id = ?";

// 4. 预编译 SQL
ps = conn.prepareStatement(sql);
ps.setLong(1, id);

// 5. 执行查询
rs = ps.executeQuery();

// 6. 手动映射结果集到对象
if (rs.next()) {
user = new User();
user.setId(rs.getLong("id"));
user.setUsername(rs.getString("username"));
user.setPassword(rs.getString("password"));
user.setEmail(rs.getString("email"));
user.setStatus(rs.getInt("status"));
user.setCreatedAt(rs.getTimestamp("created_at"));
}

} catch (Exception e) {
e.printStackTrace();
} finally {
// 7. 关闭资源(繁琐且容易出错)
if (rs != null) try { rs.close(); } catch (Exception e) {}
if (ps != null) try { ps.close(); } catch (Exception e) {}
if (conn != null) try { conn.close(); } catch (Exception e) {}
}

return user;
}

JDBC 的问题

  • ❌ 代码冗长,每个方法都要写大量模板代码
  • ❌ SQL 语句硬编码在 Java 代码中,维护困难
  • ❌ 参数映射和结果集映射需要手动编写,容易出错
  • ❌ 资源管理繁琐,容易造成资源泄漏

1.2 MyBatis 的解决方案

MyBatis 通过 XML 或注解配置 SQL,并自动处理参数映射和结果集映射,大大简化了数据库操作:

1.3 MyBatis vs Hibernate 对比

对比维度 Hibernate MyBatis
SQL 控制 框架自动生成 手动编写,完全可控
学习曲线 陡峭 平缓
性能调优 较难 容易(可直接优化 SQL)
数据库无关性 完全无关 需要适配不同数据库
关联查询 自动处理,但复杂 需要手写 JOIN
适用场景 业务简单、以查询为主 复杂业务、需要性能优化
市场占有率 逐渐下降 持续上升(国内主流)

💡 国内现状:MyBatis 凭借其灵活性和可控性,在国内 Java 企业中占据了绝对主流地位。阿里开源的 MyBatis-Plus 更是进一步简化了开发,几乎成为必学技能。


二、环境搭建与快速入门

2.1 项目结构设计

2.2 添加 Maven 依赖

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
<?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
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.example</groupId>
<artifactId>mybatis-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>

<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<mybatis.version>3.5.15</mybatis.version>
</properties>

<dependencies>
<!-- MyBatis 核心依赖 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>${mybatis.version}</version>
</dependency>

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

<!-- JUnit 测试 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>

<!-- Lombok(简化实体类) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>

<!-- 日志框架 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.9</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>2.0.9</version>
</dependency>
</dependencies>
</project>

2.3 创建数据库表

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
-- 创建数据库
CREATE DATABASE IF NOT EXISTS mybatis_demo
DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

USE mybatis_demo;

-- 创建用户表
CREATE TABLE IF NOT EXISTS users (
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '用户ID',
username VARCHAR(50) NOT NULL UNIQUE COMMENT '用户名',
password VARCHAR(255) NOT NULL COMMENT '密码',
email VARCHAR(100) COMMENT '邮箱',
phone CHAR(11) COMMENT '手机号',
status TINYINT DEFAULT 1 COMMENT '状态:1-正常,0-禁用',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

-- 创建文章表
CREATE TABLE IF NOT EXISTS articles (
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '文章ID',
user_id BIGINT NOT NULL COMMENT '作者ID',
title VARCHAR(200) NOT NULL COMMENT '标题',
content TEXT COMMENT '内容',
views INT DEFAULT 0 COMMENT '浏览量',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文章表';

-- 插入测试数据
INSERT INTO users (username, password, email, phone) VALUES
('admin', '$2a$10$xxxx', 'admin@example.com', '13800138000'),
('test', '$2a$10$yyyy', 'test@example.com', '13800138001'),
('author1', '$2a$10$zzzz', 'author@example.com', '13800138002');

INSERT INTO articles (user_id, title, content, views) VALUES
(1, 'MyBatis 入门教程', 'MyBatis 是一个优秀的持久层框架...', 100),
(1, 'MyBatis 进阶指南', '本文将深入讲解 MyBatis 的高级特性...', 200),
(2, 'JDBC vs MyBatis', '对比 JDBC 和 MyBatis 的优缺点...', 150),
(3, 'MyBatis Plus 使用手册', 'MyBatis Plus 是 MyBatis 的增强工具...', 300);

2.4 MyBatis 全局配置文件

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
78
79
80
81
82
83
84
85
86
87
88
89
90
91
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>

<!-- 引入外部配置文件(数据库连接信息等) -->
<properties resource="jdbc.properties"/>

<!-- 配置 MyBatis 日志输出 -->
<settings>
<!-- 标准日志实现 -->
<setting name="logImpl" value="SLF4J"/>

<!-- 开启驼峰命名自动映射:user_id -> userId -->
<setting name="mapUnderscoreToCamelCase" value="true"/>

<!-- 开启延迟加载(按需加载关联对象) -->
<setting name="lazyLoadingEnabled" value="true"/>

<!-- 设置超时时间(秒) -->
<setting name="defaultStatementTimeout" value="30"/>

<!-- 开启二级缓存 -->
<setting name="cacheEnabled" value="true"/>

<!-- 配置默认枚举类型处理器 -->
<setting name="defaultEnumTypeHandler" value="org.apache.ibatis.type.EnumTypeHandler"/>
</settings>

<!-- 类型别名:简化 XML 中的类名书写 -->
<typeAliases>
<!-- 单个类起别名 -->
<typeAlias type="com.example.entity.User" alias="User"/>

<!-- 批量起别名:默认别名是类名(不区分大小写) -->
<package name="com.example.entity"/>
</typeAliases>

<!-- 配置类型处理器 -->
<typeHandlers>
<package name="com.example.handler"/>
</typeHandlers>

<!-- 环境配置:可以有多个环境(如开发、测试、生产) -->
<environments default="development">

<!-- 开发环境 -->
<environment id="development">
<!-- 事务管理类型:JDBC / MANAGED -->
<transactionManager type="JDBC"/>

<!-- 数据源类型:UNPOOLED / POOLED / JNDI -->
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>

<!-- 连接池配置(POOLED 类型) -->
<property name="poolMaximumActiveConnections" value="10"/>
<property name="poolMaximumIdleConnections" value="5"/>
<property name="poolMaximumCheckoutTime" value="20000"/>
<property name="poolTimeToWait" value="20000"/>
</dataSource>
</environment>

<!-- 测试环境 -->
<environment id="test">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</dataSource>
</environment>
</environments>

<!-- 映射器:指定 Mapper XML 文件的位置 -->
<mappers>
<!-- 方式一:单个 Mapper XML -->
<mapper resource="mapper/UserMapper.xml"/>

<!-- 方式二:单个 Mapper 接口(XML 和接口同名的推荐方式) -->
<!-- <mapper class="com.example.mapper.UserMapper"/> -->

<!-- 方式三:批量注册 Mapper 接口 -->
<package name="com.example.mapper"/>
</mappers>

</configuration>

2.5 数据库连接配置文件

1
2
3
4
5
# jdbc.properties
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/mybatis_demo?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8
jdbc.username=root
jdbc.password=your_password

三、MyBatis 核心组件详解

3.1 核心组件架构

MyBatis 的核心组件包括以下几个关键角色:

组件 作用 生命周期
SqlSessionFactoryBuilder 构建 SqlSessionFactory 应用级别,整个应用只创建一次
SqlSessionFactory 创建 SqlSession 应用级别,应用启动到停止
SqlSession 执行 CRUD 操作 请求级别,每次数据库操作创建
Mapper 接口 定义数据操作方法 由 SqlSession 调用
Mapper XML 编写 SQL 语句和映射规则 由 MyBatis 解析执行

3.2 实体类创建

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
package com.example.entity;

import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import lombok.Builder;

import java.util.Date;

/**
* 用户实体类
*/
@Data // Lombok:自动生成 getter/setter/toString
@NoArgsConstructor // 无参构造
@AllArgsConstructor // 全参构造
@Builder // 构建者模式
public class User {

private Long id;
private String username;
private String password;
private String email;
private String phone;
private Integer status;
private Date createdAt;
private Date updatedAt;
}

3.3 SqlSessionFactory 构建

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.util;

import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

import java.io.IOException;
import java.io.InputStream;

/**
* MyBatis 工具类:管理 SqlSessionFactory
*/
public class MyBatisUtil {

// SqlSessionFactory 应在整个应用期间只创建一次
private static SqlSessionFactory sqlSessionFactory;

static {
try {
// 读取 MyBatis 配置文件
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);

// 构建 SqlSessionFactory
sqlSessionFactory = new SqlSessionFactoryBuilder()
.build(inputStream);

System.out.println("✅ SqlSessionFactory 初始化成功");

} catch (IOException e) {
throw new RuntimeException("初始化 SqlSessionFactory 失败", e);
}
}

/**
* 获取 SqlSessionFactory
*/
public static SqlSessionFactory getSqlSessionFactory() {
return sqlSessionFactory;
}

/**
* 获取 SqlSession
*/
public static SqlSession openSession() {
return sqlSessionFactory.openSession();
}

/**
* 获取带自动提交的 SqlSession
*/
public static SqlSession openSession(boolean autoCommit) {
return sqlSessionFactory.openSession(autoCommit);
}
}

3.4 Mapper 接口定义

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
package com.example.mapper;

import com.example.entity.User;
import org.apache.ibatis.annotations.Param;

import java.util.List;
import java.util.Map;

/**
* 用户 Mapper 接口
* MyBatis 会根据接口创建代理对象,代理对象执行 CRUD
*/
public interface UserMapper {

/**
* 根据 ID 查询用户
*/
User findById(Long id);

/**
* 查询所有用户
*/
List<User> findAll();

/**
* 条件查询:用户名模糊匹配 + 状态筛选
*/
List<User> findByConditions(@Param("username") String username,
@Param("status") Integer status);

/**
* 多参数查询:使用 Map
*/
List<User> findByMap(Map<String, Object> params);

/**
* 插入用户
*/
int insert(User user);

/**
* 插入用户(返回自增主键)
*/
int insertAndReturnId(User user);

/**
* 更新用户
*/
int update(User user);

/**
* 删除用户
*/
int deleteById(Long id);

/**
* 批量删除
*/
int batchDelete(@Param("ids") List<Long> ids);

/**
* 使用 @Select 注解定义查询
*/
User findByUsername(@Param("username") String username);

/**
* 统计用户数量
*/
int count();
}

3.5 SqlSession 执行 CRUD

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
package com.example.test;

import com.example.entity.User;
import com.example.mapper.UserMapper;
import com.example.util.MyBatisUtil;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;

import java.util.List;

public class SqlSessionDemo {

private static SqlSessionFactory factory = MyBatisUtil.getSqlSessionFactory();

public static void main(String[] args) {
// 创建 SqlSession(每次创建都是新的)
try (SqlSession session = factory.openSession()) {

// 获取 Mapper 接口的代理对象
UserMapper mapper = session.getMapper(UserMapper.class);

// 调用方法执行查询
User user = mapper.findById(1L);
System.out.println("查询结果:" + user);

// 提交事务
session.commit();
}
}
}

四、CRUD 增删改查操作

4.1 Mapper 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
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
<?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">

<!-- namespace 必须与 Mapper 接口全限定名一致 -->
<mapper namespace="com.example.mapper.UserMapper">

<!-- ========== 查询操作 ========== -->

<!-- resultType 指定返回类型(单条记录的类型) -->
<select id="findById" resultType="User">
SELECT * FROM users WHERE id = #{id}
</select>

<!-- 查询所有用户 -->
<select id="findAll" resultType="User">
SELECT id, username, password, email, phone, status,
created_at AS createdAt, updated_at AS updatedAt
FROM users
ORDER BY id DESC
</select>

<!-- 单参数查询:使用 #{参数名} 占位 -->
<select id="findByUsername" resultType="User">
SELECT * FROM users WHERE username = #{username}
</select>

<!-- 多参数查询:@Param 注解指定参数名 -->
<select id="findByConditions" resultType="User">
SELECT * FROM users
WHERE 1=1
<if test="username != null and username != ''">
AND username LIKE CONCAT('%', #{username}, '%')
</if>
<if test="status != null">
AND status = #{status}
</if>
ORDER BY created_at DESC
</select>

<!-- 使用 Map 作为参数 -->
<select id="findByMap" resultType="User">
SELECT * FROM users
WHERE 1=1
<if test="username != null">
AND username LIKE CONCAT('%', #{username}, '%')
</if>
<if test="status != null">
AND status = #{status}
</if>
ORDER BY id DESC
</select>

<!-- 统计查询:返回简单类型 -->
<select id="count" resultType="int">
SELECT COUNT(*) FROM users
</select>

<!-- ========== 插入操作 ========== -->

<!-- 插入用户(自增主键会自动返回到对象中) -->
<insert id="insert" parameterType="User">
INSERT INTO users (username, password, email, phone, status)
VALUES (#{username}, #{password}, #{email}, #{phone}, #{status})
</insert>

<!-- 插入并获取自增主键(方式一:useGeneratedKeys) -->
<insert id="insertAndReturnId" parameterType="User"
useGeneratedKeys="true" keyProperty="id">
INSERT INTO users (username, password, email, phone, status)
VALUES (#{username}, #{password}, #{email}, #{phone}, #{status})
</insert>

<!-- 插入并获取自增主键(方式二:selectKey) -->
<insert id="insertAndReturnId2" parameterType="User">
<selectKey keyProperty="id" resultType="long" order="AFTER">
SELECT LAST_INSERT_ID()
</selectKey>
INSERT INTO users (username, password, email, phone, status)
VALUES (#{username}, #{password}, #{email}, #{phone}, #{status})
</insert>

<!-- 批量插入 -->
<insert id="batchInsert" parameterType="java.util.List">
INSERT INTO users (username, password, email, phone, status)
VALUES
<foreach collection="list" item="user" separator=",">
(#{user.username}, #{user.password}, #{user.email},
#{user.phone}, #{user.status})
</foreach>
</insert>

<!-- ========== 更新操作 ========== -->

<!-- 更新用户 -->
<update id="update" parameterType="User">
UPDATE users
SET username = #{username},
password = #{password},
email = #{email},
phone = #{phone},
status = #{status}
WHERE id = #{id}
</update>

<!-- 动态更新(只更新非空字段) -->
<update id="dynamicUpdate" parameterType="User">
UPDATE users
<set>
<if test="username != null and username != ''">
username = #{username},
</if>
<if test="password != null and password != ''">
password = #{password},
</if>
<if test="email != null">
email = #{email},
</if>
<if test="phone != null">
phone = #{phone},
</if>
<if test="status != null">
status = #{status},
</if>
</set>
WHERE id = #{id}
</update>

<!-- ========== 删除操作 ========== -->

<!-- 删除用户 -->
<delete id="deleteById">
DELETE FROM users WHERE id = #{id}
</delete>

<!-- 批量删除 -->
<delete id="batchDelete">
DELETE FROM users WHERE id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</delete>

</mapper>

4.2 #{} vs ${} 区别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- #{}:预编译参数绑定(推荐,防止 SQL 注入) -->
<!-- 最终执行:SELECT * FROM users WHERE id = ? -->
<select id="findById" resultType="User">
SELECT * FROM users WHERE id = #{id}
</select>

<!-- ${}:直接字符串替换(存在 SQL 注入风险,仅适用于固定值) -->
<!-- 最终执行:SELECT * FROM users WHERE id = 1 -->
<select id="findByIdFixed" resultType="User">
SELECT * FROM users WHERE id = ${id}
</select>

<!-- 使用 ${} 的典型场景:动态表名、排序字段 -->
<select id="findAll" resultType="User">
SELECT * FROM users ORDER BY ${sortField} ${sortOrder}
</select>
特性 #{} ${}
SQL 编译 预编译,参数绑定 直接字符串替换
SQL 注入 ✅ 安全 ❌ 危险
性能 ✅ 优(预编译后复用) 一般
适用场景 参数值 动态列名/表名

⚠️ 安全警示:绝对不要使用 ${} 接收用户输入的原始值,否则可能遭受 SQL 注入攻击!

4.3 结果集映射配置

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
<!-- 方式一:开启驼峰映射(mybatis-config.xml 中配置) -->
<!-- user_id 自动映射到 userId -->
<setting name="mapUnderscoreToCamelCase" value="true"/>

<!-- 方式二:使用 resultMap 手动映射 -->
<resultMap id="UserResultMap" type="User">
<!-- 主键映射 -->
<id property="id" column="id"/>

<!-- 普通字段映射 -->
<result property="username" column="username"/>
<result property="password" column="password"/>
<result property="email" column="email"/>
<result property="phone" column="phone"/>
<result property="status" column="status"/>

<!-- 特殊字段映射:数据库下划线 -> Java 驼峰 -->
<result property="createdAt" column="created_at"/>
<result property="updatedAt" column="updated_at"/>
</resultMap>

<!-- 使用 resultMap -->
<select id="findById" resultMap="UserResultMap">
SELECT * FROM users WHERE id = #{id}
</select>

<!-- 方式三:使用 association 处理一对一关联 -->
<resultMap id="UserArticleMap" type="com.example.entity.UserArticleVO">
<id property="id" column="id"/>
<result property="username" column="username"/>
<result property="email" column="email"/>

<!-- 一对一关联:嵌套查询 -->
<association property="latestArticle" column="id"
select="com.example.mapper.ArticleMapper.findLatestByUserId"/>
</resultMap>

<!-- 方式四:使用 collection 处理一对多关联 -->
<resultMap id="UserWithArticlesMap" type="User">
<id property="id" column="id"/>
<result property="username" column="username"/>
<result property="email" column="email"/>

<!-- 一对多关联:嵌套查询 -->
<collection property="articles" column="id"
select="com.example.mapper.ArticleMapper.findByUserId"/>
</resultMap>

4.4 关联查询实战

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 文章实体类
@Data
public class Article {
private Long id;
private Long userId;
private String title;
private String content;
private Integer views;
private Date createdAt;
}

// 用户文章 VO
@Data
public class UserArticleVO {
private Long id;
private String username;
private String email;
private String latestArticleTitle;
private Integer latestArticleViews;
}
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
<!-- ArticleMapper.xml -->
<mapper namespace="com.example.mapper.ArticleMapper">

<!-- 按用户 ID 查询文章列表 -->
<select id="findByUserId" resultType="Article">
SELECT * FROM articles WHERE user_id = #{userId}
ORDER BY created_at DESC
</select>

<!-- 查询某用户最新文章 -->
<select id="findLatestByUserId" resultType="Article">
SELECT * FROM articles WHERE user_id = #{userId}
ORDER BY created_at DESC LIMIT 1
</select>

</mapper>

<!-- UserMapper.xml:嵌套 select 方式 -->
<resultMap id="UserWithArticlesMap" type="User">
<id property="id" column="id"/>
<result property="username" column="username"/>
<result property="email" column="email"/>

<!-- 一对多关联:用户有多篇文章 -->
<collection property="articles" column="id"
select="com.example.mapper.ArticleMapper.findByUserId"/>
</resultMap>

<select id="findUserWithArticles" resultMap="UserWithArticlesMap">
SELECT * FROM users WHERE id = #{id}
</select>

<!-- UserMapper.xml:JOIN 查询方式(一次查询,性能更好) -->
<resultMap id="UserWithArticlesMap2" type="User">
<id property="id" column="id"/>
<result property="username" column="username"/>
<result property="email" column="email"/>

<!-- 一对多关联:嵌套结果集 -->
<collection property="articles" ofType="Article">
<id property="id" column="article_id"/>
<result property="title" column="title"/>
<result property="content" column="content"/>
<result property="views" column="views"/>
</collection>
</resultMap>

<select id="findUserWithArticles2" resultMap="UserWithArticlesMap2">
SELECT u.id, u.username, u.email,
a.id AS article_id, a.title, a.content, a.views
FROM users u
LEFT JOIN articles a ON u.id = a.user_id
WHERE u.id = #{id}
</select>

五、动态 SQL:让 SQL 灵动起来

5.1 动态 SQL 核心标签

MyBatis 的动态 SQL 是其最强大的特性之一,可以根据条件动态拼接 SQL:

5.2 if 标签:条件判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!-- 使用 if 标签动态添加查询条件 -->
<select id="findByConditions" resultType="User">
SELECT * FROM users
WHERE 1=1
<if test="username != null and username.trim() != ''">
AND username LIKE CONCAT('%', #{username}, '%')
</if>
<if test="status != null">
AND status = #{status}
</if>
<if test="email != null">
AND email = #{email}
</if>
<if test="createdAfter != null">
AND created_at &gt;= #{createdAfter}
</if>
ORDER BY created_at DESC
</select>

5.3 where / set / trim 标签

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
<!-- where 标签:自动处理 WHERE 和多余 AND/OR -->
<select id="findByConditions2" resultType="User">
SELECT * FROM users
<where>
<if test="username != null and username != ''">
AND username LIKE CONCAT('%', #{username}, '%')
</if>
<if test="status != null">
AND status = #{status}
</if>
<if test="email != null">
AND email = #{email}
</if>
</where>
ORDER BY created_at DESC
</select>

<!-- set 标签:自动处理 UPDATE 语句中的多余逗号 -->
<update id="dynamicUpdate" parameterType="User">
UPDATE users
<set>
<if test="username != null and username != ''">
username = #{username},
</if>
<if test="password != null and password != ''">
password = #{password},
</if>
<if test="email != null">
email = #{email},
</if>
<if test="phone != null">
phone = #{phone},
</if>
<if test="status != null">
status = #{status},
</if>
</set>
WHERE id = #{id}
</update>

<!-- trim 标签:自定义前后缀处理 -->
<!-- 等价于 where 标签 -->
<trim prefix="WHERE" prefixOverrides="AND |OR ">
<if test="username != null">AND username = #{username}</if>
</trim>

<!-- 等价于 set 标签 -->
<trim prefix="SET" suffix="WHERE id = #{id}" suffixOverrides=",">
<if test="username != null">username = #{username},</if>
<if test="email != null">email = #{email},</if>
</trim>

5.4 foreach 标签:循环遍历

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
<!-- 批量插入 -->
<insert id="batchInsert" parameterType="java.util.List">
INSERT INTO users (username, password, email, status)
VALUES
<foreach collection="list" item="user" separator=",">
(#{user.username}, #{user.password}, #{user.email}, #{user.status})
</foreach>
</insert>

<!-- 批量删除(IN 查询) -->
<delete id="batchDelete">
DELETE FROM users WHERE id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</delete>

<!-- 批量更新状态 -->
<update id="batchUpdateStatus">
UPDATE users SET status = #{status}
WHERE id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</update>

<!-- foreach 的各项属性说明 -->
<!--
collection:要遍历的集合/数组/Map
item:遍历时当前元素的别名
index:遍历时当前元素的索引(0 开始)
open:拼接开始字符
close:拼接结束字符
separator:元素之间的分隔符
-->

5.5 choose / when / otherwise 标签

1
2
3
4
5
// 搜索条件:可以指定多个搜索维度,但只使用一个
public List<User> searchUsers(String type, String keyword) {
// type 可以是 "username"、"email"、"phone" 之一
// keyword 是搜索关键词
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!-- choose 标签:多选一,类似 switch-case -->
<select id="searchUsers" resultType="User">
SELECT * FROM users
<choose>
<!-- 优先按用户名搜索 -->
<when test="type == 'username' and keyword != null and keyword != ''">
WHERE username = #{keyword}
</when>
<!-- 其次按邮箱搜索 -->
<when test="type == 'email' and keyword != null and keyword != ''">
WHERE email = #{keyword}
</when>
<!-- 再按手机号搜索 -->
<when test="type == 'phone' and keyword != null and keyword != ''">
WHERE phone = #{keyword}
</when>
<!-- 都不满足则查询所有 -->
<otherwise>
WHERE status = 1
</otherwise>
</choose>
ORDER BY created_at DESC
</select>

5.6 动态 SQL 完整示例

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
<!-- 复杂的动态查询:多条件组合,支持排序 -->
<select id="complexSearch" resultType="User">
SELECT * FROM users
<where>
<!-- 用户名模糊搜索 -->
<if test="username != null and username != ''">
AND username LIKE CONCAT('%', #{username}, '%')
</if>

<!-- 状态精确匹配 -->
<if test="status != null">
AND status = #{status}
</if>

<!-- 批量状态查询:status in (1, 2) -->
<if test="statusList != null and statusList.size > 0">
AND status IN
<foreach collection="statusList" item="s" open="(" separator="," close=")">
#{s}
</foreach>
</if>

<!-- 创建时间范围 -->
<if test="startDate != null">
AND created_at &gt;= #{startDate}
</if>
<if test="endDate != null">
AND created_at &lt;= #{endDate}
</if>

<!-- ID 范围 -->
<if test="minId != null">
AND id &gt;= #{minId}
</if>
<if test="maxId != null">
AND id &lt;= #{maxId}
</if>
</where>

<!-- 动态排序 -->
<choose>
<when test="sortField == 'username'">
ORDER BY username ${sortOrder}
</when>
<when test="sortField == 'createdAt'">
ORDER BY created_at ${sortOrder}
</when>
<otherwise>
ORDER BY id DESC
</otherwise>
</choose>

<!-- 分页查询 -->
<if test="offset != null and limit != null">
LIMIT #{offset}, #{limit}
</if>
</select>

六、映射器配置与自动映射

6.1 resultMap 详解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!-- 基本 resultMap -->
<resultMap id="UserResultMap" type="User">
<id property="id" column="id"/>
<result property="username" column="username"/>
<result property="email" column="email"/>
<result property="status" column="status"/>
<!-- 开启驼峰后,以下两行可以省略 -->
<result property="createdAt" column="created_at"/>
<result property="updatedAt" column="updated_at"/>
</resultMap>

<!-- 自动映射级别设置 -->
<!--
autoMappingBehavior:
- NONE:禁用自动映射
- PARTIAL:自动映射除了嵌套结果映射之外的所有列(默认)
- FULL:自动映射所有列,包括嵌套结果映射
-->
<setting name="autoMappingBehavior" value="PARTIAL"/>

6.2 关联嵌套查询 vs 嵌套结果查询

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
<!-- 方式一:嵌套 select(会产生 N+1 查询) -->
<!-- 执行:1 次用户查询 + N 次文章查询 -->
<resultMap id="UserWithArticlesMap1" type="User">
<id property="id" column="id"/>
<result property="username" column="username"/>

<collection property="articles" column="id"
select="com.example.mapper.ArticleMapper.findByUserId"/>
</resultMap>

<!-- 方式二:嵌套 result(推荐,一次查询) -->
<resultMap id="UserWithArticlesMap2" type="User">
<id property="id" column="id"/>
<result property="username" column="username"/>

<collection property="articles" ofType="Article">
<id property="id" column="a_id"/>
<result property="title" column="title"/>
<result property="content" column="content"/>
</collection>
</resultMap>

<select id="findUserWithArticles2" resultMap="UserWithArticlesMap2">
SELECT u.id, u.username,
a.id AS a_id, a.title, a.content
FROM users u
LEFT JOIN articles a ON u.id = a.user_id
WHERE u.id = #{id}
</select>

6.3 discriminator 鉴别器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- discriminator:鉴别器,类似 switch,根据条件使用不同的 resultMap -->
<resultMap id="VehicleMap" type="Vehicle">
<id property="id" column="id"/>
<result property="brand" column="brand"/>

<!-- 鉴别器:根据 vehicle_type 字段选择不同的映射 -->
<discriminator column="vehicle_type" javaType="string">
<!-- 轿车 -->
<case value="car" resultMap="CarResultMap"/>
<!-- 卡车 -->
<case value="truck" resultMap="TruckResultMap"/>
<!-- 默认使用基础映射 -->
<case value="default" resultMap="BaseVehicleResultMap"/>
</discriminator>
</resultMap>

七、类型处理器与类型转换

7.1 内置类型处理器

MyBatis 内置了大量类型处理器,支持常见数据类型的转换:

类型 处理器
字符串 StringTypeHandler
整数 IntegerTypeHandler
长整数 LongTypeHandler
浮点数 FloatTypeHandler、DoubleTypeHandler
布尔值 BooleanTypeHandler
日期时间 DateTypeHandler、TimestampTypeHandler
字节数组 ByteArrayTypeHandler
枚举 EnumTypeHandler(存储枚举名)、EnumOrdinalTypeHandler(存储枚举序号)

7.2 自定义类型处理器

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
package com.example.handler;

import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.TypeHandler;

import java.sql.*;

/**
* 自定义类型处理器:处理 JSON 字符串与 Java Map 的转换
*/
public class JsonTypeHandler implements TypeHandler<Object> {

@Override
public void setParameter(PreparedStatement ps, int i, Object parameter,
JdbcType jdbcType) throws SQLException {
// Java 对象 -> JSON 字符串 -> 存入数据库
if (parameter == null) {
ps.setNull(i, Types.VARCHAR);
} else {
String json = JsonUtil.toJson(parameter);
ps.setString(i, json);
}
}

@Override
public Object getResult(ResultSet rs, String columnName) throws SQLException {
// JSON 字符串 -> 从数据库读取
String json = rs.getString(columnName);
return json == null ? null : JsonUtil.fromJson(json, Object.class);
}

@Override
public Object getResult(ResultSet rs, int columnIndex) throws SQLException {
String json = rs.getString(columnIndex);
return json == null ? null : JsonUtil.fromJson(json, Object.class);
}

@Override
public Object getResult(CallableStatement cs, int columnIndex)
throws SQLException {
String json = cs.getString(columnIndex);
return json == null ? null : JsonUtil.fromJson(json, Object.class);
}
}

7.3 注册自定义类型处理器

1
2
3
4
5
6
7
<!-- 在 mybatis-config.xml 中注册 -->
<typeHandlers>
<package name="com.example.handler"/>
</typeHandlers>

<!-- 或在 resultMap 中单独指定 -->
<result property="data" column="data" typeHandler="com.example.handler.JsonTypeHandler"/>

7.4 枚举类型处理器

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
// 用户状态枚举
public enum UserStatus {
// 普通枚举:默认使用 EnumTypeHandler,按名字存储
ACTIVE("正常"), // 存储字符串 "ACTIVE"
INACTIVE("禁用"),
DELETED("已删除");

private final String desc;

UserStatus(String desc) {
this.desc = desc;
}

public String getDesc() {
return desc;
}
}

// 枚举Ordinal:按序号存储
public enum OrderStatus {
PENDING, // 存储 0
PAID, // 存储 1
SHIPPED, // 存储 2
COMPLETED; // 存储 3
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- 方式一:使用默认的 EnumTypeHandler(存储枚举名) -->
<!-- 数据库中存储:ACTIVE, INACTIVE -->
<insert id="insert" parameterType="User">
INSERT INTO users (username, status) VALUES (#{username}, #{status})
</insert>

<!-- 方式二:自定义枚举处理器(存储中文描述) -->
<insert id="insert" parameterType="User">
INSERT INTO users (username, status) VALUES (#{username}, #{statusDesc})
</insert>

<!-- 方式三:使用 EnumOrdinalTypeHandler(存储枚举序号) -->
<!-- 需要在 mybatis-config.xml 中设置 -->
<setting name="defaultEnumTypeHandler"
value="org.apache.ibatis.type.EnumOrdinalTypeHandler"/>

八、分页插件与性能优化

8.1 PageHelper 分页插件

添加依赖:

1
2
3
4
5
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>6.1.0</version>
</dependency>

配置插件:

1
2
3
4
5
6
7
8
9
10
<!-- mybatis-config.xml -->
<plugins>
<plugin interceptor="com.github.pagehelper.PageInterceptor">
<!-- config 参数:插件配置 -->
<property name="helperDialect" value="mysql"/>
<property name="reasonable" value="true"/>
<property name="supportMethodsArguments" value="true"/>
<property name="params" value="count=countSql"/>
</plugin>
</plugins>

使用分页:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 分页查询示例
public PageInfo<User> findByPage(int pageNum, int pageSize) {
try (SqlSession session = MyBatisUtil.openSession()) {
UserMapper mapper = session.getMapper(UserMapper.class);

// 开启分页(必须放在查询方法之前)
PageHelper.startPage(pageNum, pageSize);

// 执行查询
List<User> users = mapper.findAll();

// 封装分页结果
return new PageInfo<>(users);
}
}

// 使用 PageInfo 获取分页信息
PageInfo<User> pageInfo = findByPage(1, 10);
System.out.println("当前页:" + pageInfo.getPageNum());
System.out.println("每页条数:" + pageInfo.getPageSize());
System.out.println("总记录数:" + pageInfo.getTotal());
System.out.println("总页数:" + pageInfo.getPages());
System.out.println("数据:" + pageInfo.getList());

8.2 性能优化技巧

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
<!-- 1. 使用分页查询避免全表扫描 -->
<select id="findByPage" resultType="User">
SELECT * FROM users
<where>
<if test="status != null">
status = #{status}
</if>
</where>
ORDER BY id DESC
LIMIT #{offset}, #{limit}
</select>

<!-- 2. 使用 LIMIT 限制返回条数 -->
<select id="findTop10" resultType="User">
SELECT * FROM users ORDER BY created_at DESC LIMIT 10
</select>

<!-- 3. 使用 EXISTS 替代 IN(子查询) -->
<!-- 低效:IN 子查询可能先执行完再匹配 -->
<select id="findWithIn" resultType="User">
SELECT * FROM users
WHERE id IN (SELECT user_id FROM orders WHERE status = 'paid')
</select>

<!-- 高效:EXISTS 只要找到一个匹配就停止 -->
<select id="findWithExists" resultType="User">
SELECT * FROM users u
WHERE EXISTS (
SELECT 1 FROM orders o WHERE o.user_id = u.id AND o.status = 'paid'
)
</select>

<!-- 4. 批量操作优化:使用 foreach 批量插入 -->
<insert id="batchInsert">
INSERT INTO users (username, password, email) VALUES
<foreach collection="list" item="user" separator=",">
(#{user.username}, #{user.password}, #{user.email})
</foreach>
</insert>

<!-- 5. 使用延迟加载优化关联查询 -->
<!-- 只有在真正访问关联对象时才查询 -->
<setting name="lazyLoadingEnabled" value="true"/>

8.3 MyBatis 日志配置

1
2
3
4
5
6
7
8
9
<!-- mybatis-config.xml -->
<settings>
<!-- 日志实现(可选:SLF4J | LOG4J | LOG4J2 | JDK_LOGGING | ...) -->
<setting name="logImpl" value="SLF4J"/>

<!-- SQL 日志输出(开发调试用) -->
<setting name="logPrefix" value="mybatis-sql"/>
<setting name="logImpl" value="org.apache.ibatis.logging.slf4j.Slf4jImpl"/>
</settings>
1
2
3
4
5
6
7
8
9
10
// 开启 SQL 日志的简单工具类
public class SqlLogger {

public static void logSql(String statementId, String sql) {
System.out.println("========== SQL 日志 ==========");
System.out.println("Statement ID: " + statementId);
System.out.println("SQL: " + sql);
System.out.println("==============================");
}
}

九、MyBatis-Plus 进阶扩展

9.1 什么是 MyBatis-Plus?

MyBatis-Plus(简称 MP)是 MyBatis 的增强工具,在 MyBatis 基础上只做增强不做改变,为简化开发而生。

9.2 MyBatis-Plus 快速入门

添加依赖:

1
2
3
4
5
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.5</version>
</dependency>

配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# application.yml
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/mybatis_demo?useSSL=false
username: root
password: your_password

# MyBatis-Plus 配置
mybatis-plus:
# Mapper 扫描路径
mapper-locations: classpath*:/mapper/**/*.xml
# 类型别名包扫描
type-aliases-package: com.example.entity
# 开启驼峰
map-underscore-to-camel-case: true
# 日志配置
configuration:
log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl

实体类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Data
// @TableName 指定表名(不指定则默认驼峰转下划线)
@TableName("users")
public class User {

// @TableId 指定主键(不指定默认使用 id)
@TableId(type = IdType.AUTO) // AUTO: 自增主键
private Long id;

// @TableField 指定字段映射
@TableField("username")
private String username;

private String password;
private String email;
private String phone;

// 自动填充字段(如创建时间、更新时间)
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createdAt;

@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updatedAt;
}

Mapper 接口:

1
2
3
4
5
6
7
// 继承 BaseMapper 即可拥有完整的 CRUD 功能
@Mapper
public interface UserMapper extends BaseMapper<User> {

// 自定义方法
User findByUsername(String username);
}

9.3 CRUD 示例

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
78
79
80
81
@Mapper
public interface UserMapper extends BaseMapper<User> {
}

@Service
public class UserService {

@Autowired
private UserMapper userMapper;

// ===== 增 =====
public void addUser(User user) {
userMapper.insert(user); // 自动插入(忽略主键为 null 的字段)
}

public void addUserIgnoreNull(User user) {
userMapper.insertAllColumn(user); // 包括 null 值的字段
}

// ===== 删 =====
public void deleteById(Long id) {
userMapper.deleteById(id);
}

public void batchDelete(List<Long> ids) {
userMapper.deleteBatchIds(ids); // 批量删除
}

public void deleteByCondition(String username) {
// 条件构造器删除
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getUsername, username);
userMapper.delete(wrapper);
}

// ===== 改 =====
public void updateUser(User user) {
userMapper.updateById(user); // 根据主键更新非 null 字段
}

public void updateStatus(Long id, Integer status) {
// Lambda 更新
LambdaUpdateWrapper<User> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(User::getId, id)
.set(User::getStatus, status);
userMapper.update(null, wrapper);
}

// ===== 查 =====
public User getById(Long id) {
return userMapper.selectById(id);
}

public List<User> getAll() {
return userMapper.selectList(null); // null 表示无条件
}

public List<User> getByStatus(Integer status) {
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getStatus, status)
.orderByDesc(User::getCreatedAt);
return userMapper.selectList(wrapper);
}

public List<User> search(String keyword) {
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.likeRight("username", keyword) // username LIKE 'keyword%'
.or()
.likeLeft("email", keyword) // OR email LIKE '%keyword'
.eq("status", 1);
return userMapper.selectList(wrapper);
}

public Page<User> getByPage(int pageNum, int pageSize) {
Page<User> page = new Page<>(pageNum, pageSize);
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq("status", 1)
.orderByDesc("created_at");
return userMapper.selectPage(page, wrapper);
}
}

9.4 Lambda 条件构造器

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
// Lambda 条件构造器:使用方法引用,告别字符串硬编码
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();

// 等于
wrapper.eq(User::getStatus, 1);

// 不等于
wrapper.ne(User::getStatus, 0);

// LIKE 查询
wrapper.like(User::getUsername, "test"); // LIKE '%test%'
wrapper.likeLeft(User::getEmail, "@gmail.com"); // LIKE '%@gmail.com'
wrapper.likeRight(User::getEmail, "test"); // LIKE 'test%'

// IN 查询
wrapper.in(User::getId, Arrays.asList(1L, 2L, 3L));

// BETWEEN 查询
wrapper.between(User::getCreatedAt, startDate, endDate);

// IS NULL / IS NOT NULL
wrapper.isNull(User::getEmail);
wrapper.isNotNull(User::getPhone);

// 排序
wrapper.orderByAsc(User::getCreatedAt);
wrapper.orderByDesc(User::getCreatedAt);

// 分页
Page<User> page = new Page<>(1, 10);
wrapper.orderByDesc(User::getCreatedAt);
Page<User> result = userMapper.selectPage(page, wrapper);

// 聚合查询
wrapper.select(User::getId, User::getUsername, User::getEmail);
wrapper.groupBy(User::getStatus);
wrapper.having("COUNT(*) > 1");

十、常见问题与最佳实践

10.1 常见问题与解决方案

10.2 MyBatis 配置清单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- mybatis-config.xml 推荐配置 -->
<settings>
<!-- 开启驼峰命名映射 -->
<setting name="mapUnderscoreToCamelCase" value="true"/>

<!-- 开启延迟加载 -->
<setting name="lazyLoadingEnabled" value="true"/>

<!-- 设置超时时间(秒) -->
<setting name="defaultStatementTimeout" value="30"/>

<!-- 开启二级缓存 -->
<setting name="cacheEnabled" value="true"/>

<!-- 配置日志 -->
<setting name="logImpl" value="SLF4J"/>
</settings>

10.3 SQL 编写最佳实践

场景 推荐做法 避免做法
参数传递 使用 #{} 手动拼接 ${}
表名/列名动态 使用 ${} 但需白名单校验 直接使用用户输入
模糊查询 LIKE CONCAT('%', #{val}, '%') LIKE '%#{val}%'
批量操作 foreach 批量 SQL 循环单条执行
分页 使用 LIMIT 全表查询后再分页
关联查询 嵌套 result 或 JOIN 嵌套 select(N+1)

10.4 Spring 集成 MyBatis

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
@Configuration
@MapperScan("com.example.mapper") // 扫描 Mapper 接口
public class MyBatisConfig {

@Bean
public SqlSessionFactory sqlSessionFactory(
DataSource dataSource) throws Exception {

SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(dataSource);

// 配置 mapper 位置
factoryBean.setMapperLocations(
new PathMatchingResourcePatternResolver()
.getResources("classpath:/mapper/**/*.xml")
);

// 配置类型别名
factoryBean.setTypeAliasesPackage("com.example.entity");

// 配置驼峰映射
SqlSessionFactory factory = factoryBean.getObject();
org.apache.ibatis.session.Configuration config =
factory.getConfiguration();
config.setMapUnderscoreToCamelCase(true);

return factory;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Service
@Transactional // Spring 声明式事务
public class UserServiceImpl implements UserService {

@Autowired
private UserMapper userMapper;

@Override
public void createUser(User user) {
userMapper.insert(user);
// 其他数据库操作...
// 如果发生异常,事务自动回滚
}
}

十一、总结

11.1 核心知识点回顾

11.2 学习路线建议

11.3 下一步推荐学习

  • 📖 MyBatis 源码解析:深入理解核心原理
  • 📖 MyBatis-Plus 源码:学习增强工具的设计思路
  • 📖 Spring Data JPA:对比学习 Hibernate 风格的 ORM
  • 📖 泛型 CRUD 封装:设计通用的 BaseMapper
  • 📖 分布式数据库:学习分库分表、读写分离方案

💡 写给读者的话:MyBatis 是 Java 后端开发中承上启下的关键框架——向下封装了 JDBC,向上支撑了 Spring Data 等更高级的 ORM。掌握 MyBatis 的原理和使用,能让你在学习和工作中游刃有余。纸上得来终觉浅,绝知此事要躬行,快动手实践吧!🚀


📅 本文首次发布于 2026 年 5 月 24 日