12. 动态SQL
12.1 动态SQL概述
新增内容:
-
动态SQL执行流程
- MyBatis如何解析动态SQL
- SQL语句构建过程
- 参数绑定机制
-
新增示例
// 动态条件查询接口示例 List<User> searchUsers(@Param("name") String name,@Param("age") Integer age,@Param("email") String email);
<!-- 动态条件查询XML示例 --> <select id="searchUsers" resultType="user">SELECT * FROM users<where><if test="name != null">AND name LIKE #{name}</if><if test="age != null">AND age = #{age}</if><if test="email != null">AND email = #{email}</if></where> </select>
-
最佳实践
- 使用@Param注解明确参数名
- 保持动态SQL简洁
- 避免过度嵌套
动态SQL是指在SQL语句中根据不同的条件动态拼接SQL语句。在实际开发中,我们经常需要根据用户输入的不同条件来构建不同的SQL语句。
版本兼容性
- MyBatis 3.x 全面支持动态SQL
- 部分标签在早期版本中可能不支持
性能考虑
- 动态SQL会增加SQL解析开销
- 合理使用可以减少数据库访问次数
- 避免过度复杂的动态SQL
常见应用场景
-
批量删除
- 示例SQL:
delete from t_car where id in(1,2,3,4,5,6,...)
- 特点:
- 需要删除的id值是动态的,根据用户选择的不同而变化
- 通常用于后台管理系统中的批量操作
- 实际应用:
- 电商后台批量删除商品
- CMS系统批量删除文章
- 用户管理系统批量删除用户
- 示例SQL:
-
多条件查询
- 示例SQL:
select * from t_car where brand like '丰田%' and guide_price > 30 and .....
- 特点:
- 查询条件根据用户输入动态变化
- 条件之间可能存在逻辑关系(AND/OR)
- 实际应用:
- 电商平台高级商品筛选
- CRM系统客户多条件查询
- 报表系统动态数据筛选
- 示例SQL:
-
动态更新
- 示例SQL:
update t_user set username=?, email=? where id=?
- 特点:
- 只更新用户提交的字段
- 避免更新未修改的字段
- 实际应用:
- 用户信息编辑页面
- 商品信息修改
- 系统配置更新
- 示例SQL:
动态SQL的优势
- 提高代码复用性
- 减少代码量
- 提高开发效率
- 使SQL语句更加灵活
12.2 if标签
if标签用于条件判断,根据条件动态拼接SQL语句。它是最基础也是最常用的动态SQL标签。
基本语法
<if test="条件表达式">SQL片段
</if>
示例:多条件查询汽车信息
// Mapper接口
List<Car> selectByMultiCondition(@Param("brand") String brand, @Param("guidePrice") Double guidePrice, @Param("carType") String carType);
<!-- Mapper XML -->
<select id="selectByMultiCondition" resultType="car">select * from t_car where<if test="brand != null and brand != ''">brand like "%"#{brand}"%"</if><if test="guidePrice != null and guidePrice != ''">and guide_price >= #{guidePrice}</if><if test="carType != null and carType != ''">and car_type = #{carType}</if>
</select>
注意事项
-
条件判断问题
- 当第一个条件为空时,SQL语句会出现语法错误(where后面直接跟and)
- 解决方案:在where后面添加一个恒成立的条件,如:
where 1=1
-
空值处理
- 使用
!= null
和!= ''
双重判断,确保参数不为空 - 对于数值类型,还需要考虑0的情况
- 使用
-
参数命名
- 建议使用@Param注解明确指定参数名称
- 避免使用模糊的参数名
12.3 where标签
where标签是if标签的增强版,专门用于处理where子句。
主要作用
-
自动处理where关键字
- 当所有条件为空时,不会生成where子句
- 当至少有一个条件满足时,自动添加where关键字
-
自动处理连接词
- 自动去除条件前面多余的and或or
- 不能自动去除后面多余的and或or
示例代码
<select id="selectByMultiConditionWithWhere" resultType="car">select * from t_car<where><if test="brand != null and brand != ''">and brand like "%"#{brand}"%"</if><if test="guidePrice != null and guidePrice != ''">and guide_price >= #{guidePrice}</if><if test="carType != null and carType != ''">and car_type = #{carType}</if></where>
</select>
使用建议
- 始终将and/or放在条件前面
- 不要依赖where标签处理后面的连接词
- 对于复杂的条件组合,考虑使用trim标签
12.4 trim标签
trim标签提供了更灵活的条件处理方式,可以自定义前缀、后缀以及要删除的内容。
属性说明
- prefix:在trim标签中的语句前添加内容
- suffix:在trim标签中的语句后添加内容
- prefixOverrides:前缀覆盖掉(去掉)
- suffixOverrides:后缀覆盖掉(去掉)
示例代码
<select id="selectByMultiConditionWithTrim" resultType="car">select * from t_car<trim prefix="where" suffixOverrides="and|or"><if test="brand != null and brand != ''">brand like "%"#{brand}"%" and</if><if test="guidePrice != null and guidePrice != ''">guide_price >= #{guidePrice} and</if><if test="carType != null and carType != ''">car_type = #{carType}</if></trim>
</select>
使用场景
- 需要自定义前缀和后缀
- 需要同时处理前后多余的连接词
- 复杂的条件组合
12.5 set标签
set标签专门用于update语句,处理set子句。
主要功能
- 自动处理set关键字
- 自动去除最后多余的逗号
- 只更新提交的不为空的字段
示例代码
<update id="updateWithSet">update t_car<set><if test="carNum != null and carNum != ''">car_num = #{carNum},</if><if test="brand != null and brand != ''">brand = #{brand},</if><if test="guidePrice != null and guidePrice != ''">guide_price = #{guidePrice},</if><if test="produceTime != null and produceTime != ''">produce_time = #{produceTime},</if><if test="carType != null and carType != ''">car_type = #{carType},</if></set>where id = #{id}
</update>
使用建议
- 每个字段赋值后都要加逗号
- 最后一个字段的逗号会被自动去除
- 确保至少有一个字段被更新
12.6 choose when otherwise
这三个标签一起使用,实现类似if-else if-else的逻辑。
特点
- 只有一个分支会被执行
- 按顺序判断条件
- 当所有when都不满足时,执行otherwise
示例代码
<select id="selectWithChoose" resultType="car">select * from t_car<where><choose><when test="brand != null and brand != ''">brand like #{brand}"%"</when><when test="guidePrice != null and guidePrice != ''">guide_price >= #{guidePrice}</when><otherwise>produce_time >= #{produceTime}</otherwise></choose></where>
</select>
使用场景
- 多条件优先级查询
- 条件互斥的场景
- 默认查询条件
12.7 foreach标签
foreach标签用于循环处理集合或数组,常用于批量操作。
属性说明
- collection:集合或数组
- item:集合或数组中的元素
- separator:分隔符
- open:开始符号
- close:结束符号
- index:当前元素的索引(可选)
批量删除示例
- 使用in方式(推荐):
<delete id="deleteBatchByForeach">delete from t_car where id in<foreach collection="ids" item="id" separator="," open="(" close=")">#{id}</foreach>
</delete>
- 使用or方式:
<delete id="deleteBatchByForeach2">delete from t_car where<foreach collection="ids" item="id" separator="or">id = #{id}</foreach>
</delete>
批量添加示例
<insert id="insertBatchByForeach">insert into t_car values <foreach collection="cars" item="car" separator=",">(null,#{car.carNum},#{car.brand},#{car.guidePrice},#{car.produceTime},#{car.carType})</foreach>
</insert>
使用建议
- 批量删除优先使用in方式,性能更好
- 注意集合参数的处理方式
- 考虑批量操作的数据量限制
12.8 sql标签与include标签
这两个标签用于SQL代码复用,提高代码的可维护性。
主要功能
- sql标签:声明可重用的SQL片段
- include标签:引用已声明的SQL片段
示例代码
<!-- 定义SQL片段 -->
<sql id="carCols">id,car_num carNum,brand,guide_price guidePrice,produce_time produceTime,car_type carType
</sql><!-- 使用SQL片段 -->
<select id="selectAllRetMap" resultType="map">select <include refid="carCols"/> from t_car
</select><select id="selectByIdRetMap" resultType="map">select <include refid="carCols"/> from t_car where id = #{id}
</select>
使用建议
- 将常用的列名、条件等提取为SQL片段
- 保持SQL片段的独立性
- 注意SQL片段的命名规范
12.9 动态SQL最佳实践
-
参数处理
- 使用@Param注解明确参数名
- 做好空值处理
- 注意参数类型转换
-
性能优化
- 避免过度使用动态SQL
- 注意SQL注入风险
- 考虑使用缓存
-
代码维护
- 保持SQL语句的可读性
- 适当使用SQL片段
- 添加必要的注释
-
错误处理
- 做好异常处理
- 添加日志记录
- 考虑回滚机制
13. MyBatis高级映射与延迟加载指南
13.1 多对一映射
13.0 概念与实现方式
多对一映射指多个实体对象关联到同一个实体对象的情况(如多个学生属于同一个班级)。
三种实现方式:
1. 级联属性映射(单SQL)2. association标签(单SQL)3. 分步查询(双SQL,推荐)
13.1.1 级联属性映射
实现步骤:
1. 在Student类中添加Clazz属性:
public class Student {private Integer sid; // 学生IDprivate String sname; // 学生姓名private Clazz clazz; // 关联的班级对象// getter和setter方法public Clazz getClazz() {return clazz;}public void setClazz(Clazz clazz) {this.clazz = clazz;}
}
2. 在StudentMapper.xml中配置:
<!-- 定义结果映射 -->
<resultMap id="studentResultMap" type="Student"><!-- 主键映射 --><id property="sid" column="sid"/><!-- 普通属性映射 --><result property="sname" column="sname"/><!-- 级联属性映射 --><result property="clazz.cid" column="cid"/><result property="clazz.cname" column="cname"/>
</resultMap><!-- 查询语句 -->
<select id="selectBySid" resultMap="studentResultMap">select s.*, c.* from t_student s join t_clazz c on s.cid = c.cid where sid = #{sid}
</select>
特点:
- 使用一条SQL语句完成查询
- 通过级联属性映射(clazz.cid)实现关联
- 配置简单,但不够灵活
- 适合简单的关联查询场景
性能考虑:
- 优点:减少数据库连接次数,提高查询效率
- 缺点:当关联表数据量大时,可能会影响查询性能
13.1.2 association标签
使用association标签可以更清晰地表达对象之间的关联关系:
<resultMap id="studentResultMap" type="Student"><id property="sid" column="sid"/><result property="sname" column="sname"/><!-- 使用association标签映射关联对象 --><association property="clazz" javaType="Clazz"><id property="cid" column="cid"/><result property="cname" column="cname"/></association>
</resultMap>
特点:
- 使用一条SQL语句完成查询
- 通过association标签实现关联
- 配置更清晰,易于理解
- 支持更复杂的映射关系
- 适合需要展示关联对象详细信息的场景
最佳实践:
1. 使用有意义的别名,提高SQL可读性2. 明确指定需要查询的字段,避免使用select *3. 合理使用索引,提高查询效率
13.1.3 分步查询(推荐)
分步查询是最常用的方式,它通过两条SQL语句完成查询:
优点:
1. 代码复用性增强:关联查询的SQL可以被其他查询复用2. 支持延迟加载:可以按需加载关联数据,提高性能3. 更好的可维护性:每个查询职责单一4. 更灵活的控制:可以独立优化每个查询
实现步骤:
1. 修改StudentMapper.xml:
<resultMap id="studentResultMap" type="Student"><id property="sid" column="sid"/><result property="sname" column="sname"/><!-- 使用association进行分步查询 --><association property="clazz"select="com.powernode.mybatis.mapper.ClazzMapper.selectByCid"column="cid"/>
</resultMap><select id="selectBySid" resultMap="studentResultMap">select s.* from t_student s where sid = #{sid}
</select>
2. 在ClazzMapper接口中添加方法:
public interface ClazzMapper {/*** 根据班级ID查询班级信息* @param cid 班级ID* @return 班级对象*/Clazz selectByCid(Integer cid);
}
3. 在ClazzMapper.xml中配置:
<select id="selectByCid" resultType="Clazz">select * from t_clazz where cid = #{cid}
</select>
性能优化建议:
1. 为关联字段建立索引2. 合理使用缓存3. 控制查询的字段数量4. 考虑使用批量查询优化性能
13.2 多对一延迟加载
延迟加载(Lazy Loading)是指只有在真正使用到关联数据时才会执行查询,这样可以提高系统性能。
13.2.1 局部延迟加载
在association标签中添加fetchType=“lazy”:
<association property="clazz"select="com.powernode.mybatis.mapper.ClazzMapper.selectByCid"column="cid"fetchType="lazy"/>
使用场景:
@Test
public void testSelectBySid(){StudentMapper mapper = SqlSessionUtil.openSession().getMapper(StudentMapper.class);Student student = mapper.selectBySid(1);// 此时只查询学生信息,不查询班级信息String sname = student.getSname();System.out.println("学生姓名:" + sname);// 当访问班级信息时,才会执行班级查询String cname = student.getClazz().getCname();System.out.println("班级名称:" + cname);
}
延迟加载的注意事项:
1. 确保SqlSession在使用完之前不要关闭2. 注意N+1查询问题3. 考虑使用批量加载优化性能4. 合理设置延迟加载的触发条件
13.2.2 全局延迟加载
在mybatis-config.xml中配置全局延迟加载:
<settings><!-- 开启全局延迟加载 --><setting name="lazyLoadingEnabled" value="true"/><!-- 设置延迟加载的触发方法 --><setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString"/>
</settings>
如果某个SQL不想使用延迟加载,可以设置fetchType=“eager”:
<association property="clazz"select="com.powernode.mybatis.mapper.ClazzMapper.selectByCid"column="cid"fetchType="eager"/>
全局延迟加载的配置选项:
1. lazyLoadingEnabled:是否开启延迟加载2. aggressiveLazyLoading:是否启用积极的延迟加载3. lazyLoadTriggerMethods:指定触发延迟加载的方法
13.3 一对多映射
一对多映射是指一个实体对象包含多个其他实体对象的情况。例如:一个班级包含多个学生。
13.3.1 实现方式
1. 在Clazz类中添加List<Student>属性:
public class Clazz {private Integer cid; // 班级IDprivate String cname; // 班级名称private List<Student> stus; // 学生列表// getter和setter方法public List<Student> getStus() {return stus;}public void setStus(List<Student> stus) {this.stus = stus;}
}
2. 使用collection标签实现:
<resultMap id="clazzResultMap" type="Clazz"><id property="cid" column="cid"/><result property="cname" column="cname"/><!-- 使用collection映射集合属性 --><collection property="stus" ofType="Student"><id property="sid" column="sid"/><result property="sname" column="sname"/></collection>
</resultMap><select id="selectClazzAndStusByCid" resultMap="clazzResultMap">select c.*, s.* from t_clazz c join t_student s on c.cid = s.cid where c.cid = #{cid}
</select>
一对多映射的性能考虑:
1. 数据量大的情况下,建议使用分步查询2. 合理使用索引提高查询效率3. 考虑使用缓存减少数据库访问4. 控制返回的数据量
13.3.2 分步查询
1. 修改ClazzMapper.xml:
<resultMap id="clazzResultMap" type="Clazz"><id property="cid" column="cid"/><result property="cname" column="cname"/><!-- 使用collection进行分步查询 --><collection property="stus"select="com.powernode.mybatis.mapper.StudentMapper.selectByCid"column="cid"/>
</resultMap><select id="selectClazzAndStusByCid" resultMap="clazzResultMap">select * from t_clazz c where c.cid = #{cid}
</select>
2. 在StudentMapper接口中添加方法:
/**
* 根据班级ID查询学生列表
* @param cid 班级ID
* @return 学生列表
*/
List<Student> selectByCid(Integer cid);
3. 在StudentMapper.xml中配置:
<select id="selectByCid" resultType="Student">select * from t_student where cid = #{cid}
</select>
分步查询的优化建议:
1. 使用批量查询代替循环查询2. 合理设置缓存策略3. 优化SQL语句,使用索引4. 控制返回的字段数量
13.4 一对多延迟加载
一对多延迟加载的实现方式与多对一相同:
1. 局部延迟加载:在collection标签中添加fetchType="lazy"2. 全局延迟加载:在mybatis-config.xml中配置lazyLoadingEnabled=true3. 如果开启全局延迟加载后,某个SQL不想使用延迟加载,可以设置fetchType="eager"
使用建议:
1. 对于频繁访问的关联数据,建议使用立即加载(eager)2. 对于不常访问的关联数据,建议使用延迟加载(lazy)3. 在开发阶段可以通过日志查看SQL执行情况,优化加载策略4. 注意处理延迟加载可能带来的性能问题
性能优化技巧:
1. 使用批量查询代替单条查询2. 合理设置缓存策略3. 优化数据库索引4. 控制返回的数据量5. 使用合适的连接池配置
常见问题及解决方案:
1. N+1查询问题:使用批量查询或缓存解决2. 性能问题:优化SQL语句,使用索引3. 内存问题:控制返回的数据量4. 事务问题:确保在事务范围内使用延迟加载
14. MyBatis缓存
14.1 缓存概述
什么是缓存?
缓存(cache)是一种临时存储机制,用于存储频繁访问的数据,以减少对原始数据源的访问次数,从而提高系统性能。
MyBatis缓存的作用
- 减少数据库IO操作
- 提高查询效率
- 减轻数据库压力
- 提升系统响应速度
MyBatis缓存机制
MyBatis的缓存机制:将select语句的查询结果存储到缓存(内存)中,当再次执行相同的select语句时,直接从缓存中获取数据,不再查询数据库。这样既减少了IO操作,又避免了重复执行复杂的查找算法,从而显著提升性能。
MyBatis缓存分类
-
一级缓存(本地缓存)
- 作用域:SqlSession级别
- 存储位置:SqlSession内部
- 特点:默认开启,无需配置
-
二级缓存(全局缓存)
- 作用域:SqlSessionFactory级别
- 存储位置:SqlSessionFactory内部
- 特点:需要手动配置开启
-
第三方缓存
- 可集成框架:EhCache、Memcache、Redis等
- 特点:提供更强大的缓存功能
- 适用场景:分布式系统、高并发场景
重要说明:
- 缓存只针对DQL语句,即只对select查询语句生效
- 增删改操作会使缓存失效
- 缓存的使用需要权衡内存占用和性能提升
14.2 一级缓存
基本概念
一级缓存是MyBatis默认开启的缓存机制,它存在于SqlSession的生命周期中。
工作原理
- 使用同一个SqlSession对象执行相同的SQL语句时,会直接使用缓存数据
- 缓存数据存储在SqlSession的HashMap中
- 当SqlSession关闭时,缓存数据会被清空
测试代码示例
@Test
public void testSelectById() throws Exception {// 获取SqlSessionFactorySqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml"));// 使用同一个SqlSession进行两次查询SqlSession sqlSession1 = sqlSessionFactory.openSession();CarMapper mapper1 = sqlSession1.getMapper(CarMapper.class);Car car1 = mapper1.selectById(83L);System.out.println(car1);CarMapper mapper2 = sqlSession1.getMapper(CarMapper.class);Car car2 = mapper2.selectById(83L);System.out.println(car2);// 使用不同的SqlSession进行查询SqlSession sqlSession2 = sqlSessionFactory.openSession();CarMapper mapper3 = sqlSession2.getMapper(CarMapper.class);Car car3 = mapper3.selectById(83L);System.out.println(car3);CarMapper mapper4 = sqlSession2.getMapper(CarMapper.class);Car car4 = mapper4.selectById(83L);System.out.println(car4);
}
一级缓存失效的情况
-
使用不同的SqlSession对象
- 每个SqlSession都有自己的一级缓存
- 不同SqlSession的缓存互不影响
-
查询条件发生变化
- SQL语句不同
- 参数值不同
-
手动清空缓存
sqlSession.clearCache();
-
执行增删改操作
- 任何insert、delete、update操作都会使一级缓存失效
- 无论操作的是哪张表,都会导致缓存失效
使用建议
- 对于频繁查询且数据变化不大的场景,建议使用一级缓存
- 对于数据实时性要求高的场景,建议关闭一级缓存
- 在事务中,合理使用一级缓存可以提高性能
14.3 二级缓存
基本概念
二级缓存是SqlSessionFactory级别的缓存,多个SqlSession可以共享二级缓存。
使用条件
-
全局配置开启缓存(默认已开启):
<setting name="cacheEnabled" value="true"/>
-
在Mapper.xml文件中添加配置:
<cache/>
-
实体类必须实现Serializable接口
public class Car implements Serializable {// 实现序列化接口 }
-
SqlSession关闭或提交后,一级缓存数据才会写入二级缓存
二级缓存失效
当两次查询之间出现任何增删改操作时:
- 二级缓存会失效
- 一级缓存也会同时失效
- 需要重新查询数据库
二级缓存配置详解
<cache eviction="LRU" flushInterval="60000" readOnly="true" size="1024"/>
配置项说明
-
eviction:缓存淘汰策略
- LRU(默认):最近最少使用,优先淘汰间隔时间内使用频率最低的对象
- FIFO:先进先出,先进入二级缓存的对象最先被淘汰
- SOFT:软引用,根据JVM垃圾回收算法淘汰对象
- WEAK:弱引用,根据JVM垃圾回收算法淘汰对象
-
flushInterval:刷新间隔(毫秒)
- 未设置时,缓存不会自动刷新,除非执行增删改操作
- 设置后,会定期刷新缓存
-
readOnly:
- true:返回共享对象,性能好但可能存在线程安全问题
- false:返回对象副本,性能一般但安全
-
size:二级缓存最大存储对象数量(默认1024)
使用建议
-
二级缓存适合:
- 查询频率高
- 数据更新频率低
- 数据量不是特别大
- 对实时性要求不高的场景
-
二级缓存不适合:
- 数据频繁更新
- 对数据实时性要求高
- 数据量特别大
- 多表关联查询复杂
14.4 MyBatis集成EhCache
为什么使用EhCache?
- 提供更强大的缓存功能
- 支持分布式缓存
- 提供更灵活的配置选项
- 性能优于MyBatis默认的二级缓存
集成步骤
- 添加依赖:
<dependency><groupId>org.mybatis.caches</groupId><artifactId>mybatis-ehcache</artifactId><version>1.2.2</version>
</dependency>
- 创建ehcache.xml配置文件:
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"updateCheck="false"><!-- 磁盘存储路径 --><diskStore path="e:/ehcache"/><!-- 默认缓存策略 --><defaultCache eternal="false" maxElementsInMemory="1000" overflowToDisk="false" diskPersistent="false"timeToIdleSeconds="0" timeToLiveSeconds="600" memoryStoreEvictionPolicy="LRU"/>
</ehcache>
- 修改Mapper.xml中的缓存配置:
<cache type="org.mybatis.caches.ehcache.EhcacheCache"/>
EhCache配置详解
-
diskStore:磁盘存储配置
- path:缓存文件存储路径
- 作用:当内存不足时,将数据存储到磁盘
-
defaultCache:默认缓存策略
-
eternal:是否永久有效
- true:缓存永久有效
- false:根据timeToIdleSeconds和timeToLiveSeconds判断有效期
-
maxElementsInMemory:内存中最大缓存对象数
-
overflowToDisk:是否缓存到磁盘
- true:内存不足时缓存到磁盘
- false:内存不足时抛出异常
-
diskPersistent:是否持久化到磁盘
- true:重启后恢复缓存
- false:重启后清空缓存
-
timeToIdleSeconds:对象空闲时间(秒)
-
0:表示一直有效
-
0:表示空闲指定时间后失效
-
-
timeToLiveSeconds:对象存活时间(秒)
-
0:表示一直有效
-
0:表示创建后指定时间失效
-
-
memoryStoreEvictionPolicy:缓存清理策略
- FIFO:先进先出
- LFU:最少使用
- LRU:最近最少使用(默认)
-
使用建议
- 根据实际需求调整缓存配置
- 合理设置缓存大小和过期时间
- 注意监控缓存命中率和内存使用情况
- 在分布式环境中注意缓存同步问题
常见问题解决
-
缓存穿透:查询不存在的数据
- 解决方案:缓存空对象或使用布隆过滤器
-
缓存雪崩:大量缓存同时失效
- 解决方案:设置不同的过期时间
-
缓存击穿:热点数据失效
- 解决方案:使用互斥锁或永不过期