假如你一定要写注释,请确保它描述了离你最近的代码。
简介
MyBatis 是一款优秀的 持久层框架 ,用于简化 JDBC 开发
主要作用:
- 封装了 JDBC 的繁琐操作(如连接管理、SQL 执行、结果映射)。
- 通过 XML 或注解 配置 SQL 与 Java 对象的映射关系。
在分层架构中的位置:
Controller(控制层) → Service(业务层) → DAO(持久层) → 数据库
- DAO(Data Access Object) :通过 MyBatis 与数据库交互。
Mybatis入门
快速入门
创建项目
新建一个SpringBoot项目用于Mybatis入门:

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

创建成功后项目中有这两个依赖:
<!-- 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>
创建数据模型
假设数据库中有如下的用户表。

我们创建对应的用户类: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 |
| 数据库 URL | spring.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 包中的实现类。

数据库连接池
数据库连接池是一个 容器 ,用于分配和管理数据库连接(Connection 对象)。
作用:
- 复用已有连接,避免频繁创建/销毁连接带来的性能开销。
- 统一管理连接的生命周期,防止连接泄漏。

核心机制
- 初始化 :启动时创建一定数量的数据库连接,存放在连接池中。
- 获取连接 :应用程序向连接池请求可用连接,而不是直接新建连接。
- 归还连接 :使用完毕后将连接放回连接池,而不是关闭物理连接。
- 回收机制 :
- 对空闲时间超过最大空闲时间的连接进行释放。
- 避免因未释放连接导致的 连接泄漏 。
常见连接池:
| 实现 | 特点 |
|---|---|
| C3P0 | 较早期,稳定性好,性能一般 |
| DBCP | Apache 提供,配置简单 |
| 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日志:

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

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日志:

主键返回
在数据添加成功后,需要获取插入数据库数据的主键。
@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,因为
?放在''中不能被识别为参数。
解决办法:
- 用
concat或在 Java 代码里提前拼好%:
WHERE name LIKE CONCAT('%', #{name}, '%')
- 在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 与方法返回值类型一致(通常是实体类全限定名)
实战示例
文件包名与位置:

基本结构:
<?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

安装插件后:

会自动匹配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 语句。它的核心功能有两个:
-
自动去掉多余的逗号
- 即使你在
<if></if>中的 SQL 末尾加了,,<set></set>会帮你在生成 SQL 时去掉最后一个多余的逗号,避免语法错误。
- 即使你在
-
条件更新
- 结合
<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());