MyBatis 概述

什么是 MyBatis

MyBatis 是一款优秀的持久层框架它支持自定义 SQL存储过程以及高级映射MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作

  • 核心功能
    • SQL 映射将 Java 对象与数据库记录进行映射
    • 动态 SQL根据条件动态生成 SQL 语句
    • 缓存机制一级缓存和二级缓存
    • 插件扩展支持拦截器扩展功能
  • 主要优势
    • 简单易学上手简单学习成本低
    • 灵活性强支持原生 SQL灵活性高
    • 解耦合SQL 与代码分离便于维护
    • 性能优秀接近原生 JDBC 的性能

MyBatis 的核心概念

核心组件

  • SqlSessionFactory
    • 创建 SqlSession 的工厂
    • 通常整个应用只需要一个实例
  • SqlSession
    • 执行 SQL 命令的对象
    • 非线程安全每次请求应创建新的实例
  • Mapper 接口
    • 定义数据访问方法的接口
    • MyBatis 自动生成实现类
  • Executor
    • SQL 执行器负责执行 SQL 语句
    • 有 SimpleReuseBatch 三种类型

配置文件

  • mybatis-config.xml全局配置文件
  • Mapper XML 文件SQL 映射文件
  • Mapper 接口Java 接口定义

MyBatis 的工作原理

执行流程

1
2
3
4
5
6
1. 读取配置文件 → 创建 SqlSessionFactory
2. 通过 SqlSessionFactory 创建 SqlSession
3. 通过 SqlSession 获取 Mapper 代理对象
4. 调用 Mapper 方法执行 SQL
5. Executor 执行 SQL 并返回结果
6. 关闭 SqlSession

SQL 执行过程

1
2
3
4
5
6
7
解析 SQL → 参数映射 → 执行 SQL → 结果映射 → 返回结果

关键步骤
- 解析解析 SQL 语句和动态标签
- 参数映射将 Java 对象参数映射到 SQL 占位符
- 执行通过 JDBC 执行 SQL
- 结果映射将 ResultSet 映射为 Java 对象

环境搭建

添加依赖

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
<!-- MyBatis 核心依赖 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.13</version>
</dependency>

<!-- MySQL 驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>

<!-- 日志框架可选 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.9</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.4.11</version>
</dependency>

Gradle 依赖

1
2
implementation 'org.mybatis:mybatis:3.5.13'
implementation 'mysql:mysql-connector-java:8.0.33'

创建数据库表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
-- 创建用户表
CREATE TABLE `user` (
`id` INT PRIMARY KEY AUTO_INCREMENT,
`username` VARCHAR(50) NOT NULL,
`password` VARCHAR(100) NOT NULL,
`email` VARCHAR(100),
`age` INT,
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 插入测试数据
INSERT INTO `user` (`username`, `password`, `email`, `age`) VALUES
('张三', '123456', 'zhangsan@example.com', 25),
('李四', '123456', 'lisi@example.com', 30),
('王五', '123456', 'wangwu@example.com', 28);

创建实体类

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

import java.time.LocalDateTime;

public class User {
private Integer id;
private String username;
private String password;
private String email;
private Integer age;
private LocalDateTime createTime;
private LocalDateTime updateTime;

// 构造方法
public User() {}

public User(String username, String password, String email, Integer age) {
this.username = username;
this.password = password;
this.email = email;
this.age = age;
}

// Getter 和 Setter
public Integer getId() { return id; }
public void setId(Integer id) { this.id = id; }

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

public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }

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

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

public LocalDateTime getCreateTime() { return createTime; }
public void setCreateTime(LocalDateTime createTime) { this.createTime = createTime; }

public LocalDateTime getUpdateTime() { return updateTime; }
public void setUpdateTime(LocalDateTime updateTime) { this.updateTime = updateTime; }

@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
", email='" + email + '\'' +
", age=" + age +
'}';
}
}

配置 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
<!-- mybatis-config.xml -->
<?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="db.properties"/>

<!-- 设置 -->
<settings>
<!-- 开启驼峰命名自动映射 -->
<setting name="mapUnderscoreToCamelCase" value="true"/>
<!-- 开启延迟加载 -->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- 开启二级缓存 -->
<setting name="cacheEnabled" value="true"/>
<!-- 日志实现 -->
<setting name="logImpl" value="SLF4J"/>
</settings>

