MyBatis 是什么?为什么说它是半自动 ORM?

半ORM(对象关系映射)框架,内部封装JDBC,开发时只需关注SQL语句本身,无需处理加载驱动、创建连接等繁杂过程。通过XML或注解配置映射,将对象与数据库表关联。

ORM

ORM(Object Relational Mapping)是一种解决关系型数据库数据与简单Java对象(POJO)映射关系的技术。它通过使用描述对象和数据库之间映射的元数据,将程序中的对象自动持久化到关系型数据库中。

与传统的JDBC相比,MyBatis的优点?

基于 SQL 语句编程,SQL 写在 XML 里,解除 sql 与程序代码的耦合

支持编写动态 SQL 语句

减少了 50%以上的代码量,消除了 JDBC 大量冗余的代码,不需要手动开关连接

能够与 Spring 很好的集成,开发效率高

Mybatis动态sql标签

  • <if>:按条件拼接 SQL。
  • <choose>、<when>、<otherwise>:从多个分支中选择一个。
  • <trim>、<where>、<set>:处理前后缀、多余的 AND、逗号等问题。
  • <foreach>:遍历集合,常用于 IN 条件和批量插入。

Mybatis有哪些核心组件?

  • SqlSessionFactoryBuilder:读取配置并创建 SqlSessionFactory。构建完成后通常就可以释放。
  • SqlSessionFactory:创建 SqlSession,构建成本较高,通常在应用中保留一个实例。
  • SqlSession:执行 SQL、管理事务并获取 Mapper。它不是线程安全的,应该限定在一次请求、一次方法或一次事务内。
  • Executor:负责执行sql,查询、更新、一级缓存。
  • StatementHandler:创建并操作 JDBC Statement。
  • ParameterHandler:把参数设置到 PreparedStatement。
  • ResultSetHandler:把 JDBC ResultSet 映射为 Java 对象。

Mybatis执行流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
SqlSessionFactory
↓ 创建
SqlSession
↓ 获取
Mapper方法
↓
Executor
↓
StatementHandler
↓
ParameterHandler
↓
数据库
↓
ResultSetHandler
↓
Java对象

初始化阶段(只做一次):

  1. 读取配置,构建 SqlSessionFactory。

