一、MyBatis 简介
MyBatis是一款优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集的工作,可以使用简单的 XML 或注解来配置和映射原生信息,将接口和 Java 的 POJOs(Plain Old Java Objects,普通的 Java 对象)映射成数据库中的记录。
二、核心组件与工作原理
1. 核心组件
- SqlSessionFactoryBuilder:用于创建 SqlSessionFactory 实例。
- SqlSessionFactory:每个 MyBatis 应用的核心,用于创建 SqlSession 实例。
- SqlSession:包含了执行 SQL 命令所需的所有方法,是 MyBatis 与数据库交互的会话。
- Executor:MyBatis 执行器,负责 SQL 语句的生成和查询缓存的维护。
- MappedStatement:封装了 SQL 语句的映射信息。
- ParameterHandler:负责将用户传递的参数转换成 JDBC Statement 所需的参数。
- ResultSetHandler:负责将 JDBC 返回的 ResultSet 结果集对象转换成 List 集合。
2. 工作原理
- 读取 MyBatis 配置文件(mybatis-config.xml)和映射文件(Mapper.xml)。
- 通过 SqlSessionFactoryBuilder 构建 SqlSessionFactory。
- SqlSessionFactory 创建 SqlSession。
- SqlSession 调用 Executor 执行器执行映射文件中定义的 SQL。
- Executor 通过 MappedStatement 对象获取 SQL 信息,通过 ParameterHandler 设置参数,通过 ResultSetHandler 处理结果集。
- 返回处理结果。
三、配置文件详解
1. 核心配置文件(mybatis-config.xml)
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <!-- 环境配置 --> <environments default="development"> <environment id="development"> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <property name="driver" value="com.mysql.cj.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/mybatis_db"/> <property name="username" value="root"/> <property name="password" value="123456"/> </dataSource> </environment> </environments> <!-- 映射器配置 --> <mappers> <mapper resource="com/example/mapper/UserMapper.xml"/> </mappers> </configuration>2. 映射文件(Mapper.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"> <mapper namespace="com.example.mapper.UserMapper"> <!-- 结果映射 --> <resultMap id="userResultMap" type="com.example.entity.User"> <id property="id" column="id"/> <result property="username" column="username"/> <result property="email" column="email"/> </resultMap> <!-- 查询语句 --> <select id="selectUserById" resultMap="userResultMap"> SELECT * FROM user WHERE id = #{id} </select> </mapper>四、动态 SQL
MyBatis 的动态 SQL 功能是其核心特性之一,它允许在 XML 映射文件中编写灵活的 SQL 语句,根据传入参数的条件动态地拼接 SQL 片段,从而避免了在 Java 代码中拼接字符串的繁琐和潜在的安全风险。
1. 动态 SQL 标签
<if> 标签
作用:根据条件判断是否包含 SQL 片段。
属性:test属性用于指定 OGNL 表达式,表达式结果为 true 时,标签内的 SQL 会被包含。
<select id="findActiveUser" resultType="User"> SELECT * FROM user WHERE status = 'ACTIVE' <if test="username != null and username != ''"> AND username = #{username} </if> <if test="email != null"> AND email = #{email} </if> </select><choose>, <when>, <otherwise> 标签
作用:实现多分支条件选择,类似于 Java 中的switch-case-default语句。
<select id="findUserByState" resultType="User"> SELECT * FROM user WHERE <choose> <when test="state == 'ACTIVE'"> status = 'ACTIVE' </when> <when test="state == 'INACTIVE'"> status = 'INACTIVE' </when> <otherwise> status IS NOT NULL </otherwise> </choose> </select><trim>, <where>, <set> 标签
<where>:智能处理 WHERE 子句。只有当其包含的标签有返回值时,才会插入 “WHERE” 关键字。如果标签返回的内容以 “AND” 或 “OR” 开头,它会自动将其去除。
<!-- 使用 where 标签,避免 WHERE 后直接跟 AND 导致语法错误 --> <select id="findUsers" resultType="User"> SELECT * FROM user <where> <if test="username != null"> AND username = #{username} </if> <if test="email != null"> AND email = #{email} </if> </where> </select><set>:智能处理 UPDATE 语句中的 SET 子句。它会动态地在行首添加 SET 关键字,并会删掉无关的逗号。
<update id="updateUser"> UPDATE user <set> <if test="username != null">username = #{username},</if> <if test="email != null">email = #{email},</if> <if test="status != null">status = #{status},</if> </set> WHERE id = #{id} </update><trim>:功能更强大的标签,可以自定义前缀、后缀以及要删除的前缀/后缀字符串。可用于替代<where>或<set>。
<!-- 用 trim 实现 where 标签的功能 --> <select id="findUsers" resultType="User"> SELECT * FROM user <trim prefix="WHERE" prefixOverrides="AND |OR "> <if test="username != null"> AND username = #{username} </if> <if test="email != null"> AND email = #{email} </if> </trim> </select><foreach> 标签
作用:遍历集合(如 List、Set、数组、Map),常用于构建 IN 条件或批量操作。
属性:
collection:指定要遍历的集合属性名。item:遍历过程中每个元素的别名。index:遍历 List 或数组时的索引,遍历 Map 时的 key。open:遍历开始时添加的字符串。close:遍历结束时添加的字符串。separator:元素之间的分隔符。
<!-- 构建 IN 查询 --> <select id="selectUsersInIds" resultType="User"> SELECT * FROM user WHERE id IN <foreach collection="ids" item="id" open="(" separator="," close=")"> #{id} </foreach> </select> <!-- 批量插入 --> <insert id="batchInsertUsers"> INSERT INTO user (username, email) VALUES <foreach collection="userList" item="user" separator=","> (#{user.username}, #{user.email}) </foreach> </insert><bind> 标签
作用:创建一个变量(通常用于模糊查询或字符串处理)并将其绑定到上下文,可以在 OGNL 表达式中使用。
<select id="findUsersLikeName" resultType="User"> <bind name="pattern" value="'%' + username + '%'" /> SELECT * FROM user WHERE username LIKE #{pattern} </select>2. 动态 SQL 原理与最佳实践
原理:MyBatis 在解析 XML 映射文件时,会构建一个包含动态 SQL 节点(如<if>,<foreach>)的 SQL 源对象。执行时,根据传入的参数对象,使用 OGNL 表达式计算每个动态节点的test条件,动态地拼接出最终的 SQL 字符串。
最佳实践:
- 优先使用
<where>和<set>标签,避免 SQL 语法错误。 - 在
<if>的test条件中,使用and和or进行逻辑判断,注意字符串空值判断:test="name != null and name != ''"。 - 对于复杂的动态 SQL,可以考虑使用
<sql>标签定义可重用的 SQL 片段,再用<include>标签引用,提高可维护性。 - 注意性能:过于复杂的动态 SQL 可能会影响 SQL 解析和缓存效率。对于固定模式的多条件查询,可以考虑使用 MyBatis-Plus 等增强框架的 QueryWrapper。
3. 综合示例
<select id="selectByCondition" resultType="User"> SELECT * FROM user <where> <if test="username != null and username != ''"> <bind name="usernamePattern" value="'%' + username + '%'" /> AND username LIKE #{usernamePattern} </if> <if test="statusList != null and statusList.size() > 0"> AND status IN <foreach collection="statusList" item="status" open="(" separator="," close=")"> #{status} </foreach> </if> <choose> <when test="orderBy == 'name'"> ORDER BY username </when> <when test="orderBy == 'time'"> ORDER BY create_time DESC </when> <otherwise> ORDER BY id </otherwise> </choose> </where> </select>五、高级特性
1. 一级缓存与二级缓存
- 一级缓存(SqlSession 级别):默认开启,同一个 SqlSession 中执行相同的 SQL 查询,第二次会从缓存中获取。
- 二级缓存(Mapper 级别):需要手动开启,多个 SqlSession 共享缓存数据。
2. 延迟加载(懒加载)与立即加载
延迟加载(Lazy Loading):也称为懒加载,是指在需要时才加载关联数据。MyBatis 默认使用立即加载,但可以通过配置开启延迟加载。
立即加载(Eager Loading):在查询主对象时,同时加载所有关联对象的数据。
配置延迟加载
<!-- 在 mybatis-config.xml 中配置 --> <settings> <!-- 开启延迟加载 --> <setting name="lazyLoadingEnabled" value="true"/> <!-- 开启按需加载(3.4.1+版本默认true) --> <setting name="aggressiveLazyLoading" value="false"/> </settings>映射文件中的配置
<!-- 在 resultMap 中配置关联关系的加载方式 --> <resultMap id="blogResultMap" type="Blog"> <id property="id" column="id"/> <result property="title" column="title"/> <!-- 立即加载 --> <association property="author" column="author_id" javaType="Author" select="selectAuthorById"/> <!-- 延迟加载 --> <collection property="comments" column="id" ofType="Comment" select="selectCommentsByBlogId" fetchType="lazy"/> </resultMap>立即加载 vs 延迟加载的选择
- 立即加载适用场景:
- 关联数据量小,且大概率会被使用
- 需要一次性获取完整对象图
- N+1 查询问题不严重的情况
- 延迟加载适用场景:
- 关联数据量大,可能不会被使用
- 需要优化首次查询性能
- 避免不必要的数据库查询
- 关联对象层级较深的情况
延迟加载的实现原理
MyBatis 通过动态代理技术(CGLIB 或 Javassist)创建关联对象的代理。当访问代理对象的属性或方法时,才会触发真正的 SQL 查询。
3. 插件(Interceptor)
MyBatis 允许在映射语句执行过程中的某些点进行拦截调用,可以用于分页、性能监控、SQL 日志等场景。
六、常见面试题
- #{} 和 ${} 的区别是什么?
- #{}:预编译处理,可以防止 SQL 注入。
- ${}:字符串替换,直接拼接 SQL 语句,存在 SQL 注入风险。
- MyBatis 如何实现延迟加载?
通过配置
<setting name="lazyLoadingEnabled" value="true"/>开启,使用 CGLIB 或 Javassist 创建代理对象,在真正访问关联对象时才执行查询。 - MyBatis 有哪些执行器?
- SimpleExecutor:每执行一次 update 或 select,就开启一个 Statement 对象,用完立刻关闭。
- ReuseExecutor:执行 update 或 select,以 SQL 作为 key 查找 Statement 对象,存在就使用,不存在就创建。
- BatchExecutor:执行 update(没有 select,JDBC 批处理不支持 select),将所有 SQL 都添加到批处理中,等待统一执行。