Mybatis

14700 字
37 分钟

假如你一定要写注释,请确保它描述了离你最近的代码。

简介

官网:MyBatis 3 | 简介 – mybatis

MyBatis 是一款优秀的 持久层框架 ,用于简化 JDBC 开发

主要作用:

  • 封装了 JDBC 的繁琐操作(如连接管理、SQL 执行、结果映射)。
  • 通过 XML 或注解 配置 SQL 与 Java 对象的映射关系。

在分层架构中的位置:

Controller(控制层) → Service(业务层) → DAO(持久层) → 数据库
  • DAO(Data Access Object) :通过 MyBatis 与数据库交互。

Mybatis入门

快速入门

创建项目

新建一个SpringBoot项目用于Mybatis入门:

1756090140008

勾选Mybatis框架以及连接Mysql的驱动:

1756090162669

创建成功后项目中有这两个依赖:

<!-- mybatis的起步依赖 -->
<dependency>
	<groupId>org.mybatis.spring.boot</groupId>
	<artifactId>mybatis-spring-boot-starter</artifactId>
	<version>3.0.5</version>
</dependency>
  
<!-- mysql数据库的连接驱动-->
<dependency>
	<groupId>com.mysql</groupId>
	<artifactId>mysql-connector-j</artifactId>
	<scope>runtime</scope>
</dependency>

创建数据模型

假设数据库中有如下的用户表。

1756090765820

我们创建对应的用户类:Model/User.java

public class User {
    private Integer id;
    private String name;
    private Short age;
    private Short gender;
    private String phone;

    public User() {
    }

    public User(Integer id, String name, Short age, Short gender, String phone) {
        this.id = id;
        this.name = name;
        this.age = age;
        this.gender = gender;
        this.phone = phone;
    }

    // 省略的getter与setter
}

配置Mybatis

配置Mybatis四要素:

要素配置键作用示例值
驱动类名spring.datasource.driver-class-name指定 JDBC 驱动类,用于连接数据库com.mysql.cj.jdbc.Driver
数据库 URLspring.datasource.url定义数据库类型、地址、端口、库名及连接参数jdbc:mysql://localhost:3306/mybatis
用户名spring.datasource.username数据库登录账号root
密码spring.datasource.password数据库登录密码1234

src/main/resources/application.properties

spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/mybatis
spring.datasource.username=root
spring.datasource.password=你的密码

编写SQL语句

Mapper/UserMapper.java

@Mapper
public interface UserMapper {
    @Select("SELECT * FROM USER")
    public List<User> selectAll();
}

声明一个接口并使用 @Mapper进行注解后,Mybatis会自动生成运行对象,并将对象交给IOC管理。调用对应的方法自动执行 @Select注解中的SQL语句,并将查询结构封装为对象返回。

测试程序

src/test/java/top/ulna520/mybatisquickstart/MybatisQuickStartApplicationTests.java

@SpringBootTest
class MybatisQuickStartApplicationTests {

    @Autowired
    private UserMapper userMapper;

	@Test
	void contextLoads() {
        List<User> users = userMapper.selectAll();
        users.forEach(System.out::println);
	}

}

运行结果:

User{id=1, name='Alice', age=25, gender=2, phone='13800000001'}
User{id=2, name='Bob', age=30, gender=1, phone='13800000002'}
User{id=3, name='Charlie', age=28, gender=0, phone='13800000003'}

JDBC

定义

  • JDBC :Java DataBase Connectivity
  • 使用 Java 语言 操作 关系型数据库 的一套 API 规范

本质

  • Sun 公司官方定义的一套操作所有关系型数据库的 统一接口规范
  • 各数据库厂商根据规范提供 驱动实现类 (打包成驱动 jar 包)。
  • 开发者调用 JDBC 接口编程,真正执行的代码是驱动 jar 包中的实现类。

1756093565266

数据库连接池

数据库连接池是一个 容器 ,用于分配管理数据库连接(Connection 对象)。