执行阶段(每次操作):

  1. 开启 SqlSession。
  2. 利用JDK 动态代理获取 Mapper 接口的代理对象。
  3. 调用 Mapper 方法,底层交由核心中枢 Executor(执行器)调度(查缓存或查库)。
  4. StatementHandler:创建并操作 JDBC Statement。
  5. ParameterHandler:给 SQL 中的 ? 占位符赋值(处理 #{})。
  6. ResultSetHandler:将返回的数据库表格数据自动映射并封装为 Java 对象。

其中MyBatis 会为 Mapper 接口创建动态代理对象,调用接口方法时,代理对象会找到对应的 SQL 并执行。

所以哪怕Mapper 接口本身没有实现类

比如:

1
2
3
public interface UserMapper {
User selectById(Long id);
}

也可以:

1
mapper.selectById(1L);

#{} 和 ${} 的本质区别到底是什么?

#{} 会被 MyBatis 解析为 ? 占位符。参数赋值时,JDBC 会自动对特殊字符进行转义。核心作用是防止 SQL 注入。

${} 只是简单的字符串替换,发生在 SQL 解析的阶段,直接把参数拼接到 SQL 原文中,极易引发 SQL 注入。

比如:SELECT * FROM user WHERE username = ? AND password = ?就不会sql注入

为什么 MyBatis 还要设计 ${}?什么时候可能会用到 ${}?

因为有些东西不能用 #{} 作为参数传递。

最典型的就是:表名、列名、排序字段。

假设要做一个动态排序,用户点击前端按钮,想按 create_time 降序排列。 如果用 #{}:

ORDER BY #{columnName} ➡️ 解析后变成 ORDER BY 'create_time' (表字段被加了单引号,变成了普通字符串,SQL 会报错或排序失效)

如果用 ${}:

ORDER BY ${columnName} ➡️ 解析后变成 ORDER BY create_time (直接拼上去了,语法正确,完美运行)

所以**#{} 用于值,${} 可以用于 SQL 结构**

resultType和resultMap

resultType

resultType 直接指定返回对象类型,适合列名与属性名容易自动匹配的简单查询。

1
2
3
4
5
public class User {
private Long id;
private String name;
private Integer age;
}
1
2
3
4
5
<select id="selectById" resultType="User">
SELECT id, name, age
FROM user
WHERE id = #{id}
</select>

如果数据库字段名和 Java 属性名能够对应上,直接用 resultType 就行。

resultMap

resultMap 可以显式描述列与属性的关系

1
2
3
4
<resultMap id="userMap" type="User">
<id property="id" column="id"/>
<result property="userName" column="user_name"/>
</resultMap>

自动映射可以配合 mapUnderscoreToCamelCase=true 把 user_name 映射到 userName。复杂 Join 查询不宜过度依赖全自动映射,重名列应使用别名,并在 resultMap 中明确配置。

resultMap 还可以通过 <association> 和 <collection> 标签来处理一对一、一对多的复杂嵌套查询和结果集映射。

Mybatis缓存

一级缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
SqlSession
│
├── 第一次 selectById(1)
│ ↓
│ 查数据库
│ ↓
│ 放进一级缓存
│
└── 第二次 selectById(1)
↓
先查缓存
↓
有 → 直接返回

同一个 SqlSession 中,相同的查询可以直接从缓存获取,减少数据库访问。

缓存失效:

如果执行了:

1
2
3
INSERT
UPDATE
DELETE

MyBatis 通常会清空一级缓存。因为数据库数据已经发生变化了。

二级缓存

它是跨 SqlSession 的。只要是针对同一个 Mapper(即同一个 XML 配置文件里的 SQL)的查询,不同的会话都可以共享这块缓存。

开发一般禁止使用:跨 Mapper 修改同一张表时,其他命名空间的缓存不会自动知道。可能有脏数据

Mapper 方法有多个参数时,如何引用参数?

推荐使用 @Param 明确参数名:

1
2
3
4
User selectByTenantAndId(
@Param("tenantId") Long tenantId,
@Param("userId") Long userId
);
1
2
3
4
5
6
<select id="selectByTenantAndId" resultType="User">
SELECT id, name
FROM users
WHERE tenant_id = #{tenantId}
AND id = #{userId}
</select>

在 Java 代码编译成字节码的过程中,方法的局部变量名(比如你的 tenantId 和 userId)默认会被“擦除”。如果不加任何注解,MyBatis 在运行时看到的参数名会变成类似 arg0、arg1 或者 param1、param2 这种毫无意义的代号。

@Mapper和@MapperScan

@Mapper 用于标识一个接口是 MyBatis Mapper,让 MyBatis 为它创建代理对象;@MapperScan 用于批量扫描指定包下的 Mapper 接口,避免每个接口都添加 @Mapper

1
2
3
4
5
@MapperScan("com.xxx.mapper")
@SpringBootApplication
public class Application {

}

注:@Transactional 一般放在 Service 层,因为一个业务可能包含多个数据库操作,调用多个mapper

分页

mysql分页

1
2
SELECT * FROM user
LIMIT 0,10;//从第 0 条开始,查询 10 条数据

实际项目:PageHelper

1
2
3
PageHelper.startPage(1,10);//相当于LIMIT 0,10

List<User> list = userMapper.selectUser();

什么是 N+1 问题?

简单来说,当你想要查询一个包含子对象的数据列表时,框架执行了 1 次主查询获取了 N 条主数据,然后为了补全这 N 条主数据的关联对象,又在底层偷偷执行了 N 次子查询。

场景举例: 假设你要查询 100 个用户信息,并且每个用户都要带上他们的部门信息。

  • 1次主查询: SELECT * FROM user LIMIT 100 (查出 100 个用户)
  • N次子查询: 接着 MyBatis 会循环这 100 个用户,执行 100 次 SELECT * FROM department WHERE id = ?

总共执行了 101 次 SQL。如果数据量大,频繁的网络开销和数据库连接会直接把数据库性能拖垮。

1
2
3
4
5
6
7
<!-- 🚨 危险:这就是导致 N+1 的写法 -->
<resultMap id="BadMap" type="User">
<!-- 这里有一个 select 属性,告诉 MyBatis 遇到这个字段时,去执行另一条 SQL -->
<association property="department" column="dept_id" select="selectDeptById" />
</resultMap>


解决:代码层面的 IN 查询(分步批处理)

  1. 查出 100 个用户(1 次 SQL)。
  2. 在代码里提取出这 100 个用户的 dept_id 集合。
  3. 用 SELECT * FROM department WHERE id IN (...) 查出所有部门(1 次 SQL)。
  4. 在 Java 内存里用 Map 把部门拼装到用户对象里。