Skip to content

MyBatis 框架

导航目录

1. 简介

1.1 MyBatis 概述

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

发展历程

核心特点

  • 半自动 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=debug

2.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

特性resultTyperesultMap
复杂度简单映射复杂映射
适用场景单表简单查询多表复杂查询
配置自动映射手动配置映射关系

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=false

N+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 关闭时,一级缓存即被清空。
  • 失效场景
    1. SqlSession 不同。
    2. 执行了 commit() 操作(会清空缓存)。
    3. 执行了任何增、删、改操作。

7.2 二级缓存(Mapper 级别)

  • 作用域:跨 SqlSession,作用于同一个 Mappernamespace
  • 开启方式
    1. 全局配置开启:mybatis.configuration.cache-enabled=true
    2. 在 Mapper XML 中添加 <cache/> 标签。
    3. 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 插件
  • 配置数据库连接

生成代码

  1. 在数据库表上右键
  2. 选择 MyBatisX-Generator
  3. 配置生成路径和选项

生成的文件

  • 实体类(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=reuse

9.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 实例,用完必须关闭。