MyBatis
MyBatis 是一款半 ORM 框架,SQL 由开发者编写,适合复杂查询和性能要求高的场景。相比 Hibernate 更加轻量灵活,允许直接编写原生 SQL。
架构流程
核心组件
| 组件 | 说明 |
|---|---|
| Configuration | 保存所有配置信息 |
| SqlSession | 与数据库交互的会话 |
| Executor | 执行器,负责 SQL 生成和缓存维护 |
| StatementHandler | 封装 JDBC Statement 操作 |
| ParameterHandler | 参数转 JDBC 类型 |
| ResultSetHandler | ResultSet 转 List 集合 |
| TypeHandler | Java 类型与 JDBC 类型转换 |
| MappedStatement | 封装 SQL 映射配置 |
| SqlSource | 动态生成 SQL |
| BoundSql | 动态 SQL 及参数信息 |
MyBatis vs Hibernate
| 维度 | MyBatis | Hibernate |
|---|---|---|
| 类型 | 半 ORM | 全 ORM |
| SQL 控制 | 开发者编写原生 SQL | 自动生成 SQL |
| 学习曲线 | 低(只需 Java + SQL) | 较高 |
| 适用场景 | 复杂查询、高性能 | 标准 CRUD |
MyBatis-Plus
MyBatis-Plus 是 MyBatis 的增强工具,提供更便捷的 CRUD 操作。
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>注意
避免版本冲突
引入 MyBatis-Plus 后不要再次引入 MyBatis 以及 MyBatis-Spring。
参数绑定 #{} 与 ${}
| 区别 | #{} | ${} |
|---|---|---|
| 处理方式 | 预编译处理 | 字符串替换 |
| SQL 表现 | 替换为 ?,PreparedStatement 赋值 | 直接替换成变量值 |
| 安全性 | 有效防止 SQL 注入 | 存在 SQL 注入风险 |
注意
${} 的适用场景
动态传入表名、列名、ORDER BY ${columnName} 时才使用。拼接 SQL 时始终确保参数来源可信,或优先使用 #{}。
模糊查询写法
// 方式一:Java 代码添加通配符(推荐)
String wildcardname = "%smi%";
List<Name> names = mapper.selectlike(wildcardname);<!-- 方式一 -->
<select id="selectlike">
select * from foo where bar like #{value}
</select>
<!-- 方式二:SQL 中拼接通配符 -->
<select id="selectlike">
select * from foo where bar like "%"#{value}"%"
</select>Mapper 接口原理
| 实现方式 | 适用场景 |
|---|---|
| 注解绑定 | SQL 简单(@Select、@Update 等) |
| XML 绑定 | SQL 复杂(需指定 namespace) |
Mapper 接口基于 JDK 动态代理 实现:
- 运行时为 Mapper 接口生成代理对象
- 代理对象拦截接口方法
- 转为执行 MapperStatement 所代表的 SQL
- 返回执行结果
命名规则:接口全限名 = 映射文件 namespace,接口方法名 = Statement id,接口参数 = SQL 参数。Mapper 接口不能重载(全限名+方法名的保存策略)。
XML 配置
<configuration>
<properties resource="application.properties" />
<settings>
<setting name="cacheEnabled" value="true" />
<setting name="lazyLoadingEnabled" value="false" />
</settings>
<typeAliases>
<typeAlias alias="Student" type="com.mybatis3.domain.Student" />
<package name="com.mybatis3.domain" />
</typeAliases>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC" />
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driverClassName}" />
<property name="url" value="${jdbc.url}" />
<property name="username" value="${jdbc.username}" />
<property name="password" value="${jdbc.password}" />
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="com.mybatis3/mappers/StudentMapper.xml" />
</mappers>
</configuration>动态 SQL
<!-- if -->
<select id="findActiveBlogLike" resultType="Blog">
SELECT * FROM BLOG WHERE state = 'ACTIVE'
<if test="title != null">AND title like #{title}</if>
<if test="author != null">AND author like #{author}</if>
</select>
<!-- choose/when/otherwise -->
<select id="findActiveBlogLike" resultType="Blog">
SELECT * FROM BLOG WHERE state = 'ACTIVE'
<choose>
<when test="title != null">AND title like #{title}</when>
<when test="author != null">AND author like #{author}</when>
<otherwise>AND featured = 1</otherwise>
</choose>
</select>
<!-- where -->
<select id="findActiveBlogLike" resultType="Blog">
SELECT * FROM BLOG
<where>
<if test="state != null">state = #{state}</if>
<if test="title != null">AND title like #{title}</if>
<if test="author != null">AND author like #{author}</if>
</where>
</select>
<!-- set -->
<update id="updateAuthorIfNecessary" parameterType="domain.blog.Author">
update Author
<set>
<if test="username != null">username=#{username},</if>
<if test="email != null">email=#{email},</if>
</set>
where id=#{id}
</update>
<!-- foreach -->
<select id="selectPostIn" resultType="domain.blog.Post">
SELECT * FROM POST WHERE ID in
<foreach item="item" index="index" collection="list" open="(" separator="," close=")">
#{item}
</foreach>
</select>注解配置
public interface StudentMapper {
@Select("SELECT * FROM STUDENTS")
List<Student> findAllStudents();
@Select("SELECT * FROM STUDENTS WHERE STUD_ID=#{id}")
Student findStudentById(Integer id);
@Insert("INSERT INTO STUDENTS(NAME,EMAIL,DOB) VALUES(#{name},#{email},#{dob})")
@Options(useGeneratedKeys=true, keyProperty="studId")
int insertStudent(Student student);
}Spring 集成
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="mapperLocations" value="classpath:com/mybatis3/mappers/*.xml" />
</bean>
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.mybatis3.mappers" />
</bean>分页
| 方式 | 原理 | 适用场景 |
|---|---|---|
| RowBounds | 内存分页 | 小数据量 |
| 分页插件 | 拦截 SQL,物理分页 | 生产环境推荐 |
分页插件(如 PageHelper)原理:拦截待执行 SQL,重写添加物理分页语句。生产环境推荐使用分页插件,避免内存溢出。
延迟加载
MyBatis 仅支持 association(一对一)和 collection(一对多)的延迟加载,使用 CGLIB 创建代理对象。
<settings>
<setting name="lazyLoadingEnabled" value="true"/>
</settings>缓存机制
| 缓存 | 作用域 | 默认状态 |
|---|---|---|
| 一级缓存 | Session | 默认打开 |
| 二级缓存 | Mapper (Namespace) | 默认关闭 |
一级缓存:Session 级,Session flush/close 后清空,自动生效无需配置。
二级缓存:在 Mapper XML 中添加 <cache/> 开启,实体类需实现 Serializable 接口,CUD 操作后默认清除。
插件机制
四大组件支持插件拦截:ParameterHandler、ResultSetHandler、StatementHandler、Executor。
@Intercepts({
@Signature(type = StatementHandler.class, method = "query",
args = {StatementHandler.class, ResultSetHandler.class})
})
public class ExamplePlugin implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
return invocation.proceed();
}
}批量操作
SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
try {
Mapper mapper = sqlSession.getMapper(Mapper.class);
for (String name : names) {
mapper.insertName(name);
}
sqlSession.commit();
} finally {
sqlSession.close();
}关联查询对比
| 方式 | 说明 | SQL 次数 |
|---|---|---|
| 联合查询 | 几个表联合查询 | 1 次 |
| 嵌套查询 | 先查主表再查关联表 | N+1 次 |
联合查询适合数据量小、关联字段有索引的场景;嵌套查询适合主表数据量大、关联表数据量小的场景。
ORM 映射规约
- 明确字段列表,按需查询,避免
* - 使用 resultMap 明确映射关系
- 使用
#{}避免 SQL 拼接 - 批量操作使用
<foreach>标签 is_xxx布尔字段在 POJO 中去掉is前缀,resultMap 中显式映射- 更新记录时必须同时更新
update_time
常见问题
| 问题 | 解决方案 |
|---|---|
| SQL 注入风险 | 始终使用 #{} 预编译参数;动态表名/列名必须白名单校验 |
| N+1 查询问题 | 使用联合查询(JOIN)替代嵌套查询;或开启延迟加载 |
| 一级缓存不生效 | 确认同一 SqlSession 中查询;检查是否调用了 commit() 或 close() |