MyBatis 持久层开发
本文介绍 MyBatis 持久层开发的核心用法,覆盖 MyBatis 基本概念、Spring Boot 集成、Mapper 接口、XML 映射文件、参数传递、# 与 $ 的区别、ResultMap 和动态 SQL。文章帮助读者理解 Java 后端项目如何通过 MyBatis 完成数据库查询和数据映射。
第一章 什么是 MyBatis?
在传统的 Java Web 开发中,我们通常使用 JDBC 来连接数据库和执行 SQL,但原生 JDBC 存在很多痛点:
- 繁杂的样板代码:需要手动获取和关闭数据库连接、创建 Statement。
- 参数设置繁琐:需要手动将 Java 对象中的数据,逐个按索引设置到 SQL 语句(
?)中。 - 结果集解析痛苦:需要手动遍历
ResultSet,将每一行记录映射封装成 Java 对象。
MyBatis 是一款优秀的持久层框架,更准确地说属于 SQL 映射框架。它内部封装了 JDBC,使开发者只需要关注 SQL 语句本身,而不需要花费精力去处理繁杂的数据库连接及结果映射过程。 MyBatis 的核心思想是:将 SQL 语句配置在 XML 或注解中,通过映射机制把 Java 对象与 SQL 语句及结果集关联起来。
第二章 快速入门程序(基于 Spring Boot)
下面我们通过一个最基础的 Spring Boot 整合 MyBatis 的例子,带你快速体验开发流程。
2.1 引入依赖
在 pom.xml 中引入 MyBatis 官方提供的起步依赖和 MySQL 驱动。
<!-- MyBatis Spring Boot 启动器 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
<!-- MySQL 驱动 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
2.2 配置数据源
在 application.yml 中配置数据库连接信息。Spring Boot 默认集成并使用了高性能的 HikariCP 连接池。
spring:
datasource:
url: jdbc:mysql://localhost:3306/db_name?characterEncoding=utf-8&serverTimezone=UTC
username: root
password: root_password
driver-class-name: com.mysql.cj.jdbc.Driver
2.3 准备实体类 (Entity)
创建一个与数据库表字段对应的 Java 类:
public class User {
private Integer id;
private String name;
private Integer age;
// 省略 getter、setter 和 toString() 方法
}
2.4 定义 Mapper 接口
Mapper 接口相当于我们常说的 Dao 层。使用 @Mapper 注解将其交给 Spring 容器管理。
@Mapper
public interface UserMapper {
// #{id} 是 MyBatis 的参数占位符,它会自动将方法参数的值填充进来
@Select("SELECT * FROM user WHERE id = #{id}")
User findById(Integer id);
@Options(useGeneratedKeys = true, keyProperty = "id")
@Insert("INSERT INTO user(name, age) VALUES(#{name}, #{age})")
void insert(User user);
@Update("UPDATE user SET age = #{age} WHERE id = #{id}")
void update(User user);
@Delete("DELETE FROM user WHERE id = #{id}")
void delete(Integer id);
}
获取自增主键: 如果在插入数据之后想要立刻获取到数据库自动生成的主键 ID,可以使用 @Options(useGeneratedKeys = true, keyProperty = "id")。
useGeneratedKeys = true:开启获取数据库生成的主键。keyProperty = "id":将生成的主键值赋给传入对象(如User对象)的id属性。执行完insert后,可直接通过user.getId()拿到该主键。
2.5 调用与测试
在 Service 逻辑层或单元测试中,直接通过 @Autowired 注入 Mapper 即可使用:
@SpringBootTest
public class UserMapperTest {
@Autowired
private UserMapper userMapper;
@Test
public void testFindById() {
User user = userMapper.findById(1);
System.out.println("查询到的用户:" + user);
}
}
第三章 XML 映射文件详解
在入门程序中,我们使用了 @Select 等注解来编写 SQL。但当 SQL 语句非常长,或者需要多表关联、动态拼接(如多条件搜索)时,写在注解里就难以维护。因此,XML 映射才是 MyBatis 的灵魂和企业开发最常用的方式。
3.1 配置 XML 扫描路径
通常,我们将 XML 映射文件放在 src/main/resources/mapper 目录下。需要在 application.yml 中告诉 MyBatis 去哪里找它们:
mybatis:
mapper-locations: classpath:mapper/*.xml
# 开启实体类别名扫描,在 XML 中就不需要写 com.example.entity.User 这么长的全限定类名了,直接写 User 即可
type-aliases-package: com.example.demo.entity
3.2 编写 XML 映射文件
新建 resources/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">
<!-- namespace 必须与对应的 Mapper 接口全限定名完全一致 -->
<mapper namespace="com.example.demo.mapper.UserMapper">
<!-- id 必须与接口中的方法名一致 -->
<!-- resultType 指定返回结果的单条记录映射类型(使用了别名,直接写 User) -->
<select id="findById" resultType="User">
SELECT id, name, age FROM user WHERE id = #{id}
</select>
<!-- insert 时,配置 useGeneratedKeys="true" 和 keyProperty="id" 可以自动将数据库生成的自增 ID 回填给 User 对象 -->
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
INSERT INTO user(name, age) VALUES(#{name}, #{age})
</insert>
</mapper>
对应 Mapper 接口的方法只需声明即可,无需任何注解:
@Mapper
public interface UserMapper {
User findById(Integer id);
void insert(User user);
}
第四章 参数传递与安全性(# 与 $ 的区别)
在 MyBatis 中传递参数有两种符号:
#{}(预编译):相当于 JDBC 中的?。MyBatis 会使用PreparedStatement安全地设置参数,能有效防止 SQL 注入。绝大多数情况下(如WHERE id = #{id})都应使用它。${}(字符串拼接):相当于直接把参数拼接到 SQL 语句中,存在 SQL 注入风险。- 使用场景:当需要动态传递表名、列名,或者动态指定排序字段时(例如:
ORDER BY ${columnName})。这种场景不能用预编译占位符,但必须在后端代码中严格校验传入的值,避免安全漏洞。
第五章 ResultMap:解决字段不匹配与复杂查询
5.0 resultType 与 resultMap 的选择
5.0.1 什么是 resultType?
resultType 是 MyBatis 的“快捷映射”方式,适合以下简单场景:
- 单表查询,数据库列名与实体类属性名一致(或通过开启全局驼峰映射
map-underscore-to-camel-case自动转换)。 - 返回基本类型或包装类型,如
Integer、String、Map<String,Object>。 - 支持配合全局
TypeHandler实现简单的枚举和类型转换。
注意:它无法处理嵌套对象、集合关联(一对一、一对多)等复杂结构,也不能为特定列单独指定 TypeHandler。
示例:
<select id="findById" resultType="User">
SELECT id, name, age FROM user WHERE id = #{id}
</select>
5.0.2 什么时候必须用 resultMap?
- 列名与属性名无法直接对应(如
user_name→userName且未开启全局驼峰)。 - 需要嵌套映射(一对一
<association>、一对多<collection>)。 - 需要为某些列局部指定类型转换(如指定特定的
TypeHandler处理加解密字段)。 - 需要复用映射规则,多处 SQL 共用同一套字段映射。
- 需要控制延迟加载或缓存优化(
id标签可提升性能)。
一句话总结:
能用 resultType 绝不用 resultMap;一旦字段对不上或要嵌套,立刻换成 resultMap。
5.1 解决字段名与属性名不一致
当数据库字段名叫 create_time,而实体类属性名叫 createTime 时,MyBatis 默认是映射不上的。
快捷方案:在application.yml中开启驼峰命名自动映射:mybatis.configuration.map-underscore-to-camel-case=true
灵活方案:使用 <resultMap> 手动建立映射关系:
<resultMap id="UserResultMap" type="User">
<!-- id 标签专门映射主键,有助于提高性能 -->
<id property="id" column="id"/>
<!-- result 标签映射普通字段 -->
<result property="userName" column="user_name"/>
<result property="createTime" column="create_time"/>
</resultMap>
<!-- 使用 resultMap 代替 resultType -->
<select id="findAll" resultMap="UserResultMap">
SELECT id, user_name, create_time FROM user
</select>
5.2 注解式映射:@Results 与 @Result
如果不使用 XML,也可以直接在 Mapper 接口的方法上通过注解完成结果集映射:
@Results(id = "userResultMap", value = {
@Result(property = "id", column = "id", id = true), // id = true 标记主键
@Result(property = "userName", column = "user_name"),
@Result(property = "createTime", column = "create_time")
})
@Select("SELECT id, user_name, create_time FROM user WHERE id = #{id}")
User findById(Integer id);
@Results 与 @Result 的核心属性:
| 注解 | 属性 | 说明 |
|---|---|---|
@Results | id | 为该映射起个 ID,方便 @ResultMap 复用 |
@Result | property | Java 实体类的属性名 |
@Result | column | 数据库字段名 |
@Result | id | 是否为数据库主键(有助于性能优化) |
@Result | many | 一对多关系映射(搭配 @Many) |
@Result | one | 一对一关系映射(搭配 @One) |
复用已定义的 @Results:
用 @ResultMap 引用其他方法上定义的 @Results(通过 id 属性引用):
// 先定义映射
@Results(id = "userMap", value = {
@Result(property = "id", column = "id", id = true),
@Result(property = "userName", column = "user_name")
})
@Select("SELECT * FROM user WHERE id = #{id}")
User findById(Integer id);
// 复用上面的映射,无需重复定义
@ResultMap("userMap")
@Select("SELECT * FROM user WHERE name LIKE CONCAT('%', #{name}, '%')")
List<User> searchByName(String name);
@Resultsvs XML<resultMap>: 注解方式定义直观,适合简单映射;XML 方式支持更复杂的一对多、多对多关联,且可在多个 XML 映射文件中灵活引用。建议简单场景用注解,复杂关联用 XML。
5.3 复杂的多表关联查询
<resultMap> 常用于处理 一对一(<association>) 和 一对多(<collection>) 的多表关联映射,将关联表的记录嵌套组装到复杂的 Java 对象树中。
1. 一对一映射 (<association>)
场景:一个用户(User)拥有一个身份证信息(IdCard)。 实体类:
public class User {
private Integer id;
private String name;
private IdCard idCard; // 嵌套关联对象
// getters and setters...
}
public class IdCard {
private Integer id;
private String idNumber;
// getters and setters...
}
XML 映射:
<resultMap id="UserWithIdCardMap" type="User">
<id property="id" column="user_id"/>
<result property="name" column="user_name"/>
<!-- association 映射一对一关系 -->
<!-- property: User 实体中的属性名; javaType: 关联对象的类型 -->
<association property="idCard" javaType="IdCard">
<id property="id" column="card_id"/>
<result property="idNumber" column="id_number"/>
</association>
</resultMap>
<select id="findUserWithIdCard" resultMap="UserWithIdCardMap">
SELECT
u.id as user_id, u.name as user_name,
c.id as card_id, c.id_number as id_number
FROM user u
LEFT JOIN id_card c ON u.id = c.user_id
WHERE u.id = #{id}
</select>
2. 一对多映射 (<collection>)
场景:一个部门(Department)包含多个员工(Employee)。 实体类:
public class Department {
private Integer id;
private String deptName;
private List<Employee> employees; // 嵌套集合
// getters and setters...
}
public class Employee {
private Integer id;
private String empName;
// getters and setters...
}
XML 映射:
<resultMap id="DepartmentWithEmployeesMap" type="Department">
<id property="id" column="dept_id"/>
<result property="deptName" column="dept_name"/>
<!-- collection 映射一对多关系 -->
<!-- property: Department 实体中的集合属性名; ofType: 集合内元素的类型 -->
<collection property="employees" ofType="Employee">
<id property="id" column="emp_id"/>
<result property="empName" column="emp_name"/>
</collection>
</resultMap>
<select id="findDepartmentWithEmployees" resultMap="DepartmentWithEmployeesMap">
SELECT
d.id as dept_id, d.dept_name as dept_name,
e.id as emp_id, e.emp_name as emp_name
FROM department d
LEFT JOIN employee e ON d.id = e.dept_id
WHERE d.id = #{id}
</select>
3. 注解方式实现复杂映射
同样,注解也可以实现复杂映射。以一对一为例,使用 @One 和 @Many 配合嵌套查询(N+1 查询方式):
@Select("SELECT * FROM user WHERE id = #{id}")
@Results({
@Result(property = "id", column = "id", id = true),
@Result(property = "name", column = "name"),
// 嵌套查询 IdCard,将 user 表的 id 传递给 findIdCardByUserId 方法
@Result(property = "idCard", column = "id",
one = @One(select = "com.example.mapper.IdCardMapper.findIdCardByUserId"))
})
User findUserWithIdCard(Integer id);
注意:注解的@One和@Many通常采用“嵌套查询”的方式,会引发著名的 N+1 查询问题(执行 1 条主查询后,还需要额外执行 N 条子查询来加载关联数据,影响性能)。而在 XML 中使用的<association>和<collection>配合JOIN语句,可以实现一条 SQL 查出所有数据(嵌套结果映射),性能更好。这也是为什么复杂查询强烈推荐使用 XML 映射文件的原因。
第六章 动态 SQL(MyBatis 的核心杀手锏)
业务中常常遇到条件可选的搜索页面,此时 SQL 的 WHERE 条件是动态变化的。MyBatis 提供了强大的动态标签来实现这一点。
6.1 <if> 与 <where>
<where> 标签会智能地添加 WHERE 关键字,并且自动去除首个条件前多余的 AND 或 OR。
<select id="searchUsers" resultType="User">
SELECT * FROM user
<where>
<!-- 如果 name 属性不为空,则拼接这段 SQL -->
<if test="name != null and name != ''">
AND name LIKE CONCAT('%', #{name}, '%')
</if>
<!-- 如果 age 不为空,则拼接这段 SQL -->
<if test="age != null">
AND age = #{age}
</if>
</where>
</select>
6.2 <set>
用于动态更新,自动添加 SET 关键字,并剔除末尾多余的逗号。
<update id="updateSelective">
UPDATE user
<set>
<if test="name != null">name = #{name},</if>
<if test="age != null">age = #{age},</if>
</set>
WHERE id = #{id}
</update>
6.3 <foreach>
作用:遍历集合/数组,常用于批量操作或 IN 查询的动态 SQL 拼接。
核心属性含义:
collection:被遍历的集合名称(例如传递的参数名叫ids,这里就写ids)。item:集合遍历出来的元素/项(代表当前遍历到的那个变量)。separator:每一次遍历使用的分隔符(可选,例如IN (1, 2, 3)中的,)。open:遍历开始前拼接的片段(可选,例如()。close:遍历结束后拼接的片段(可选,例如))。
<!-- 假设接口传入的参数名为 ids: List<Integer> ids -->
<select id="findByIds" resultType="User">
SELECT * FROM user WHERE id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</select>
第七章 Spring 事务与 MyBatis 协作
在 Spring Boot 项目中,事务由 Spring 管理,MyBatis 的 Mapper 在 @Transactional 方法内会自动使用同一条数据库连接,无需手动 SqlSession.commit()。
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Transactional
public void createOrder(Order order) {
orderMapper.insert(order);
orderMapper.insertLog(...);
// 任一步失败 → 全部回滚
}
}
要点:
- 事务注解应加在 Service 层,不要加在
Mapper或Controller。 - 避免在同一个 Service 内用
this.xxx()调用另一个带@Transactional的方法(会失效)。
系统学习 ACID、隔离级别、传播行为与常见失效场景,请参阅 《Java 数据库事务实战》。
第八章 总结与进阶建议
通过 MyBatis,我们彻底告别了硬编码 JDBC 的时代:
- 注解开发:适合极简的单表 CRUD,开发快速。
- XML 开发:适合复杂 SQL、超长 SQL、连表查询,易于集中管理、复用和排查问题。
- 动态 SQL:让多条件搜索、批量操作等业务逻辑在 SQL 层变得非常灵活且优雅。
进阶方向: 在实际的企业级开发中,我们往往会在 MyBatis 的基础上引入 MyBatis-Plus。MyBatis-Plus 封装了几乎所有的单表 CRUD 操作,让你连简单的 XML 都不用写,而把 XML 专门留给复杂的“多表联查”和“报表统计”。这也是为什么先学懂 MyBatis 原理,再去学习 MyBatis-Plus 才能做到“知其然并知其所以然”。