1. MyBatis多表查询实战指南
作为一名使用MyBatis多年的开发者,我深知在实际项目中单表查询往往无法满足复杂业务需求。当系统需要处理关联表查询时,掌握MyBatis的多表映射技巧就显得尤为重要。本文将深入探讨MyBatis中一对一、一对多和多对多关系的实现方式,并分享性能优化的延迟加载策略。
2. 多表联合查询基础
2.1 多表查询的必要性
在真实业务场景中,数据往往分散在多个表中。例如电商系统中的订单与用户信息、学生选课系统中的学生与课程关系等。MyBatis提供了强大的结果映射功能,能够优雅地处理这些关联关系。
2.2 关联关系类型
MyBatis支持四种主要的关联关系映射:
- 一对一(如订单与用户)
- 一对多(如用户与订单)
- 多对一(订单对用户,实际与一对一相同)
- 多对多(如学生与课程)
3. 一对一关系实现
3.1 数据准备
我们以订单和用户为例,首先创建相关表结构:
CREATE TABLE `t_user` ( `id` int NOT NULL AUTO_INCREMENT COMMENT '编号id', `username` varchar(50) DEFAULT NULL COMMENT '用户名', `password` varchar(50) DEFAULT NULL COMMENT '密码', `age` int DEFAULT NULL COMMENT '年龄', `sex` varchar(2) DEFAULT NULL COMMENT '性别', `email` varchar(50) DEFAULT NULL COMMENT '邮箱', PRIMARY KEY (`id`) ); CREATE TABLE `orders` ( `id` int NOT NULL AUTO_INCREMENT, `user_id` int DEFAULT NULL COMMENT '用户ID', `number` varchar(50) DEFAULT NULL COMMENT '订单编号', `create_time` datetime DEFAULT NULL COMMENT '创建时间', `note` varchar(100) DEFAULT NULL COMMENT '备注', PRIMARY KEY (`id`), KEY `user_id` (`user_id`) );3.2 嵌套结果方式
3.2.1 实体类设计
在Order类中添加User属性:
public class Order { private Integer id; private Integer userId; private String number; private String createTime; private String note; // 一对一关系 private User user; // getters and setters }3.2.2 Mapper接口定义
public interface OrderMapper { List<Order> queryOrderUserResultMap(); }3.2.3 XML映射配置
<select id="queryOrderUserResultMap" resultMap="orderUserResultMap"> SELECT o.id oid, o.user_id, o.number, o.create_time, o.note, u.id uid, u.username, u.password, u.age, u.sex, u.email FROM `orders` o, `t_user` u WHERE o.user_id = u.id </select> <resultMap id="orderUserResultMap" type="order"> <id column="oid" property="id"/> <result column="user_id" property="userId"/> <result column="number" property="number"/> <result column="create_time" property="createTime"/> <result column="note" property="note"/> <association property="user" javaType="com.sy.pojo.User"> <id column="uid" property="id"/> <result column="username" property="username"/> <result column="password" property="password"/> <result column="age" property="age"/> <result column="sex" property="sex"/> <result column="email" property="email"/> </association> </resultMap>3.3 嵌套查询方式
3.3.1 Mapper接口
public interface OrderMapper { List<Order> queryOrderUserResultMap02(); } public interface UserMapper { User findUserById(int id); }3.3.2 XML映射配置
<!-- OrderMapper.xml --> <select id="queryOrderUserResultMap02" resultMap="orderUserResultMap02"> SELECT * FROM `orders` </select> <resultMap id="orderUserResultMap02" type="order"> <id column="id" property="id"/> <result column="user_id" property="userId"/> <result column="number" property="number"/> <result column="create_time" property="createTime"/> <result column="note" property="note"/> <association property="user" javaType="user" select="com.sy.mapper.UserMapper.findUserById" column="user_id"> </association> </resultMap> <!-- UserMapper.xml --> <select id="findUserById" parameterType="int" resultType="user"> SELECT * FROM `t_user` WHERE id = #{id} </select>4. 一对多关系实现
4.1 嵌套结果方式
4.1.1 实体类设计
在User类中添加订单集合:
public class User { private int id; private String username; // 其他字段... // 一对多关系 private List<Order> orderList; // getters and setters }4.1.2 Mapper接口
public interface UserMapper { List<User> findUserOrders(); }4.1.3 XML映射配置
<select id="findUserOrders" resultMap="userResultMap"> SELECT u.id uid, u.username, u.password, u.age, u.sex, u.email, o.id oid, o.user_id, o.number, o.create_time, o.note FROM t_user u, orders o WHERE u.id = o.user_id </select> <resultMap id="userResultMap" type="user"> <id column="uid" property="id"/> <result column="username" property="username"/> <!-- 其他用户字段映射 --> <collection property="orderList" ofType="order"> <id column="oid" property="id"/> <result column="user_id" property="userId"/> <result column="number" property="number"/> <result column="create_time" property="createTime"/> <result column="note" property="note"/> </collection> </resultMap>4.2 嵌套查询方式
4.2.1 Mapper接口
public interface UserMapper { List<User> findUserOrders02(); } public interface OrderMapper { List<Order> findOrderByUid(int uid); }4.2.2 XML映射配置
<!-- UserMapper.xml --> <select id="findUserOrders02" resultMap="userResultMap02"> SELECT * FROM t_user </select> <resultMap id="userResultMap02" type="user"> <id column="id" property="id"/> <!-- 其他用户字段映射 --> <collection property="orderList" ofType="order" select="com.sy.mapper.OrderMapper.findOrderByUid" column="id"> </collection> </resultMap> <!-- OrderMapper.xml --> <select id="findOrderByUid" parameterType="int" resultType="order"> SELECT * FROM orders WHERE user_id = #{uid} </select>5. 多对多关系实现
5.1 数据准备
创建学生、课程及中间表:
CREATE TABLE `student` ( `sid` int NOT NULL AUTO_INCREMENT, `name` varchar(50) DEFAULT NULL, PRIMARY KEY (`sid`) ); CREATE TABLE `course` ( `cid` int NOT NULL AUTO_INCREMENT, `cname` varchar(50) DEFAULT NULL, PRIMARY KEY (`cid`) ); CREATE TABLE `student_course` ( `id` int NOT NULL AUTO_INCREMENT, `sid` int DEFAULT NULL, `cid` int DEFAULT NULL, PRIMARY KEY (`id`), KEY `fk_sid` (`sid`), KEY `fk_cid` (`cid`), CONSTRAINT `fk_cid` FOREIGN KEY (`cid`) REFERENCES `course` (`cid`), CONSTRAINT `fk_sid` FOREIGN KEY (`sid`) REFERENCES `student` (`sid`) );5.2 嵌套结果方式
5.2.1 实体类设计
public class Student { private Integer sid; private String name; private List<Course> courseList; // getters and setters } public class Course { private Integer cid; private String cname; private List<Student> studentList; // getters and setters }5.2.2 Mapper接口
public interface StudentMapper { List<Student> findStudentCourses(); }5.2.3 XML映射配置
<select id="findStudentCourses" resultMap="studentResultMap"> SELECT stu.sid, stu.name, c.cid, c.cname FROM student stu LEFT JOIN student_course sc ON stu.sid = sc.sid LEFT JOIN course c ON sc.cid = c.cid </select> <resultMap id="studentResultMap" type="student"> <id column="sid" property="sid"/> <result column="name" property="name"/> <collection property="courseList" ofType="course"> <id column="cid" property="cid"/> <result column="cname" property="cname"/> </collection> </resultMap>6. 延迟加载策略
6.1 延迟加载原理
延迟加载的核心思想是"按需加载",只有在真正需要使用关联数据时才执行查询,避免不必要的数据库访问。
6.2 配置方式
6.2.1 全局配置
在mybatis-config.xml中:
<settings> <!-- 开启延迟加载 --> <setting name="lazyLoadingEnabled" value="true"/> <!-- 禁用积极加载 --> <setting name="aggressiveLazyLoading" value="false"/> </settings>6.2.2 局部配置
在特定的association或collection标签中:
<association property="user" javaType="user" select="com.sy.mapper.UserMapper.findUserById" column="user_id" fetchType="lazy"> </association>6.3 使用建议
- 对于频繁访问的关联数据,不建议使用延迟加载
- 对于大数据量或不常用的关联数据,推荐使用延迟加载
- 注意N+1查询问题,必要时可以使用批量加载优化
7. MyBatis缓存机制
7.1 一级缓存
- 默认开启,作用域为SqlSession
- 同一会话中相同的查询会直接从缓存返回
- 执行增删改操作或调用clearCache()会清空缓存
7.2 二级缓存
- 需要手动配置,作用域为Mapper级别
- 实体类需要实现Serializable接口
- 在Mapper.xml中添加 标签启用
<cache eviction="LRU" flushInterval="60000" size="512" readOnly="true"/>7.3 缓存使用建议
- 查询多更新少的数据适合使用缓存
- 实时性要求高的数据慎用缓存
- 注意缓存一致性问题的处理
8. 实战经验分享
在实际项目中使用MyBatis处理多表关系时,我总结了以下几点经验:
关联选择:对于简单的一对一或一对多关系,优先考虑使用嵌套结果方式,减少SQL查询次数;对于复杂的多对多关系,嵌套查询方式可能更灵活。
性能优化:当处理大量数据时,要注意N+1查询问题。我曾遇到一个场景,使用嵌套查询方式处理1000条主记录,每条主记录有5条关联记录,结果产生了1001次查询!解决方案是使用批量加载或改为嵌套结果方式。
延迟加载陷阱:延迟加载在Web应用中要特别注意会话关闭问题。典型的错误是在Controller层获取了主对象,然后在视图层尝试访问延迟加载的属性,此时会话已关闭会导致异常。解决方法包括:
- 使用Open Session In View模式
- 在Service层预先加载需要的关联数据
- 使用DTO代替实体直接返回
缓存策略:对于配置类等不常变化的数据,二级缓存效果显著。但要注意及时清理缓存,我曾遇到因为缓存导致配置修改后不生效的问题,后来增加了缓存刷新机制。
复杂查询处理:对于特别复杂的多表关联查询,有时直接使用SQL视图或存储过程,然后在MyBatis中简单映射,比在MyBatis中处理复杂的关联关系更高效。
结果映射技巧:当字段名和属性名不一致时,除了使用resultMap,还可以使用SQL别名简化映射。例如:
SELECT u.id as userId, u.name as userName FROM user u这样即使不定义resultMap也能正确映射。
分页优化:在多表关联查询中使用分页时,要注意不同数据库的分页语法差异。MySQL的LIMIT在复杂关联查询中可能导致性能问题,有时需要先分页再关联。
通过合理运用这些技巧,MyBatis能够高效处理各种复杂的数据关联场景,为应用程序提供灵活且高性能的数据访问层解决方案。