深入掌握MyBatis:连接池、动态SQL、多表查询与缓存
文章目录
- 一、MyBatis连接池
- 1.1 连接池的作用
- 1.2 MyBatis连接池分类
- 二、动态SQL
- 2.1 if标签
- 2.2 where标签
- 2.3 foreach标签
- 2.4 SQL片段复用
- 三、多表查询
- 3.1 多对一查询(一对一)
- 3.2 一对多查询
- 四、延迟加载
- 4.1 立即加载 vs 延迟加载
- 4.2 配置延迟加载
- 五、MyBatis缓存
- 5.1 一级缓存
- 5.2 二级缓存
- 5.3 一级缓存 vs 二级缓存:核心差异
- 5.4 缓存使用陷阱与最佳实践**
- 总结
一、MyBatis连接池
1.1 连接池的作用
- 什么是连接池:存储数据库连接的容器,避免频繁创建和销毁连接。
- 解决的问题:每次执行SQL都创建连接会浪费资源,连接池复用连接提升性能。
1.2 MyBatis连接池分类
类型 | 描述 |
---|---|
POOLED | 使用连接池(默认) |
UNPOOLED | 不使用连接池(适合简单场景) |
JNDI | 通过JNDI获取外部连接池 |
配置示例:
<dataSource type="POOLED"><property name="driver" value="com.mysql.jdbc.Driver"/><property name="url" value="jdbc:mysql://localhost:3306/test"/><property name="username" value="root"/>
</dataSource>
二、动态SQL
2.1 if标签
动态拼接查询条件,避免手动拼接SQL字符串。
<select id="findByWhere" resultType="User">SELECT * FROM user<where><if test="username != null">AND username LIKE #{username}</if><if test="sex != null">AND sex = #{sex}</if></where>
</select>
2.2 where标签
自动处理WHERE
后的AND/OR
冗余,替代WHERE 1=1
。
<where><if test="username != null">username LIKE #{username}</if>
</where>
2.3 foreach标签
遍历集合生成条件,支持IN
或OR
拼接。
<!-- IN (1,2,3) -->
<foreach collection="ids" item="i" open="id IN (" separator="," close=")">#{i}
</foreach><!-- OR id=1 -->
<foreach collection="ids" item="i" open="id=" separator=" OR id=">#{i}
</foreach>
2.4 SQL片段复用
提取公共SQL片段,减少重复代码。
<sql id="baseSelect">SELECT * FROM user</sql>
<select id="findAll"><include refid="baseSelect"/>
</select>
三、多表查询
3.1 多对一查询(一对一)
需求:查询账户信息并关联用户信息。
JavaBean:
public class Account {private Integer id;private Double money;private User user; // 关联用户
}
XML配置:
<resultMap id="accountMap" type="Account"><id property="id" column="id"/><association property="user" javaType="User"><result property="username" column="username"/></association>
</resultMap><select id="findAll" resultMap="accountMap">SELECT a.*, u.username FROM account a JOIN user u ON a.uid = u.id
</select>
3.2 一对多查询
需求:查询用户及其所有账户。
JavaBean:
public class User {private List<Account> accounts; // 用户拥有多个账户
}
XML配置:
<resultMap id="userMap" type="User"><collection property="accounts" ofType="Account"><result property="money" column="money"/></collection>
</resultMap><select id="findOneToMany" resultMap="userMap">SELECT u.*, a.money FROM user u LEFT JOIN account a ON u.id = a.uid
</select>
四、延迟加载
延迟加载,也叫懒加载,简单来说就是在真正需要数据的时候才去加载数据,而不是在一开始就把所有关联数据都加载出来。这可以有效减少资源的浪费,尤其是在处理复杂对象关系时,避免一次性加载大量无用数据。
4.1 立即加载 vs 延迟加载
- 立即加载:查询主对象时,直接加载关联对象(默认)。
- 延迟加载:按需加载关联对象,提升性能。
4.2 配置延迟加载
开启全局延迟加载:
<settings><setting name="lazyLoadingEnabled" value="true"/><setting name="aggressiveLazyLoading" value="false"/>
</settings>
一对一延迟加载示例:
假设有 User
和 Address
两个实体,一个用户对应一个地址。在传统的查询方式中,可能会一次性将用户及其地址信息都查询出来。但在延迟加载模式下,查询用户时并不会立即查询地址信息。
然后在 User
的映射文件中配置:
<resultMap id="userMap" type="User"><id column="user_id" property="id"/><result column="username" property="username"/><association property="address" select="cn.tx.mapper.AddressMapper.findAddressByUserId" column="user_id" fetchType="lazy"/>
</resultMap>
这样,当查询用户时,只有在访问 user.getAddress()
时才会去查询地址信息。
多对一延迟加载示例:
比如多个订单对应一个用户,在查询订单时,用户信息可以延迟加载。在 Order
的映射文件中配置:
<resultMap id="orderMap" type="Order"><id column="order_id" property="id"/><result column="order_no" property="orderNo"/><association property="user" select="cn.tx.mapper.UserMapper.findUserByOrderId" column="user_id" fetchType="lazy"/>
</resultMap>
当访问 order.getUser()
时才会加载用户信息。
一对多延迟加载示例:
在 User
的映射文件中配置如下:
<resultMap id="userMap" type="User"><id column="user_id" property="id"/><result column="username" property="username"/><collection property="orders" select="cn.tx.mapper.OrderMapper.findOrdersByUserId" column="user_id" fetchType="lazy"/>
</resultMap>
只有在调用 user.getOrders()
时,才会执行查询订单的SQL语句。
多对多延迟加载示例:
假设 User
和 Role
是多对多关系,需要通过中间表 user_role
来关联。
首先在 User
的映射文件中配置:
<resultMap id="userMap" type="User"><id column="user_id" property="id"/><result column="username" property="username"/><collection property="roles" select="cn.tx.mapper.RoleMapper.findRolesByUserId" column="user_id" fetchType="lazy"/>
</resultMap>
在 Role
的映射文件中也做类似配置:
<resultMap id="roleMap" type="Role"><id column="role_id" property="id"/><result column="role_name" property="roleName"/><collection property="users" select="cn.tx.mapper.UserMapper.findUsersByRoleId" column="role_id" fetchType="lazy"/>
</resultMap>
这样在查询用户或角色时,相关联的多对多关系数据只有在实际访问时才会加载。
五、MyBatis缓存
5.1 一级缓存
1. 定义与特性
- 作用范围:
SqlSession
级别,默认开启。 - 生命周期:与
SqlSession
绑定,Session 关闭或执行commit()
/rollback()
时清空。 - 工作机制:同一个 Session 内多次执行 相同查询,优先从缓存读取。
SqlSession session = sqlSessionFactory.openSession();
UserMapper mapper = session.getMapper(UserMapper.class);
User user1 = mapper.findById(1); // 第一次查询,从数据库获取数据并放入一级缓存
User user2 = mapper.findById(1); // 第二次查询,从一级缓存中获取数据
2. 缓存命中与失效
- 命中条件:相同的
SQL
+ 相同的参数 + 相同的环境。 - 失效场景:
- 执行
INSERT/UPDATE/DELETE
操作。 - 手动调用
sqlSession.clearCache()
。 - 跨
SqlSession
的查询不会共享缓存。
- 执行
3. 实战注意点
- 坑点:跨方法调用时,若使用不同
SqlSession
,一级缓存不共享。 - 适用场景:短生命周期操作(如单次请求内的重复查询)。
5.2 二级缓存
1. 定义与特性
- 作用范围:
Mapper
级别(跨SqlSession
),需手动开启。 - 生命周期:与应用进程绑定,重启或调用
clear()
时清空。 - 存储结构:序列化后的数据(需实体类实现
Serializable
)。
2. 缓存策略与配置
- 开启步骤:
<!-- MyBatis 全局配置 --> <settings><setting name="cacheEnabled" value="true"/> </settings><!-- Mapper XML 中声明 --> <mapper namespace="..."><cache eviction="LRU" flushInterval="60000" size="512"/> </mapper>
- 淘汰策略(
eviction
):LRU
:最近最少使用(默认)。FIFO
:先进先出。SOFT
:软引用,基于垃圾回收器状态回收。
3. 工作流程
- 查询顺序:二级缓存 → 一级缓存 → 数据库。
- 数据提交:
SqlSession
关闭时,一级缓存数据同步到二级缓存。
5.3 一级缓存 vs 二级缓存:核心差异
维度 | 一级缓存 | 二级缓存 |
---|---|---|
作用范围 | SqlSession 内 | 跨 SqlSession (同一 Mapper) |
默认状态 | 开启 | 需手动配置 |
存储位置 | 内存(JVM 堆) | 可配置为磁盘或第三方缓存(如 Redis) |
数据共享 | 不共享 | 多个 Session 共享 |
生命周期 | 随 Session 销毁 | 长期存在,需主动管理 |
5.4 缓存使用陷阱与最佳实践**
1. 常见问题
- 脏读风险:跨服务节点时,二级缓存可能导致数据不一致。
- 序列化开销:二级缓存序列化影响性能,需权衡缓存粒度。
- 缓存穿透:频繁查询不存在的数据,需设置空值缓存。
2. 优化建议
- 合理配置:根据数据更新频率选择缓存策略(如
flushInterval
)。 - 第三方缓存:集成 Redis 或 Ehcache 实现分布式缓存。
- 注解控制:使用
@CacheNamespace
和@CacheNamespaceRef
精细化管理。
总结
掌握这些核心技能,可以高效利用MyBatis构建健壮的数据库应用。实际开发中,需根据业务场景选择合适策略,平衡性能与功能需求。