<!-- 类型别名 -->
<typeAliases>
<package name="com.example.entity"/>
</typeAliases>

<!-- 环境配置 -->
<environments default="development">
<environment id="development">
<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 映射 -->
<mappers>
<mapper resource="mapper/UserMapper.xml"/>
<!-- 或者使用包扫描 -->
<!-- <package name="com.example.mapper"/> -->
</mappers>
</configuration>

数据库配置文件

1
2
3
4
5
# db.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=123456

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

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

import java.util.List;

public interface UserMapper {

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

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

// 根据用户名查询
User findByUsername(String username);

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

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

// 删除用户
int delete(Integer id);

// 条件查询
List<User> findByCondition(@Param("username") String username,
@Param("age") Integer age);
}

创建 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
<!-- mapper/UserMapper.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.example.mapper.UserMapper">

<!-- 结果映射 -->
<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="age" column="age"/>
<result property="createTime" column="create_time"/>
<result property="updateTime" column="update_time"/>
</resultMap>

<!-- 查询所有用户 -->
<select id="findAll" resultMap="UserResultMap">
SELECT * FROM user
</select>

<!-- 根据 ID 查询 -->
<select id="findById" resultMap="UserResultMap">
SELECT * FROM user WHERE id = #{id}
</select>

<!-- 根据用户名查询 -->
<select id="findByUsername" resultMap="UserResultMap">
SELECT * FROM user WHERE username = #{username}
</select>