作用

  • 复用已有连接,避免频繁创建/销毁连接带来的性能开销。
  • 统一管理连接的生命周期,防止连接泄漏。

1756094531971

核心机制

  1. 初始化 :启动时创建一定数量的数据库连接,存放在连接池中。
  2. 获取连接 :应用程序向连接池请求可用连接,而不是直接新建连接。
  3. 归还连接 :使用完毕后将连接放回连接池,而不是关闭物理连接。
  4. 回收机制
    • 空闲时间超过最大空闲时间的连接进行释放。
    • 避免因未释放连接导致的 连接泄漏

常见连接池

实现特点
C3P0较早期,稳定性好,性能一般
DBCPApache 提供,配置简单
Druid阿里巴巴开源,功能强大,性能优秀,监控能力强
HikariCP高性能,轻量级,Spring Boot 默认连接池

更改数据库连接池:直接引入依赖

<dependency>
	<groupId>com.alibaba</groupId>
	<artifactId>druid</artifactId>
	<version>${druid-version}</version>
</dependency>

lombok

Lombok 是一个 Java 类库,通过注解自动生成常用方法(构造器、getter/setter、equals、hashCode、toString 等),并可自动生成日志变量。

作用:简化开发,减少样板代码,提高开发效率

注解功能
@Getter / @Setter所有属性生成 get / set 方法
@ToString自动生成易读的 toString() 方法
@EqualsAndHashCode根据类的非静态字段自动重写 equals()haMshCode() 方法
@Data综合注解,等价于 @Getter + @Setter + @ToString + @EqualsAndHashCode
@NoArgsConstructor生成无参构造方法
@AllArgsConstructor生成包含所有字段(除 static 修饰字段)的全参构造方法

依赖示例:

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>最新版本</version>
    <scope>provided</scope>
</dependency>

scope=provided 表示编译期可用,运行期不需要 Lombok.jar。

Mybatis-crud

首先我们准备一个Mapper注解接口:

@Mapper
public interface EmpMapper {

}

接下来我们写的增删改查都是其中的一个方法。

删除

SQL语句:

DELETE FROM emp WHERE id = 1;

接口方法:

@Delete("DELETE FROM emp WHERE id = #{id}")
public void delete(Integer id);

也可以是

@Delete("DELETE FROM emp WHERE id = #{id}")
public Integer delete(Integer id);

此时返回删除的数据条数。

打开Mypatis日志

application.properties

# 输出到控制台
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

Mybatis日志:

1756173793325

预编译SQL

普通 SQL vs 预编译 SQL:

特性普通 SQL(Statement)预编译 SQL(PreparedStatement)
SQL 发送次数每次执行都发送完整 SQL首次发送 SQL 模板,后续只传参数
编译次数每次执行都重新解析、优化、编译首次编译后缓存执行计划,后续复用
性能多次重复执行时性能较低多次重复执行时性能更高
安全性易受 SQL 注入攻击参数化传值,防止 SQL 注入
适用场景一次性执行的 SQL多次执行的相似 SQL
代码可维护性需要频繁拼接字符串SQL 模板与参数分离,结构清晰

1756174062701

SQL注入

采用普通SQL的语句:

SELECT COUNT(*) FROM user where username = '' AND password = '';

普通sql语句会直接将我们传入的用户名密码拼接到SQL语句中。

例如我们输入:ulna 123456

SELECT COUNT(*) FROM user where username = 'ulna' AND password = '123456';

但是如果我们输入:ulna 'OR '1'='1

SELECT COUNT(*) FROM user where username = 'ulna' AND password = '' OR '1' = '1';

此时由于 '1' = '1'条件恒成立,所以用户名和密码错误也可以登录成功。

参数占位符

写法处理方式是否预编译安全性典型用途
#{}在执行 SQL 前会被替换为 ? 占位符,由 JDBC 进行参数绑定✅ 是高(防 SQL 注入)传递参数值(数字、字符串、日期等)
${}在执行 SQL 前直接进行字符串拼接,将参数值原样拼入 SQL❌ 否低(易受 SQL 注入)动态拼接表名、列名、排序字段等

