Appearance
MyBatis 框架
导航目录
1. 简介
1.1 MyBatis 概述
MyBatis 是一款优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射。MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。
发展历程:
- 最初是 Apache 的开源项目 iBatis
- 2010 年迁移到 Google Code 并更名为 MyBatis
- 2013 年代码迁移到 Github
- 官网:https://mybatis.org/mybatis-3/zh_CN/index.html
核心特点:
- 半自动 ORM 框架:研发效率较低,但程序执行效率较高
- 全自动 ORM 框架:如 Hibernate,研发效率高,但程序执行效率低
- ORM:对象关系映射,将对象属性与表中字段建立映射关系
1.2 持久化层框架对比
| 框架 | 优点 | 缺点 |
|---|---|---|
| JDBC | 性能最高 | SQL 耦合在代码中,维护困难 |
| Hibernate/JPA | 开发效率高,操作简便 | 复杂 SQL 需要绕过框架,性能较低 |
| MyBatis | 性能出色,SQL 与 Java 分离 | 开发效率稍逊于 Hibernate |
效率对比:
- 开发效率:Hibernate > MyBatis > JDBC
- 运行效率:JDBC > MyBatis > Hibernate
2. 快速入门
2.1 环境准备
数据库准备:
sql
CREATE DATABASE `db_mybatis`;
USE `db_mybatis`;
CREATE TABLE `t_emp`(
emp_id INT AUTO_INCREMENT,
emp_name CHAR(100),
emp_salary DOUBLE(10,5),
PRIMARY KEY(emp_id)
);
INSERT INTO `t_emp`(emp_name,emp_salary) VALUES("tom",200.33);
INSERT INTO `t_emp`(emp_name,emp_salary) VALUES("jerry",666.66);2.2 依赖配置
Spring Boot 版本:
xml
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>2.3 配置文件
application.properties:
properties
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/db_mybatis
spring.datasource.username=root
spring.datasource.password=root
# MyBatis配置
mybatis.mapper-locations=classpath:mappers/*.xml
mybatis.configuration.map-underscore-to-camel-case=true
mybatis.type-aliases-package=com.at.pojo
logging.level.com.at.mapper=debug2.4 Mapper 接口
java
@Mapper
public interface EmployeeMapper {
List<Employee> selectAllEmps();
}2.5 映射文件
EmployeeMapper.xml:
xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.at.mapper.EmployeeMapper">
<select id="selectAllEmps" resultType="employee">
SELECT emp_id, emp_name, emp_salary FROM t_emp
</select>
</mapper>2.6 测试代码
java
@SpringBootTest
class MyBatisTest {
@Autowired
private EmployeeMapper employeeMapper;
@Test
void testSelectAll() {
List<Employee> employees = employeeMapper.selectAllEmps();
employees.forEach(System.out::println);
}
}3. 数据输入
3.1 参数传递方式
3.1.1 单个普通参数
java
Employee selectById(int empId);xml
<select id="selectById" resultType="employee">
SELECT * FROM t_emp WHERE emp_id = #{empId}
</select>3.1.2 多个普通参数
java
Employee selectByCondition(int empId, String empName);xml
<select id="selectByCondition" resultType="employee">
SELECT * FROM t_emp
WHERE emp_id = #{param1} AND emp_name = #{param2}
</select>3.1.3 命名参数
java
Employee selectByNamedParam(@Param("id") int empId, @Param("name") String empName);xml
<select id="selectByNamedParam" resultType="employee">
SELECT * FROM t_emp
WHERE emp_id = #{id} AND emp_name = #{name}
</select>3.1.4 POJO 参数
java
void insertEmployee(Employee employee);xml
<insert id="insertEmployee">
INSERT INTO t_emp(emp_name, emp_salary)
VALUES(#{empName}, #{empSalary})
</insert>3.1.5 Map 参数
java
List<Employee> selectByMap(Map<String, Object> map);xml
<select id="selectByMap" resultType="employee">
SELECT * FROM t_emp
WHERE emp_id = #{id} AND emp_name = #{name}
</select>3.2 #{}与${}的区别
| 特性 | #{} | ${} |
|---|---|---|
| 底层实现 | PreparedStatement(?占位符) | Statement(字符串拼接) |
| 安全性 | 防止 SQL 注入,安全 | 存在 SQL 注入风险,不安全 |
| 使用场景 | 参数值 | 表名、列名等非参数位置 |
4. 数据输出
4.1 返回类型处理
4.1.1 返回单个字面量
java
Integer selectEmpCount();xml
<select id="selectEmpCount" resultType="int">
SELECT COUNT(*) FROM t_emp
</select>4.1.2 返回单个 POJO
java
Employee selectById(int empId);xml
<select id="selectById" resultType="employee">
SELECT * FROM t_emp WHERE emp_id = #{empId}
</select>4.1.3 返回 List 类型
java
List<Employee> selectAll();xml
<select id="selectAll" resultType="employee">
SELECT * FROM t_emp
</select>4.1.4 返回 Map 类型
java
@MapKey("empId")
Map<Integer, Employee> selectAllReturnMap();xml
<select id="selectAllReturnMap" resultType="employee">
SELECT * FROM t_emp
</select>4.2 主键返回
4.2.1 自增主键
xml
<insert id="insertEmployee" useGeneratedKeys="true" keyProperty="empId">
INSERT INTO t_emp(emp_name, emp_salary)
VALUES(#{empName}, #{empSalary})
</insert>4.2.2 非自增主键
xml
<insert id="insertUser" parameterType="User">
<selectKey keyProperty="id" resultType="java.lang.String" order="BEFORE">
SELECT UUID() as id
</selectKey>
INSERT INTO user (id, username, password)
VALUES (#{id}, #{username}, #{password})
</insert>5. 结果映射
5.1 resultType vs resultMap
| 特性 | resultType | resultMap |
|---|---|---|
| 复杂度 | 简单映射 | 复杂映射 |
| 适用场景 | 单表简单查询 | 多表复杂查询 |
| 配置 | 自动映射 | 手动配置映射关系 |
5.2 实体类关系映射
5.2.1 对一关系
java
// 员工对部门(一对一)
public class Employee {
private Integer empId;
private String empName;
private Department dept; // 对一关系
}5.2.2 对多关系(一对多)
java
// 部门对员工(一对多)
public class Department {
private Integer deptId;
private String deptName;
private List<Employee> employees; // 对多关系
}5.2.3 多对多关系
java
// 学生类
public class Student {
private Integer studentId;
private String studentName;
private List<Course> courses; // 多对多关系
}
// 课程类
public class Course {
private Integer courseId;
private String courseName;
private List<Student> students; // 多对多关系
}5.3 级联映射
xml
<resultMap id="empAndDeptResultMap" type="Employee">
<id column="emp_id" property="empId"/>
<result column="emp_name" property="empName"/>
<result column="dept_id" property="dept.deptId"/>
<result column="dept_name" property="dept.deptName"/>
</resultMap>5.4 association 映射
xml
<resultMap id="empAndDeptResultMap" type="Employee">
<id column="emp_id" property="empId"/>
<result column="emp_name" property="empName"/>
<association property="dept" javaType="Department">
<id column="dept_id" property="deptId"/>
<result column="dept_name" property="deptName"/>
</association>
</resultMap>5.5 collection 映射
5.5.1 一对多映射
xml
<resultMap id="deptAndEmpResultMap" type="Department">
<id column="dept_id" property="deptId"/>
<result column="dept_name" property="deptName"/>
<collection property="employees" ofType="Employee">
<id column="emp_id" property="empId"/>
<result column="emp_name" property="empName"/>
</collection>
</resultMap>
<select id="selectDeptWithEmps" resultMap="deptAndEmpResultMap">
SELECT d.dept_id, d.dept_name, e.emp_id, e.emp_name
FROM t_dept d
LEFT JOIN t_emp e ON d.dept_id = e.dept_id
WHERE d.dept_id = #{deptId}
</select>5.5.2 多对多映射
xml
<resultMap id="studentAndCoursesResultMap" type="Student">
<id column="student_id" property="studentId"/>
<result column="student_name" property="studentName"/>
<collection property="courses" ofType="Course">
<id column="course_id" property="courseId"/>
<result column="course_name" property="courseName"/>
</collection>
</resultMap>
<select id="selectStudentWithCourses" resultMap="studentAndCoursesResultMap">
SELECT s.student_id, s.student_name, c.course_id, c.course_name
FROM t_student s
LEFT JOIN t_student_course sc ON s.student_id = sc.student_id
LEFT JOIN t_course c ON sc.course_id = c.course_id
WHERE s.student_id = #{studentId}
</select>5.6 分步查询
5.6.1 对一分步查询
xml
<resultMap id="empAndDeptStepMap" type="Employee">
<id column="emp_id" property="empId"/>
<result column="emp_name" property="empName"/>
<association property="dept"
select="com.at.mapper.DeptMapper.selectById"
column="dept_id"/>
</resultMap>
<select id="selectEmpWithDept" resultMap="empAndDeptStepMap">
SELECT * FROM t_emp WHERE emp_id = #{empId}
</select>5.6.2 对多分步查询
xml
<resultMap id="deptAndEmpStepMap" type="Department">
<id column="dept_id" property="deptId"/>
<result column="dept_name" property="deptName"/>
<collection property="employees"
select="com.at.mapper.EmpMapper.selectByDeptId"
column="dept_id"/>
</resultMap>
<select id="selectDeptWithEmps" resultMap="deptAndEmpStepMap">
SELECT * FROM t_dept WHERE dept_id = #{deptId}
</select>5.7 延迟加载与 N+1 问题
延迟加载:在分步查询中,只有当真正需要访问关联对象时,才会执行第二条 SQL。通过以下配置开启:
properties
mybatis.configuration.lazy-loading-enabled=true
mybatis.configuration.aggressive-lazy-loading=falseN+1 问题:如果查询了 N 个主对象,并且每个主对象都需要触发一次分步查询来加载关联对象,那么总共会执行 1 + N 次 SQL,严重影响性能。解决方案是尽量使用多表 JOIN 查询(如级联、association、collection),一次性将所有数据查出。
6. 动态 SQL
6.1 常用动态 SQL 标签
6.1.1 if 标签
xml
<select id="selectByCondition" resultType="Employee">
SELECT * FROM t_emp
<where>
<if test="empName != null and empName != ''">
AND emp_name LIKE CONCAT('%', #{empName}, '%')
</if>
<if test="minSalary != null">
AND emp_salary >= #{minSalary}
</if>
</where>
</select>6.1.2 choose/when/otherwise
xml
<select id="selectByChoose" resultType="Employee">
SELECT * FROM t_emp
<where>
<choose>
<when test="empId != null">
emp_id = #{empId}
</when>
<when test="empName != null">
emp_name = #{empName}
</when>
<otherwise>
1=1
</otherwise>
</choose>
</where>
</select>6.1.3 trim 标签
xml
<insert id="insertSelective">
INSERT INTO t_emp
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="empName != null">emp_name,</if>
<if test="empSalary != null">emp_salary,</if>
</trim>
<trim prefix="VALUES (" suffix=")" suffixOverrides=",">
<if test="empName != null">#{empName},</if>
<if test="empSalary != null">#{empSalary},</if>
</trim>
</insert>6.1.4 set 标签
xml
<update id="updateSelective">
UPDATE t_emp
<set>
<if test="empName != null">emp_name = #{empName},</if>
<if test="empSalary != null">emp_salary = #{empSalary},</if>
</set>
WHERE emp_id = #{empId}
</update>6.1.5 foreach 标签
xml
<select id="selectByIds" resultType="Employee">
SELECT * FROM t_emp
WHERE emp_id IN
<foreach collection="ids" item="id" open="(" close=")" separator=",">
#{id}
</foreach>
</select>
<insert id="insertBatch">
INSERT INTO t_emp(emp_name, emp_salary) VALUES
<foreach collection="employees" item="emp" separator=",">
(#{emp.empName}, #{emp.empSalary})
</foreach>
</insert>6.1.6 bind 标签
xml
<select id="selectByLikeName" resultType="Employee">
<bind name="pattern" value="'%' + empName + '%'"/>
SELECT * FROM t_emp
WHERE emp_name LIKE #{pattern}
</select>6.1.7 sql/include 标签
xml
<sql id="baseColumn">
emp_id, emp_name, emp_salary, dept_id
</sql>
<select id="selectAll" resultType="Employee">
SELECT <include refid="baseColumn"/> FROM t_emp
</select>7. 缓存机制(面试高频)
MyBatis 内置了一套缓存机制,用于提升查询性能,减少对数据库的直接访问。
7.1 一级缓存(SqlSession 级别)
- 作用域:默认开启,作用于同一个
SqlSession内部。在同一个会话中,执行相同的 SQL 查询会直接从缓存中获取结果。 - 生命周期:
SqlSession关闭时,一级缓存即被清空。 - 失效场景:
SqlSession不同。- 执行了
commit()操作(会清空缓存)。 - 执行了任何增、删、改操作。
7.2 二级缓存(Mapper 级别)
- 作用域:跨
SqlSession,作用于同一个Mapper的namespace。 - 开启方式:
- 全局配置开启:
mybatis.configuration.cache-enabled=true - 在 Mapper XML 中添加
<cache/>标签。 - POJO 实体类必须实现
Serializable接口。
- 全局配置开启:
- 工作原理:当一个会话提交或关闭时,其一级缓存中的数据会被刷新到二级缓存中。
- 缓存穿透问题:如果查询一个数据库中不存在的数据,每次都会打到数据库。解决方案是在缓存中也存一个空值(如
null),并设置较短的过期时间。
8. 分页插件与逆向工程
8.1 PageHelper 分页插件
添加依赖:
xml
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.4.2</version>
</dependency>使用示例:
java
@Test
public void testPageHelper() {
// 开启分页
PageHelper.startPage(1, 10);
// 查询数据
List<Employee> employees = employeeMapper.selectAll();
// 封装分页信息
PageInfo<Employee> pageInfo = new PageInfo<>(employees);
System.out.println("当前页:" + pageInfo.getPageNum());
System.out.println("总页数:" + pageInfo.getPages());
System.out.println("总记录数:" + pageInfo.getTotal());
System.out.println("每页大小:" + pageInfo.getPageSize());
}8.2 MyBatisX 插件使用(逆向工程)
安装插件:
- 在 IDEA 中安装 MyBatisX 插件
- 配置数据库连接
生成代码:
- 在数据库表上右键
- 选择 MyBatisX-Generator
- 配置生成路径和选项
生成的文件:
- 实体类(POJO)
- Mapper 接口
- Mapper XML 文件
8.3 生成代码示例
实体类:
java
@Data
public class Employee {
private Integer empId;
private String empName;
private Double empSalary;
private Integer deptId;
}Mapper 接口:
java
public interface EmployeeMapper {
int deleteByPrimaryKey(Integer empId);
int insert(Employee record);
Employee selectByPrimaryKey(Integer empId);
int updateByPrimaryKey(Employee record);
}9. 最佳实践与原理剖析
9.1 配置优化
properties
# 开启自动映射
mybatis.configuration.auto-mapping-behavior=full
# 开启二级缓存
mybatis.configuration.cache-enabled=true
# 设置默认执行器
mybatis.configuration.default-executor-type=reuse9.2 代码规范
- 使用@Param 注解明确参数名称
- 复杂的查询使用 resultMap 而不是 resultType
- 批量操作使用 foreach 标签
- 敏感操作使用#{}防止 SQL 注入
9.3 性能优化
- 合理使用延迟加载
- 大数据量查询使用分页
- 频繁查询考虑使用缓存
- 关联查询使用分步查询减少 JOIN
9.4 自定义类型处理器(TypeHandler)
当 MyBatis 内置的类型处理器无法满足需求时(如 Java 的 LocalDateTime 与数据库的 timestamp 映射),可以自定义 TypeHandler。
java
@MappedTypes(LocalDateTime.class)
@MappedJdbcTypes(JdbcType.TIMESTAMP)
public class LocalDateTimeTypeHandler extends BaseTypeHandler<LocalDateTime> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, LocalDateTime parameter, JdbcType jdbcType) throws SQLException {
ps.setTimestamp(i, Timestamp.valueOf(parameter));
}
@Override
public LocalDateTime getNullableResult(ResultSet rs, String columnName) throws SQLException {
Timestamp timestamp = rs.getTimestamp(columnName);
return timestamp != null ? timestamp.toLocalDateTime() : null;
}
// ... 省略其他 getNullableResult 方法
}9.5 MyBatis 核心原理
- SqlSessionFactory:重量级对象,每个数据库环境对应一个。它是创建
SqlSession的工厂,通常在应用启动时构建。 - SqlSession:轻量级、非线程安全的对象,代表与数据库的一次会话。每个线程都应该有自己的
SqlSession实例,用完必须关闭。