MyBatis 缓存策略与多表查询实现详解
一、MyBatis 两级缓存架构
缓存的核心价值在于降低数据库访问频次,MyBatis 以 SQL 语句的 namespace + id 作为缓存键值。当多次执行同一标识的查询时,可直接命中缓存。
1.1 一级缓存(会话级)
一级缓存与 SqlSession 绑定,默认启用。其生命周期随会话创建而开始,随会话关闭而终结。同一会话内重复查询将直接读取内存数据。
public void demoFirstLevelCache() {
// 初始化查询条件
User criteria = new User();
criteria.setUsername("李清照");
// 首次查询,访问数据库
User result1 = userDao.selectByName(criteria);
System.out.println(result1);
// 同一 SqlSession 内再次查询,命中一级缓存
User criteria2 = new User();
criteria2.setUsername("李清照");
User result2 = userDao.selectByName(criteria2);
System.out.println(result2);
// 手动清空缓存:session.clearCache();
}
1.2 二级缓存(应用级)
二级缓存作用于 SqlSessionFactory 层面,支持跨会话共享。需显式开启:
全局配置(mybatis-config.xml):
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
映射文件配置:
<!-- 在 Mapper.xml 头部添加 -->
<cache eviction="LRU" flushInterval="60000" size="512" readOnly="true"/>
public void demoSecondLevelCache() {
SqlSession session1 = factory.openSession();
UserMapper mapper1 = session1.getMapper(UserMapper.class);
User param = new User();
param.setUsername("辛弃疾");
User data = mapper1.selectByName(param);
session1.close(); // 数据刷入二级缓存
SqlSession session2 = factory.openSession();
UserMapper mapper2 = session2.getMapper(UserMapper.class);
// 新会话直接命中二级缓存,无需访问数据库
User cached = mapper2.selectByName(param);
}
二、字段映射不一致的解决方案
2.1 别名映射(简易方案)
SELECT emp_id AS employeeId, dept_code AS departmentCode FROM t_employee
2.2 自定义 ResultMap(推荐方案)
<resultMap id="employeeMapping" type="com.example.domain.Employee">
<id column="emp_id" property="employeeId"/>
<result column="emp_name" property="employeeName"/>
<result column="dept_code" property="deptCode"/>
<result column="salary_amount" property="salary"/>
</resultMap>
<select id="selectById" resultMap="employeeMapping">
SELECT emp_id, emp_name, dept_code, salary_amount
FROM t_employee
WHERE emp_id = #{id}
</select>
三、多表关联查询实现模式
以员工(Employee)与部门(Department)为例,展示三种关联查询策略。
3.1 业务层装配模式
Mapper 层保持单表查询,Service 层负责数据组装。
// EmployeeMapper.xml
<select id="selectAll" resultType="Employee">
SELECT * FROM t_employee
</select>
// DepartmentMapper.xml
<select id="selectByCode" resultType="Department">
SELECT * FROM t_department WHERE dept_code = #{code}
</select>
// Service 层装配
public List<Employee> getEmployeeWithDept() {
List<Employee> employees = employeeMapper.selectAll();
for (Employee emp : employees) {
Department dept = deptMapper.selectByCode(emp.getDeptCode());
emp.setDepartment(dept);
}
return employees;
}
3.2 N+1 查询模式(association)
利用 MyBatis 延迟加载机制,自动触发关联查询。
<resultMap id="employeeWithDept" type="Employee">
<id property="employeeId" column="emp_id"/>
<result property="employeeName" column="emp_name"/>
<result property="deptCode" column="dept_code"/>
<association property="department"
javaType="Department"
column="dept_code"
select="com.example.mapper.DepartmentMapper.selectByCode"/>
</resultMap>
<select id="selectById" resultMap="employeeWithDept">
SELECT emp_id, emp_name, dept_code FROM t_employee WHERE emp_id = #{id}
</select>
3.3 嵌套结果映射模式(多对一)
单次 SQL 联表查询,通过 resultMap 完成结果映射。
<resultMap id="employeeNestedResult" type="Employee">
<id property="employeeId" column="e_id"/>
<result property="employeeName" column="e_name"/>
<association property="department" javaType="Department">
<id property="deptCode" column="d_code"/>
<result property="deptName" column="d_name"/>
<result property="location" column="d_loc"/>
</association>
</resultMap>
<select id="selectWithDept" resultMap="employeeNestedResult">
SELECT
e.emp_id as e_id,
e.emp_name as e_name,
d.dept_code as d_code,
d.dept_name as d_name,
d.location as d_loc
FROM t_employee e
LEFT JOIN t_department d ON e.dept_code = d.dept_code
WHERE e.emp_id = #{id}
</select>
3.4 集合映射模式(一对多)
N+1 方式:
<resultMap id="deptWithEmployees" type="Department">
<id property="deptCode" column="dept_code"/>
<collection property="employees"
ofType="Employee"
column="dept_code"
select="com.example.mapper.EmployeeMapper.selectByDeptCode"/>
</resultMap>
嵌套结果方式:
<resultMap id="deptNestedCollection" type="Department">
<id property="deptCode" column="d_code"/>
<result property="deptName" column="d_name"/>
<collection property="employees" ofType="Employee">
<id property="employeeId" column="e_id"/>
<result property="employeeName" column="e_name"/>
<result property="hireDate" column="e_hire"/>
</collection>
</resultMap>
<select id="selectWithEmployees" resultMap="deptNestedCollection">
SELECT
d.dept_code as d_code,
d.dept_name as d_name,
e.emp_id as e_id,
e.emp_name as e_name,
e.hire_date as e_hire
FROM t_department d
LEFT JOIN t_employee e ON d.dept_code = e.dept_code
WHERE d.dept_code = #{code}
</select>
3.5 自动映射别名技巧
利用 MyBatis 自动映射特性,通过别名直接映射嵌套属性。
<select id="selectAutoMapping" resultType="Employee">
SELECT
e.emp_id,
e.emp_name,
e.hire_date,
d.dept_code AS "department.deptCode",
d.dept_name AS "department.deptName",
d.location AS "department.location"
FROM t_employee e
JOIN t_department d ON e.dept_code = d.dept_code
WHERE e.emp_id = #{id}
</select>
四、注解式开发
MyBatis 3.0+ 支持注解替代 XML 配置,简化单表操作。
public interface EmployeeMapper {
@Select("SELECT * FROM t_employee WHERE emp_id = #{id}")
@Results({
@Result(property = "employeeId", column = "emp_id", id = true),
@Result(property = "employeeName", column = "emp_name"),
@Result(property = "department",
column = "dept_code",
one = @One(select = "com.example.mapper.DepartmentMapper.selectByCode"))
})
Employee selectByIdWithDept(Integer id);
@Insert("INSERT INTO t_employee(emp_name, dept_code) VALUES(#{employeeName}, #{deptCode})")
@Options(useGeneratedKeys = true, keyProperty = "employeeId")
int insert(Employee record);
@Update("UPDATE t_employee SET emp_name=#{employeeName}, dept_code=#{deptCode} WHERE emp_id=#{employeeId}")
int update(Employee record);
@Delete("DELETE FROM t_employee WHERE emp_id = #{id}")
int deleteById(Integer id);
}