新增

sql语句:

INSERT INTO emp(username, name, gender, image, job, entrydate, dept_id, create_time, update_time)
VALUES('songyuanqiao', '宋元桥', '男', '1.jpg', '工程师', '2012-10-09', 2, '2022-10-01 00:00:00', '2022-10-01 00:00:00');

Mybatis接口方法示例:

@Insert("INSERT INTO emp(username, name, gender, image, job, entrydate, dept_id, create_time, update_time) " +
        "VALUES(#{username}, #{name}, #{gender}, #{image}, #{job}, #{entrydate}, #{deptId}, #{createTime}, #{updateTime})")
void insert(Emp emp);

Mybatis日志:

1756176230932

主键返回

在数据添加成功后,需要获取插入数据库数据的主键

@Options(keyProperty = "id", useGeneratedKeys = true)
@Insert("INSERT INTO emp(username, name, gender, image, job, entrydate, dept_id, create_time, update_time) " +
        "VALUES(#{username}, #{name}, #{gender}, #{image}, #{job}, #{entrydate}, #{deptId}, #{createTime}, #{updateTime})")
void insert(Emp emp);

@Options

  • useGeneratedKeys = true → 启用 JDBC 的自动生成主键功能
  • keyProperty = "id" → 指定实体类中接收主键的属性名

添加这个注解后,就会将自动生成的主键值封装到传入对象的 id属性中

更新

sql语句:

UPDATE emp
SET username = 'songdaxia',
    name = '宋大侠',
    gender = 1,
    image = '1.jpg',
    job = 2,
    entrydate = '2012-01-01',
    dept_id = 2,
    update_time = '2022-10-11 12:12:12'
WHERE id = 19;

接口方法:

@Update("UPDATE emp SET username=#{username}, name=#{name}, gender=#{gender}, image=#{image}, job=#{job}, entrydate=#{entrydate}, dept_id=#{deptId}, update_time=#{updateTime} WHERE id=#{id}")
void update(Emp emp);

查询

数据封装

在 MyBatis 中,Java 实体类(POJO)与数据库表字段之间的映射关系是数据持久化的核心。

如果字段名与列名不一致,MyBatis 无法自动映射,需要额外配置。

java实体类:

public class Emp {
    private Integer id;
    private String username;
    private String password;
    private String name;
    private String gender;
    private String image;
    private String job;
    private LocalDate entryDate;
    private Integer deptId;
    private LocalDateTime createTime;
    private LocalDateTime updateTime;
}

数据库表:

CREATE TABLE emp (
    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(20),
    password VARCHAR(20),
    name VARCHAR(10),
    gender CHAR(1),
    image VARCHAR(300),
    job VARCHAR(10),
    entry_date DATE,
    dept_id INT UNSIGNED,
    create_time DATETIME,
    update_time DATETIME
);

默认规则 :Java 属性名与数据库列名 完全一致 时,MyBatis 会自动映射。

下划线转驼峰

  • 数据库列名:entry_date
  • Java 属性名:entryDate

方法一:给字段起别名

// 功能:给字段起别名,让取出与对象属性一致
@Select("select id, username, password, name, gender, mname, job, entrydate, " +
        "dept_id deptId, create_time createTime, update_time updateTime from emp where id = #{id}")
public Emp getById(Integer id);

方法二@Results 注解

// 通过 @Results、@Result 注解手动映射封装
@Results({
    @Result(column = "dept_id", property = "deptId"),
    @Result(column = "create_time", property = "createTime"),
    @Result(column = "update_time", property = "updateTime")
})
@Select("select * from emp where id = #{id}")
public Emp getById(Integer id);

方法三:开启Mybatis的驼峰命名与下划线自动映射开关(推荐)

# 驼峰命名与下划线命名自动映射开关
mybatis.configuration.map-underscore-to-camel-case=true

简单查询

SQL语句:

SELECT * FROM emp WHERE id = 1

Mybatis接口:

