Mybatis
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:创建并操作 JDBCStatement。ParameterHandler:把参数设置到PreparedStatement。ResultSetHandler:把 JDBCResultSet映射为 Java 对象。
Mybatis执行流程
1 | SqlSessionFactory |
初始化阶段(只做一次):
- 读取配置,构建
SqlSessionFactory。
执行阶段(每次操作):
- 开启
SqlSession。 - 利用JDK 动态代理获取 Mapper 接口的代理对象。
- 调用 Mapper 方法,底层交由核心中枢
Executor(执行器)调度(查缓存或查库)。 StatementHandler:创建并操作 JDBCStatement。ParameterHandler:给 SQL 中的?占位符赋值(处理#{})。ResultSetHandler:将返回的数据库表格数据自动映射并封装为 Java 对象。
其中MyBatis 会为 Mapper 接口创建动态代理对象,调用接口方法时,代理对象会找到对应的 SQL 并执行。
所以哪怕Mapper 接口本身没有实现类
比如:
1 | public interface UserMapper { |
也可以:
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 | public class User { |
1 | <select id="selectById" resultType="User"> |
如果数据库字段名和 Java 属性名能够对应上,直接用 resultType 就行。
resultMap
resultMap 可以显式描述列与属性的关系
1 | <resultMap id="userMap" type="User"> |
自动映射可以配合 mapUnderscoreToCamelCase=true 把 user_name 映射到 userName。复杂 Join 查询不宜过度依赖全自动映射,重名列应使用别名,并在 resultMap 中明确配置。
resultMap 还可以通过 <association> 和 <collection> 标签来处理一对一、一对多的复杂嵌套查询和结果集映射。
Mybatis缓存
一级缓存
1 | SqlSession |
同一个 SqlSession 中,相同的查询可以直接从缓存获取,减少数据库访问。
缓存失效:
如果执行了:
1 | INSERT |
MyBatis 通常会清空一级缓存。因为数据库数据已经发生变化了。
二级缓存
它是跨 SqlSession 的。只要是针对同一个 Mapper(即同一个 XML 配置文件里的 SQL)的查询,不同的会话都可以共享这块缓存。
开发一般禁止使用:跨 Mapper 修改同一张表时,其他命名空间的缓存不会自动知道。可能有脏数据
Mapper 方法有多个参数时,如何引用参数?
推荐使用 @Param 明确参数名:
1 | User selectByTenantAndId( |
1 | <select id="selectByTenantAndId" resultType="User"> |
在 Java 代码编译成字节码的过程中,方法的局部变量名(比如你的 tenantId 和 userId)默认会被“擦除”。如果不加任何注解,MyBatis 在运行时看到的参数名会变成类似 arg0、arg1 或者 param1、param2 这种毫无意义的代号。
@Mapper和@MapperScan
@Mapper 用于标识一个接口是 MyBatis Mapper,让 MyBatis 为它创建代理对象;@MapperScan 用于批量扫描指定包下的 Mapper 接口,避免每个接口都添加 @Mapper
1 | @MapperScan("com.xxx.mapper") |
注:@Transactional 一般放在 Service 层,因为一个业务可能包含多个数据库操作,调用多个mapper
分页
mysql分页
1 | SELECT * FROM user |
实际项目:PageHelper
1 | PageHelper.startPage(1,10);//相当于LIMIT 0,10 |
什么是 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 | <!-- 🚨 危险:这就是导致 N+1 的写法 --> |
解决:代码层面的 IN 查询(分步批处理)
- 查出 100 个用户(1 次 SQL)。
- 在代码里提取出这 100 个用户的
dept_id集合。 - 用
SELECT * FROM department WHERE id IN (...)查出所有部门(1 次 SQL)。 - 在 Java 内存里用
Map把部门拼装到用户对象里。