<!-- 插入用户 -->
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
INSERT INTO user (username, password, email, age)
VALUES (#{username}, #{password}, #{email}, #{age})
</insert>

<!-- 更新用户 -->
<update id="update">
UPDATE user
SET username = #{username},
password = #{password},
email = #{email},
age = #{age}
WHERE id = #{id}
</update>

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

<!-- 条件查询 -->
<select id="findByCondition" resultMap="UserResultMap">
SELECT * FROM user
<where>
<if test="username != null and username != ''">
AND username LIKE CONCAT('%', #{username}, '%')
</if>
<if test="age != null">
AND age = #{age}
</if>
</where>
</select>
</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
package com.example.test;

import com.example.entity.User;
import com.example.mapper.UserMapper;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

import java.io.InputStream;
import java.util.List;

public class MyBatisTest {

public static void main(String[] args) throws Exception {
// 1. 读取配置文件
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");

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

// 3. 创建 SqlSession
SqlSession sqlSession = sqlSessionFactory.openSession();

try {
// 4. 获取 Mapper 代理
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);

// 5. 执行查询
List<User> users = userMapper.findAll();
users.forEach(System.out::println);

// 6. 提交事务增删改需要
sqlSession.commit();
} finally {
// 7. 关闭 SqlSession
sqlSession.close();
}
}
}

CRUD 操作

查询操作

基本查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- 简单查询 -->
<select id="findAll" resultType="User">
SELECT * FROM user
</select>

<!-- 带参数查询 -->
<select id="findById" resultType="User">
SELECT * FROM user WHERE id = #{id}
</select>

<!-- 多参数查询 -->
<select id="findByUsernameAndAge" resultType="User">
SELECT * FROM user
WHERE username = #{username} AND age = #{age}
</select>

模糊查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!-- 方式一使用 CONCAT -->
<select id="searchByUsername" resultType="User">
SELECT * FROM user
WHERE username LIKE CONCAT('%', #{username}, '%')
</select>

<!-- 方式二使用 ${}注意 SQL 注入风险 -->
<select id="searchByUsername2" resultType="User">
SELECT * FROM user
WHERE username LIKE '%${username}%'
</select>

<!-- 方式三在 Java 代码中拼接 -->
<!-- Java: String keyword = "%" + username + "%"; -->
<select id="searchByUsername3" resultType="User">
SELECT * FROM user
WHERE username LIKE #{keyword}
</select>

分页查询

1
2
3
4
5
6
7
8
9
10
11
<!-- 物理分页推荐 -->
<select id="findPage" resultType="User">
SELECT * FROM user
LIMIT #{offset}, #{pageSize}
</select>

<!-- 使用 PageHelper 插件逻辑分页 -->
<!-- Java 代码 -->
PageHelper.startPage(pageNum, pageSize);
List<User> users = userMapper.findAll();
PageInfo<User> pageInfo = new PageInfo<>(users);

插入操作

基本插入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!-- 简单插入 -->
<insert id="insert">
INSERT INTO user (username, password, email, age)
VALUES (#{username}, #{password}, #{email}, #{age})
</insert>

<!-- 插入并返回主键 -->
<insert id="insertWithKey" useGeneratedKeys="true" keyProperty="id">
INSERT INTO user (username, password, email, age)
VALUES (#{username}, #{password}, #{email}, #{age})
</insert>

<!-- 使用 selectKey 获取主键 -->
<insert id="insertWithSelectKey">
<selectKey keyProperty="id" resultType="int" order="AFTER">
SELECT LAST_INSERT_ID()
</selectKey>
INSERT INTO user (username, password, email, age)
VALUES (#{username}, #{password}, #{email}, #{age})
</insert>

批量插入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- 方式一foreach 批量插入 -->
<insert id="batchInsert">
INSERT INTO user (username, password, email, age)
VALUES
<foreach collection="users" item="user" separator=",">
(#{user.username}, #{user.password}, #{user.email}, #{user.age})
</foreach>
</insert>

<!-- 方式二使用 Batch 模式 -->
<!-- Java 代码 -->
SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
for (User user : users) {
mapper.insert(user);
}
sqlSession.commit();

更新操作

基本更新

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
<!-- 全量更新 -->
<update id="update">
UPDATE user
SET username = #{username},
password = #{password},
email = #{email},
age = #{age}
WHERE id = #{id}
</update>

<!-- 选择性更新只更新非空字段 -->
<update id="updateSelective">
UPDATE user
<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="age != null">
age = #{age},
</if>
</set>
WHERE id = #{id}
</update>

批量更新

1
2
3
4
5
6
7
8
9
<!-- 批量更新 -->
<update id="batchUpdate">
<foreach collection="users" item="user" separator=";">
UPDATE user
SET username = #{user.username},
email = #{user.email}
WHERE id = #{user.id}
</foreach>
</update>

删除操作

基本删除

1
2
3
4
5
6
7
8
9
10
11
12
<!-- 单条删除 -->
<delete id="delete">
DELETE FROM user WHERE id = #{id}
</delete>

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

动态 SQL

if 标签

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- 条件判断 -->
<select id="findByCondition" resultType="User">
SELECT * FROM user
<where>
<if test="username != null and username != ''">
AND username LIKE CONCAT('%', #{username}, '%')
</if>
<if test="age != null">
AND age = #{age}
</if>
<if test="email != null and email != ''">
AND email = #{email}
</if>
</where>
</select>

choose/when/otherwise 标签

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- 多选一 -->
<select id="search" resultType="User">
SELECT * FROM user
<where>
<choose>
<when test="username != null and username != ''">
AND username = #{username}
</when>
<when test="email != null and email != ''">
AND email = #{email}
</when>
<otherwise>
AND age > 18
</otherwise>
</choose>
</where>
</select>

where 标签

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- 智能处理 AND/OR -->
<select id="findByCondition" resultType="User">
SELECT * FROM user
<where>
<if test="username != null">
AND username = #{username}
</if>
<if test="age != null">
AND age = #{age}
</if>
</where>
</select>

<!-- where 标签会自动
1. 去掉第一个 AND 或 OR
2. 如果没有任何条件不生成 WHERE 关键字
-->

set 标签

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!-- 智能处理逗号 -->
<update id="updateSelective">
UPDATE user
<set>
<if test="username != null">
username = #{username},
</if>
<if test="email != null">
email = #{email},
</if>
<if test="age != null">
age = #{age},
</if>
</set>
WHERE id = #{id}
</update>

<!-- set 标签会自动
1. 去掉最后一个逗号
2. 如果没有任何字段不生成 SET 关键字
-->

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
<!-- 遍历集合 -->
<select id="findByIds" resultType="User">
SELECT * FROM user WHERE id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</select>

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

<!-- foreach 属性
- collection: 要遍历的集合
- item: 当前元素的变量名
- index: 当前元素的索引
- open: 开始符号
- close: 结束符号
- separator: 分隔符
-->

trim 标签

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!-- 自定义前后缀处理 -->
<select id="findByCondition" resultType="User">
SELECT * FROM user
<trim prefix="WHERE" prefixOverrides="AND |OR ">
<if test="username != null">
AND username = #{username}
</if>
<if test="age != null">
AND age = #{age}
</if>
</trim>
</select>

<!-- trim 属性
- prefix: 添加前缀
- suffix: 添加后缀
- prefixOverrides: 去除的前缀
- suffixOverrides: 去除的后缀
-->

bind 标签

1
2
3
4
5
<!-- 创建变量 -->
<select id="searchByUsername" resultType="User">
<bind name="pattern" value="'%' + username + '%'"/>
SELECT * FROM user WHERE username LIKE #{pattern}
</select>

关联映射

一对一映射

数据库设计

1
2
3
4
5
6
7
8
9
10
11
12
13
-- 用户表
CREATE TABLE `user` (
`id` INT PRIMARY KEY AUTO_INCREMENT,
`username` VARCHAR(50)
);

-- 身份证表
CREATE TABLE `id_card` (
`id` INT PRIMARY KEY AUTO_INCREMENT,
`card_no` VARCHAR(20),
`user_id` INT UNIQUE,
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`)
);

实体类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class User {
private Integer id;
private String username;
private IdCard idCard; // 一对一关系

// Getter 和 Setter
}

public class IdCard {
private Integer id;
private String cardNo;
private Integer userId;

// Getter 和 Setter
}

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
<!-- 方式一嵌套查询N+1 问题 -->
<resultMap id="UserWithIdCard" type="User">
<id property="id" column="id"/>
<result property="username" column="username"/>
<association property="idCard" column="id"
select="com.example.mapper.IdCardMapper.findById"/>
</resultMap>

<select id="findUserWithIdCard" resultMap="UserWithIdCard">
SELECT * FROM user WHERE id = #{id}
</select>

<!-- 方式二嵌套结果推荐 -->
<resultMap id="UserWithIdCard2" type="User">
<id property="id" column="user_id"/>
<result property="username" column="username"/>
<association property="idCard" javaType="IdCard">
<id property="id" column="card_id"/>
<result property="cardNo" column="card_no"/>
<result property="userId" column="user_id"/>
</association>
</resultMap>

<select id="findUserWithIdCard2" resultMap="UserWithIdCard2">
SELECT u.id as user_id, u.username,
c.id as card_id, c.card_no, c.user_id
FROM user u
LEFT JOIN id_card c ON u.id = c.user_id
WHERE u.id = #{id}
</select>

一对多映射

数据库设计

1
2
3
4
5
6
7
8
9
10
11
12
13
-- 用户表
CREATE TABLE `user` (
`id` INT PRIMARY KEY AUTO_INCREMENT,
`username` VARCHAR(50)
);

-- 订单表
CREATE TABLE `order` (
`id` INT PRIMARY KEY AUTO_INCREMENT,
`order_no` VARCHAR(50),
`user_id` INT,
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`)
);

实体类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class User {
private Integer id;
private String username;
private List<Order> orders; // 一对多关系

// Getter 和 Setter
}

public class Order {
private Integer id;
private String orderNo;
private Integer userId;

// Getter 和 Setter
}

Mapper 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!-- 嵌套结果方式 -->
<resultMap id="UserWithOrders" type="User">
<id property="id" column="user_id"/>
<result property="username" column="username"/>
<collection property="orders" ofType="Order">
<id property="id" column="order_id"/>
<result property="orderNo" column="order_no"/>
<result property="userId" column="user_id"/>
</collection>
</resultMap>

<select id="findUserWithOrders" resultMap="UserWithOrders">
SELECT u.id as user_id, u.username,
o.id as order_id, o.order_no, o.user_id
FROM user u
LEFT JOIN `order` o ON u.id = o.user_id
WHERE u.id = #{id}
</select>

多对多映射

数据库设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
-- 学生表
CREATE TABLE `student` (
`id` INT PRIMARY KEY AUTO_INCREMENT,
`name` VARCHAR(50)
);

-- 课程表
CREATE TABLE `course` (
`id` INT PRIMARY KEY AUTO_INCREMENT,
`name` VARCHAR(50)
);

-- 中间表
CREATE TABLE `student_course` (
`student_id` INT,
`course_id` INT,
PRIMARY KEY (`student_id`, `course_id`),
FOREIGN KEY (`student_id`) REFERENCES `student`(`id`),
FOREIGN KEY (`course_id`) REFERENCES `course`(`id`)
);

实体类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Student {
private Integer id;
private String name;
private List<Course> courses; // 多对多关系

// Getter 和 Setter
}

public class Course {
private Integer id;
private String name;
private List<Student> students; // 多对多关系

// Getter 和 Setter
}

Mapper 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<resultMap id="StudentWithCourses" type="Student">
<id property="id" column="student_id"/>
<result property="name" column="student_name"/>
<collection property="courses" ofType="Course">
<id property="id" column="course_id"/>
<result property="name" column="course_name"/>
</collection>
</resultMap>

<select id="findStudentWithCourses" resultMap="StudentWithCourses">
SELECT s.id as student_id, s.name as student_name,
c.id as course_id, c.name as course_name
FROM student s
LEFT JOIN student_course sc ON s.id = sc.student_id
LEFT JOIN course c ON sc.course_id = c.id
WHERE s.id = #{id}
</select>

缓存机制

一级缓存

  • 作用域SqlSession 级别
  • 默认开启无需配置
  • 失效条件
    • 执行增删改操作
    • 手动清空缓存
    • SqlSession 关闭
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 一级缓存示例
SqlSession session = sqlSessionFactory.openSession();
UserMapper mapper = session.getMapper(UserMapper.class);

// 第一次查询从数据库获取
User user1 = mapper.findById(1);

// 第二次查询从一级缓存获取
User user2 = mapper.findById(1); // 不会执行 SQL

// 执行更新清空一级缓存
mapper.update(user);

// 第三次查询重新从数据库获取
User user3 = mapper.findById(1); // 会执行 SQL

session.close();

二级缓存

  • 作用域Mapper/Namespace 级别
  • 需要配置手动开启
  • 共享性多个 SqlSession 共享

开启二级缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- mybatis-config.xml -->
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>

<!-- Mapper XML -->
<mapper namespace="com.example.mapper.UserMapper">
<!-- 开启二级缓存 -->
<cache/>

<!-- 或者自定义配置 -->
<cache eviction="LRU"
flushInterval="60000"
size="512"
readOnly="true"/>
</mapper>

缓存配置说明

属性 说明 默认值
eviction 回收策略 LRU
flushInterval 刷新间隔毫秒 不刷新
size 缓存对象数量 1024
readOnly 是否只读 false
1
2
3
4
5
回收策略
- LRU最近最少使用默认
- FIFO先进先出
- SOFT软引用
- WEAK弱引用

使用二级缓存的注意事项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- 实体类必须实现 Serializable -->
public class User implements Serializable {
private static final long serialVersionUID = 1L;
// ...
}

<!-- 禁用某个语句的二级缓存 -->
<select id="findById" resultType="User" useCache="false">
SELECT * FROM user WHERE id = #{id}
</select>

<!-- 强制刷新二级缓存 -->
<insert id="insert" flushCache="true">
INSERT INTO user ...
</insert>

第三方缓存集成

EhCache 集成

1
2
3
4
5
6
7
8
9
<!-- 添加依赖 -->
<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-ehcache</artifactId>
<version>1.2.2</version>
</dependency>

<!-- 配置 EhCache -->
<cache type="org.mybatis.caches.ehcache.EhcacheCache"/>

Redis 集成

1
2
3
4
5
6
7
8
9
<!-- 添加依赖 -->
<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-redis</artifactId>
<version>1.0.0-beta2</version>
</dependency>

<!-- 配置 Redis -->
<cache type="org.mybatis.caches.redis.RedisCache"/>

插件扩展

分页插件 PageHelper

添加依赖

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

配置插件

1
2
3
4
5
6
7
8
9
10
11
<!-- mybatis-config.xml -->
<plugins>
<plugin interceptor="com.github.pagehelper.PageInterceptor">
<!-- 使用 MySQL 方言 -->
<property name="helperDialect" value="mysql"/>
<!-- 合理化分页 -->
<property name="reasonable" value="true"/>
<!-- 支持通过 Mapper 接口参数传递分页 -->
<property name="supportMethodsArguments" value="true"/>
</plugin>
</plugins>

使用分页

1
2
3
4
5
6
7
8
9
10
11
12
13
// 方式一使用 PageHelper
PageHelper.startPage(1, 10);
List<User> users = userMapper.findAll();
PageInfo<User> pageInfo = new PageInfo<>(users);

System.out.println("总记录数" + pageInfo.getTotal());
System.out.println("总页数" + pageInfo.getPages());
System.out.println("当前页" + pageInfo.getPageNum());
System.out.println("每页大小" + pageInfo.getPageSize());

// 方式二使用 RowBounds不推荐
RowBounds rowBounds = new RowBounds(0, 10);
List<User> users = userMapper.findAll(rowBounds);

通用 Mapper

添加依赖

1
2
3
4
5
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper-spring-boot-starter</artifactId>
<version>4.2.3</version>
</dependency>

使用通用 Mapper

1
2
3
4
5
6
7
8
9
10
11
// 继承通用 Mapper
public interface UserMapper extends Mapper<User> {
// 自动拥有 CRUD 方法
}

// 使用
List<User> users = userMapper.selectAll();
User user = userMapper.selectByPrimaryKey(1);
userMapper.insert(user);
userMapper.updateByPrimaryKey(user);
userMapper.deleteByPrimaryKey(1);

Spring 整合

Spring Boot 整合

添加依赖

1
2
3
4
5
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.2</version>
</dependency>

配置文件

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

mybatis:
# 配置文件位置
config-location: classpath:mybatis-config.xml
# Mapper XML 位置
mapper-locations: classpath:mapper/*.xml
# 类型别名包
type-aliases-package: com.example.entity
configuration:
# 开启驼峰命名
map-underscore-to-camel-case: true
# 日志实现
log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl

Mapper 接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Mapper
public interface UserMapper {
List<User> findAll();
User findById(Integer id);
int insert(User user);
int update(User user);
int delete(Integer id);
}

// 或者在启动类添加 @MapperScan
@SpringBootApplication
@MapperScan("com.example.mapper")
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

Service 层

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
@Service
public class UserService {

@Autowired
private UserMapper userMapper;

public List<User> findAll() {
return userMapper.findAll();
}

public User findById(Integer id) {
return userMapper.findById(id);
}

@Transactional
public int save(User user) {
return userMapper.insert(user);
}

@Transactional
public int update(User user) {
return userMapper.update(user);
}

@Transactional
public int delete(Integer id) {
return userMapper.delete(id);
}
}

Controller 层

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
@RestController
@RequestMapping("/users")
public class UserController {

@Autowired
private UserService userService;

@GetMapping
public List<User> list() {
return userService.findAll();
}

@GetMapping("/{id}")
public User get(@PathVariable Integer id) {
return userService.findById(id);
}

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

@PutMapping
public int update(@RequestBody User user) {
return userService.update(user);
}

@DeleteMapping("/{id}")
public int delete(@PathVariable Integer id) {
return userService.delete(id);
}
}

最佳实践

命名规范

1
2
3
4
5
6
7
8
9
10
11
12
13
Mapper 接口命名
- 接口名XxxMapper
- 方法名selectXxxinsertXxxupdateXxxdeleteXxx

XML 文件命名
- 文件名XxxMapper.xml
- namespace与 Mapper 接口全限定名一致

SQL ID 命名
- 查询selectXxxfindXxxgetXxx
- 插入insertXxxaddXxx
- 更新updateXxxmodifyXxx
- 删除deleteXxxremoveXxx

性能优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!-- 1. 避免使用 SELECT * -->
<select id="findAll" resultType="User">
SELECT id, username, email, age FROM user
</select>

<!-- 2. 合理使用索引 -->
<!-- 确保查询字段有索引 -->

<!-- 3. 避免 N+1 查询问题 -->
<!-- 使用嵌套结果而非嵌套查询 -->

<!-- 4. 合理使用缓存 -->
<cache eviction="LRU" size="512"/>

<!-- 5. 批量操作 -->
<insert id="batchInsert">
INSERT INTO user VALUES
<foreach collection="list" item="item" separator=",">
(#{item.username}, #{item.password})
</foreach>
</insert>

安全建议

1
2
3
4
5
6
7
8
9
10
11
12
<!-- 1. 防止 SQL 注入使用 #{} 而非 ${} -->
<!-- 正确 -->
SELECT * FROM user WHERE username = #{username}

<!-- 错误有 SQL 注入风险 -->
SELECT * FROM user WHERE username = '${username}'

<!-- 2. 敏感信息加密 -->
<!-- 密码等敏感字段应加密存储 -->

<!-- 3. 权限控制 -->
<!-- 在 Service 层进行权限校验 -->

代码规范

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
// 1. 及时关闭 SqlSession
try (SqlSession session = sqlSessionFactory.openSession()) {
// 执行操作
}

// 2. 事务管理
@Transactional
public void updateUser(User user) {
userMapper.update(user);
}

// 3. 异常处理
try {
userMapper.insert(user);
} catch (Exception e) {
log.error("插入用户失败", e);
throw new BusinessException("插入用户失败");
}

// 4. 参数校验
public User findById(Integer id) {
if (id == null || id <= 0) {
throw new IllegalArgumentException("ID 不能为空");
}
return userMapper.findById(id);
}

常见问题

字段映射问题

1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- 问题数据库字段与 Java 属性名不一致 -->
<!-- 解决方案一使用 resultMap -->
<resultMap id="UserResultMap" type="User">
<result property="userName" column="user_name"/>
</resultMap>

<!-- 解决方案二开启驼峰命名 -->
<setting name="mapUnderscoreToCamelCase" value="true"/>

<!-- 解决方案三使用 AS 别名 -->
<select id="findAll" resultType="User">
SELECT user_name AS userName FROM user
</select>

中文乱码问题

1
2
3
4
5
# 数据库 URL 添加编码参数
jdbc.url=jdbc:mysql://localhost:3306/db?characterEncoding=utf8

# MyBatis 配置文件指定编码
<?xml version="1.0" encoding="UTF-8" ?>

日期时间处理

1
2
3
4
5
6
7
8
9
10
<!-- 使用 Java 8 时间 API -->
<resultMap id="UserResultMap" type="User">
<result property="createTime" column="create_time"
javaType="java.time.LocalDateTime"
jdbcType="TIMESTAMP"/>
</resultMap>

<!-- 或者使用 TypeHandler -->
<result property="createTime" column="create_time"
typeHandler="org.apache.ibatis.type.LocalDateTimeTypeHandler"/>

报错处理

💗💗 MyBatis 报错BindingException

1
2
3
4
5
6
7
8
9
10
11
12
13
错误信息
Invalid bound statement (not found): com.example.mapper.UserMapper.findById

错误原因
1. Mapper XML 文件未找到
2. namespace 配置错误
3. SQL ID 不存在

解决方案
1. 检查 Mapper XML 文件是否在 classpath 下
2. 确认 namespace 与 Mapper 接口全限定名一致
3. 确认 XML 中的 SQL ID 与接口方法名一致
4. 检查 mybatis-config.xml 中是否配置了 Mapper

💗💗 MyBatis 报错PersistenceException

1
2
3
4
5
6
7
8
9
10
11
12
13
错误信息
Error querying database. Cause: java.sql.SQLException

错误原因
1. SQL 语法错误
2. 数据库连接失败
3. 参数类型不匹配

解决方案
1. 检查 SQL 语句是否正确
2. 检查数据库连接配置
3. 确认参数类型与数据库字段类型匹配
4. 查看详细错误信息定位问题

💗💗 MyBatis 报错TooManyResultsException

1
2
3
4
5
6
7
8
9
10
错误信息
Expected one result (or null) to be returned by selectOne(), but found: 2

错误原因
使用 selectOne 查询但返回多条记录

解决方案
1. 改用 selectList 方法
2. 添加 LIMIT 1 限制
3. 检查查询条件是否唯一

学习资源

  • 视频
    • MyBatis 零基础教程https://www.bilibili.com/video/BV1VP4y1c7j7
  • 官方文档
    • MyBatis 官方文档https://mybatis.org/mybatis-3/zh/index.html
    • MyBatis GitHubhttps://github.com/mybatis/mybatis-3
  • 书籍
    • MyBatis 从入门到精通刘增辉著
    • 深入浅出 MyBatis 技术原理与实战杨开振著
  • 教程
    • MyBatis 入门教程https://www.runoob.com/w3cnote/mybatis-tutorial.html
    • Baeldung MyBatis 教程https://www.baeldung.com/category/persistence/mybatis/
  • 工具
    • MyBatis Generator代码生成工具
    • MyBatis Plus增强工具
    • PageHelper分页插件
  • 社区
    • Stack Overflow MyBatis 标签https://stackoverflow.com/questions/tagged/mybatis
    • MyBatis 中文社区https://mybatis.org/