MyBatis注解进阶:@One与@Many实现高效关联查询实战
1. 为什么需要One和Many注解刚开始用MyBatis的时候我最头疼的就是处理对象之间的关联关系。比如查询用户信息时顺带把用户的身份证信息或者权限列表也查出来。最早我都是手动写多个SQL分别查询然后在Java代码里拼装对象不仅代码冗长还容易出错。后来发现MyBatis提供的One和Many注解简直就像发现了新大陆。这两个注解可以帮我们实现One处理一对一关系比如用户和身份证Many处理一对多关系比如用户和权限列表实际项目中我遇到过这样一个场景电商系统的订单查询需要同时获取订单详情一对一和订单商品列表一对多。用传统方式要写3个查询方法而用注解只需要1个主查询2个嵌套查询代码量减少了40%。2. 环境准备与基础配置2.1 项目初始化我习惯用Spring Boot来快速搭建MyBatis项目。首先创建一个基础的Spring Boot项目这里我用的是2.7.x版本。pom.xml中关键依赖如下dependencies !-- MyBatis整合Spring Boot -- dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.2.2/version /dependency !-- MySQL驱动 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.28/version /dependency !-- Lombok简化代码 -- dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies提示建议使用Lombok简化实体类的getter/setter方法可以让代码更简洁2.2 数据库配置application.yml配置示例spring: datasource: url: jdbc:mysql://localhost:3306/mybatis_demo?useSSLfalseserverTimezoneUTC username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis: type-aliases-package: com.example.entity configuration: map-underscore-to-camel-case: true lazy-loading-enabled: true这里有几个关键配置需要注意map-underscore-to-camel-case开启数据库字段到Java属性的自动转换lazy-loading-enabled启用懒加载这对关联查询性能很重要3. One注解实战一对一关联3.1 业务场景分析最近做一个用户管理系统需要查询用户信息时同时获取用户的身份证信息。典型的1:1关系非常适合用One注解实现。先看数据库表设计CREATE TABLE tb_user ( user_id INT PRIMARY KEY AUTO_INCREMENT, user_name VARCHAR(50) NOT NULL ); CREATE TABLE tb_idcard ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT UNIQUE, id_number VARCHAR(18) NOT NULL, FOREIGN KEY (user_id) REFERENCES tb_user(user_id) );3.2 实体类设计使用Lombok简化代码Data public class User { private Integer userId; private String userName; private IdCard idCard; // 一对一关联 } Data public class IdCard { private Integer id; private Integer userId; private String idNumber; }3.3 Mapper接口实现关键部分来了看如何在Mapper中使用One注解Mapper public interface UserMapper { Select(SELECT * FROM tb_user WHERE user_id #{userId}) Results(id userMap, value { Result(property userId, column user_id), Result(property userName, column user_name), Result(property idCard, column user_id, one One(select getByIdCardUserId, fetchType FetchType.LAZY)) }) User getUserWithIdCard(Integer userId); Select(SELECT * FROM tb_idcard WHERE user_id #{userId}) IdCard getByIdCardUserId(Integer userId); }这里有几个技术细节需要注意Result中的property对应实体类属性名column指定将哪个字段值传递给嵌套查询one One定义一对一关联fetchType建议使用LAZY懒加载3.4 踩坑经验分享第一次用One时我遇到过N1查询问题。比如查询10个用户如果不注意会执行1次用户查询10次身份证查询。解决方案是合理使用二级缓存必要时改用JOIN查询批量查询场景考虑使用Many的变通方案4. Many注解实战一对多关联4.1 典型业务场景继续上面的用户系统现在需要查询用户及其所有权限1:N关系。数据库设计CREATE TABLE tb_role ( role_id INT PRIMARY KEY AUTO_INCREMENT, user_id INT, role_name VARCHAR(50), FOREIGN KEY (user_id) REFERENCES tb_user(user_id) );4.2 实体类调整Data public class User { private Integer userId; private String userName; private IdCard idCard; private ListRole roles; // 一对多关联 } Data public class Role { private Integer roleId; private Integer userId; private String roleName; }4.3 Mapper实现Mapper public interface UserMapper { Select(SELECT * FROM tb_user WHERE user_id #{userId}) Results({ Result(property userId, column user_id), Result(property roles, column user_id, many Many(select getRolesByUserId)) }) User getUserWithRoles(Integer userId); Select(SELECT * FROM tb_role WHERE user_id #{userId}) ListRole getRolesByUserId(Integer userId); }与One的主要区别使用many Many替代one One返回类型是集合List而非单个对象同样支持fetchType参数控制加载方式4.4 性能优化技巧在大数据量场景下我总结了几点优化经验对分页查询先分页获取主表ID再批量查询关联数据使用Options注解设置fetchSize复杂场景考虑使用ResultMap的继承特性5. 高级应用与最佳实践5.1 延迟加载的陷阱与解决方案MyBatis的延迟加载Lazy Loading是把双刃剑。我曾在生产环境遇到过因为session关闭导致的LazyInitializationException。解决方案有使用OpenSessionInView模式在Service层主动初始化需要的数据配置aggressiveLazyLoadingfalse5.2 嵌套查询 vs JOIN查询经过多次性能测试我得出的经验法则数据量小、关联简单用嵌套查询One/Many数据量大、关联复杂用JOIN查询ResultMapJOIN查询示例Select(SELECT u.*, r.* FROM tb_user u LEFT JOIN tb_role r ON u.user_id r.user_id WHERE u.user_id #{userId}) Results({ Result(property userId, column user_id), Result(property roles, javaType List.class, column user_id, many Many(resultMap roleMap)) }) User getUserWithRolesJoin(Integer userId);5.3 复杂嵌套场景对于多层嵌套如订单-订单项-商品我推荐使用定义可重用的ResultMap合理使用ResultMap引用考虑使用XML配置方式提高可读性ResultMap(userWithRolesAndCards) Select(...复杂SQL...) User getComplexUser(Integer userId);6. 常见问题排查指南在实际项目中我遇到过这些典型问题问题1关联数据没加载出来检查Result的column是否匹配检查嵌套查询方法是否存在查看MyBatis日志确认SQL执行情况问题2性能突然变慢检查是否意外触发了EAGER加载使用Arthas等工具分析SQL执行情况考虑添加二级缓存问题3循环引用导致栈溢出使用JsonIgnore注解打断循环自定义DTO代替实体类返回配置合适的toString()方法7. 测试与验证编写测试用例时我习惯用这种结构SpringBootTest class UserMapperTest { Autowired private UserMapper userMapper; Test void testGetUserWithIdCard() { User user userMapper.getUserWithIdCard(1); assertNotNull(user.getIdCard()); assertEquals(123456, user.getIdCard().getIdNumber()); } Test void testGetUserWithRoles() { User user userMapper.getUserWithRoles(1); assertEquals(3, user.getRoles().size()); } }测试要点验证主对象非空验证关联对象非空验证关联数据正确性对懒加载场景单独测试8. 延伸思考经过多个项目实践我发现One和Many注解虽然方便但也不是银弹。在微服务架构下有时候直接调用其他服务获取关联数据可能是更好的选择。具体如何选择需要根据业务场景、数据量、性能要求等因素综合考虑。对于简单的CRUD应用使用注解开发效率很高。但对于复杂查询特别是需要多表关联、动态SQL的场景我仍然推荐使用XML配置方式因为它的表达能力更强也更易于维护。