@Select("SELECT * FROM emp WHERE id = #{id}")
Emp selectById(Integer id);

只要我们开启了上述的自动映射,这里无需额外操作。

条件查询

示例:姓名模糊匹配 + 性别精确匹配 + 入职时间范围筛选 + 更新时间降序

SQL语句:

SELECT * 
FROM emp 
WHERE name LIKE '%${name}%' 
  AND gender = #{gender} 
  AND entrydate BETWEEN #{begin} AND #{end} 
ORDER BY update_time DESC;

Mybatis接口语句:

@Select("SELECT * FROM emp " +
        "WHERE name LIKE '%${name}%' " +
        "AND gender = #{gender} " +
        "AND emp_date BETWEEN #{begin} AND #{end} " +
        "ORDER BY update_time DESC")
public List<Emp> list(String name, Short gender, LocalDate begin, LocalDate end);

为什么不能直接使用#{name}

#{} 会被替换成 ? 占位符,'%#{name}%' 会变成:

WHERE name LIKE '%?%'

这是无效的 SQL,因为 ?放在 ''中不能被识别为参数。

解决办法:

  1. concat 或在 Java 代码里提前拼好 %
WHERE name LIKE CONCAT('%', #{name}, '%')
  1. 在Java传参前:
name = "%" + name + "%";

然后在SQL里直接:

WHERE name LIKE #{name}

参数说明

SpringBoot2.x 集成 MyBatis

  • 写法 :方法参数名可直接作为 SQL 占位符使用
  • 示例
@Select("SELECT * FROM emp " +
        "WHERE name LIKE #{name} " +
        "AND gender = #{gender} " +
        "AND update_time BETWEEN #{begin} AND #{end} " +
        "ORDER BY update_time DESC")
List<Emp> list(String name, Short gender, LocalDate begin, LocalDate end);

独立Mybatis(非 SpringBoot 环境)

  • 写法 :必须使用 @Param 显式绑定参数名
  • 示例
@Select("SELECT * FROM emp " +
        "WHERE name LIKE CONCAT('%', #{name}, '%') " +
        "AND gender = #{gender} " +
        "AND update_time BETWEEN #{begin} AND #{end} " +
        "ORDER BY update_time DESC")
List<Emp> list(@Param("name") String var1,
               @Param("gender") Short var2,
               @Param("begin") LocalDate var3,
               @Param("end") LocalDate var4);

XML配置文件

XML映射文件规范:

  • 文件位置 必须与 Mapper接口包名相同
  • namespace 必须与 Mapper 接口的全限定类名 一致 例:namespace="com.itheima.mapper.EmpMapper"
  • id 必须与 Mapper 接口方法名 一致 例:<select id="list"> 对应 EmpMapper.list(...)
  • resultType 与方法返回值类型一致(通常是实体类全限定名)

实战示例

文件包名与位置:

1756181793871

基本结构:

<?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="top/ulna520/springbootmybatiscrud/Mapper/EmpMapper.java">

</mapper>

我们将这个方法改为XML配置:

@Select("SELECT * FROM emp WHERE id = #{id}")
Emp selectById(Integer id);

我们在xml中添加对应的Select配置:

<?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="top.ulna520.springbootmybatiscrud.Mapper.EmpMapper">
    <select id="selectById" resultType="top.ulna520.springbootmybatiscrud.Model.Emp">
        SELECT * FROM emp WHERE id = #{id}
    </select>
</mapper>

然后将EmpMapper中的注解删除,只保留签名方法:

Emp selectById(Integer id);

插件推荐——MybatisX

1756182457140

安装插件后:

1756182701968

会自动匹配Mybatis接口与XML配置。可以点击左侧的小鸟图标方便的跳转。

动态SQL

动态 SQL 适用于根据用户输入或外部条件动态拼接查询语句的场景,常用于:

  • 多条件筛选(如姓名、性别、日期范围)
  • 后台管理系统的列表查询
  • 提高 SQL 灵活性,避免硬编码

if

用于判断条件是否成立。使用test属性进行条件判断,如果条件为true,则拼接SQL。

原始sql:

select * from pm_memo 
where title like concat('%', #{name}, '%') 
  and explore = #{explore} 
  and entrydate between #{fbegin} and #{fend} 
order by update_time desc

使用if标签进行改造:

 <select id="list" resultType="com.itheima.pojo.Emp">
    select id, username, password, name, gender, image, job,
    entrydate, dept_id, create_time, update_time from emp
    where
    <if test="name != null">
        name like concat('%',#{name},'%')
    </if>
    <if test="gender != null">
        and gender = #{gender}
    </if>
    <if test="begin != null and end != null">
        and entrydate between #{begin} and #{end}
    </if>
    order by update_time desc
</select>

只有当test中的条件为ture时,<if>中的SQL语句才会被拼接。

但是此时如果所有的条件都为 false,就会导致存在 where但where中条件为空。SQL语句报错。

WHERE

  • 自动添加 WHERE 关键字 :当内部有条件时自动生成 WHERE,否则不生成。
  • 自动去除首个 AND / OR :避免 SQL 语法错误,如 WHERE AND ...
  • 提升 SQL 可读性与健壮性 :无需手动判断是否需要加 WHERE 或处理连接符。

示例:

<select id="list" resultType="com.itheima.pojo.Emp">
    select id, username, password, name, gender, image, job,
    entrydate, dept_id, create_time, update_time from emp
    <where>
        <if test="name != null">
            name like concat('%',#{name},'%')
        </if>
        <if test="gender != null">
            and gender = #{gender}
        </if>
        <if test="begin != null and end != null">
            and entrydate between #{begin} and #{end}
        </if>
    </where>
    order by update_time desc
</select>

✅ 如果第一个条件没有 and,后续条件可以保留 and,MyBatis 会自动处理连接逻辑。

SET

在 MyBatis 中,<set></set> 标签主要用于 动态生成 SQL 的 SET 子句 ,尤其适合 UPDATE 语句。它的核心功能有两个:

  1. 自动去掉多余的逗号

    • 即使你在 <if></if> 中的 SQL 末尾加了 ,<set></set> 会帮你在生成 SQL 时去掉最后一个多余的逗号,避免语法错误。
  2. 条件更新

    • 结合 <if></if> 标签,只有在参数不为空(或满足条件)时,对应的字段才会出现在 SET 子句中,实现 部分字段的动态更新

示例:

<update id="update2">
    update emp
    <set>
        <if test="username != null">username = #{username},</if>
        <if test="name != null">name = #{name},</if>
        <if test="gender != null">gender = #{gender},</if>
        <if test="image != null">image = #{image},</if>
        <if test="job != null">job = #{job},</if>
        <if test="entrydate != null">entrydate = #{entrydate},</if>
        <if test="deptId != null">dept_id = #{deptId},</if>
        <if test="updateTime != null">update_time = #{updateTime}</if>
    </set>
    where id = #{id}
</update>

foreach

在 MyBatis 中,<foreach></foreach> 标签用于 遍历集合类型参数 ,动态生成 SQL 片段,常用于:

  • IN 查询(如批量删除、批量查询)
  • 批量插入
  • 拼接多个字段或条件

常见属性:

属性名说明
collection要遍历的集合名称(如 List、数组、或 Map 的某个键)
item每次遍历的元素变量名
index当前元素的索引(可选)
separator每个元素之间的分隔符(如 ,
open遍历开始前添加的字符串(如 (
close遍历结束后添加的字符串(如 )

collection名称必须和传入参数一致,若是Map类型,要用 collection="mapKey"

示例:

接口方法:

public void deleteByIds(List<Integer> ids);

xml映射文件:

<delete id="deleteByIds">
    delete from emp where id in
    <foreach collection="ids" item="id" separator="," open="(" close=")">
        #{id}
    </foreach>
</delete>

sql&include

SQL 片段 (<sql></sql> 标签)

  • 用于定义可重用的 SQL 语句片段,避免多处重复编写相同列或条件。
  • 必须有唯一 id 作为引用标识

引用 SQL 片段 (<include></include> 标签)

  • 通过 refid 属性引入指定的 <sql></sql> 片段内容。
  • 会在编译映射文件时直接替换成对应 SQL 字符串。

示例:

<!-- ① 定义 SQL 片段 -->
<sql id="commonsSelect">
  id, username, password, name, gender, image, job, dept_id, create_time, update_time
</sql>

<!-- ② 在查询中引用 -->
<select id="list" resultType="com.thitima.pojo.Emp">
  select 
    <include refid="commonsSelect"/>
  from emp
  <where>
    <if test="name != null">
      and name like #{name}
    </if>
    <if test="gender != null and gender != ''">
      and gender = #{gender}
    </if>
    <if test="deptId != null">
      and dept_id = #{deptId}
    </if>
    <if test="begin != null and end != null">
      and create_time between #{begin} and #{end}
    </if>
  </where>
</select>

refid 必须匹配 :引用名应与 <sql id="..."></sql> 完全一致。

PageHelper

PageHelper 是一个基于 MyBatis 的无侵入式分页插件。它的核心作用是自动在SQL中追加分页语句(如 LIMIT),并提供分页信息封装、避免手写分页逻辑。

快速集成

<dependency>
        <groupId>com.github.pagehelper</groupId>
        <artifactId>pagehelper-spring-boot-starter</artifactId>
        <version>1.4.7</version>
</dependency>

使用示例

创建简单查询语句

@Select("SELECT * FROM emp")
    public List<Emp> list();

在Service层使用Pagehelper

@Service
public class EmpService {

    @Autowired
    public EmpMapper empMapper;

    public List<Emp> findAll(int pageNum, int pageSize) {
        PageHelper.startPage(pageNum, pageSize);

        List<Emp> empList = empMapper.list();
        Page<Emp> page = (Page<Emp>) empList;

        return page.getResult();
    }
}

PageHelper常用方法:

方法作用示例
startPage(int pageNum, int pageSize)按页码分页PageHelper.startPage(1, 10)
startPage(int pageNum, int pageSize, String orderBy)分页并指定排序规则PageHelper.startPage(1, 10, "id desc")
offsetPage(long offset, long limit)按偏移量分页(适合无限滚动等场景)PageHelper.offsetPage(20, 10)
orderBy(String orderBy)单独设置排序规则(不分页也可用)PageHelper.orderBy("create_time desc")
clearPage()清除当前线程分页参数,避免影响后续查询PageHelper.clearPage()

Page对象常用方法:

方法返回值类型说明
getResult()List<T>当前页数据列表
getTotal()long总记录数
getPages()int总页数
getPageNum()int当前页码
getPageSize()int每页记录数
getStartRow()int当前页起始行号(从 1 开始)
getEndRow()int当前页结束行号
isCount()boolean是否执行了 count 查询

PageInfo(对Page进行封装类)常用方法:

方法返回值类型说明
getList()List<T>当前页数据
getTotal()long总记录数
getPages()int总页数
getPageNum()int当前页码
getPageSize()int每页记录数
isHasNextPage()boolean是否有下一页
isHasPreviousPage()boolean是否有上一页
getNavigatePages()int导航页码数量(可配置)
getNavigatepageNums()int[]导航页码数组
isIsFirstPage()boolean是否为第一页
isIsLastPage()boolean是否为最后一页

分页查询示例:

import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;

public PageInfo<User> listUsers(int pageNum, int pageSize) {
    // 1. 开启分页(自动作用于紧跟的第一条查询语句)
    PageHelper.startPage(pageNum, pageSize, "create_time desc");
  
    // 2. 执行查询(Mapper 方法不需要修改 SQL)
    List<User> users = userMapper.selectAll();

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

调用:

PageInfo<User> pageInfo = listUsers(2, 5);
System.out.println("总记录数: " + pageInfo.getTotal());
System.out.println("总页数: " + pageInfo.getPages());
System.out.println("当前页数据: " + pageInfo.getList());