Java-10 深入浅出 MyBatis 一对多与多对多查询配置详解

Java-10 深入浅出 MyBatis 注解模式:一对多与多对多查询配置详解

TL;DR

本文讲解 MyBatis 注解模式下如何实现一对多和多对多关系查询。

核心结论:

  • 一对多查询可以通过 @Select@Results@Result@Many 实现。
  • 多对多查询通常需要通过中间表完成关联,再结合 @Many 完成嵌套结果映射。
  • 注解模式适合简单关系查询和教学演示,复杂 SQL、动态 SQL、性能敏感场景更推荐使用 XML。
  • 使用 @Many 时要重点注意字段映射、Mapper 方法路径、外键条件以及 N+1 查询问题。

本文会分别完成:

  • User -> Order 一对多查询
  • User -> Role 多对多查询
  • Mapper 注解配置
  • 测试调用代码
  • 常见错误排查
  • 注解模式优缺点总结

一、版本与环境说明

功能 版本/环境 状态 说明
MyBatis 注解查询 3.5.x 已验证 使用 @Select@Results@Many
数据库 MySQL 8.0 已验证 示例表包括用户表、订单表、角色表、中间表
一对多查询 User -> Order 已验证 通过 uid 外键关联
多对多查询 User -> Role 已验证 通过 user_role 中间表关联
N+1 查询问题 存在 需要注意 每条用户记录会额外触发一次子查询
资源释放 必须处理 已优化 推荐使用 try-with-resources 自动关闭 SqlSession

二、MyBatis 注解模式关系查询的核心思路

MyBatis 中的关系查询,本质是把数据库中的表关系映射到 Java 对象关系中。

例如:

  • 一个用户有多个订单,对应 Java 中的 List<WzkOrder> orderList
  • 一个用户有多个角色,对应 Java 中的 List<WzkRole> roleList

使用注解模式时,核心注解包括:

java 复制代码
@Select
@Results
@Result
@Many

其中:

  • @Select:定义当前 Mapper 方法执行的 SQL。
  • @Results:定义结果映射规则。
  • @Result:定义单个字段和 Java 属性之间的映射关系。
  • @Many:定义一对多或多对多场景下的子查询方法。

基本结构如下:

java 复制代码
@Select("select * from wzk_user")
@Results({
    @Result(id = true, property = "id", column = "id"),
    @Result(property = "username", column = "username"),
    @Result(property = "orderList", column = "id",
            javaType = List.class,
            many = @Many(select = "完整Mapper路径.方法名"))
})
List<WzkUser> findAll();

这里最关键的是:

java 复制代码
@Result(property = "orderList", column = "id", many = @Many(...))

含义是:

把用户表中的 id 作为参数,传递给 @Many 指定的子查询方法,然后把子查询结果封装到 orderList 属性中。

三、一对多查询:User 查询 Order

1. 查询模型

用户表和订单表是一对多关系。

业务含义是:

一个用户可以拥有多个订单,一个订单只属于一个用户。

查询目标是:

查询所有用户,并同时查询每个用户对应的订单列表。

数据库关系可以理解为:

text 复制代码
wzk_user.id = wzk_orders.uid

Java 对象关系可以理解为:

java 复制代码
public class WzkUser {
    private Integer id;
    private String username;
    private String password;
    private Date birthday;
    private List<WzkOrder> orderList;
    private List<WzkRole> roleList;
}

四、一对多查询代码实现

1. UserMapper 配置

UserMapper 中新增方法:

java 复制代码
@Select("select * from wzk_user")
@Results({
        @Result(id = true, property = "id", column = "id"),
        @Result(property = "username", column = "username"),
        @Result(property = "password", column = "password"),
        @Result(property = "birthday", column = "birthday"),
        @Result(property = "orderList", column = "id",
                javaType = List.class,
                many = @Many(select = "icu.wzk.mapper.OrderMapper.findByUserIdWithAnnotation"))
})
List<WzkUser> findAllUserAndOrderWithAnnotation();

这里需要注意一个关键点:

java 复制代码
@Result(property = "orderList", column = "id")

property = "orderList" 表示查询出来的订单集合要封装到 WzkUserorderList 属性中。

column = "id" 表示把当前用户的 id 传递给子查询方法。

也就是说,MyBatis 会先执行:

sql 复制代码
select * from wzk_user

然后对每一个用户,再调用:

java 复制代码
OrderMapper.findByUserIdWithAnnotation(userId)

2. OrderMapper 配置

OrderMapper 中新增根据用户 ID 查询订单的方法:

java 复制代码
public interface OrderMapper {

    @Select("select * from wzk_orders where uid = #{uid}")
    List<WzkOrder> findByUserIdWithAnnotation(int uid);

}

这里必须注意:

sql 复制代码
where uid = #{uid}

不能写成:

sql 复制代码
where id = #{id}

因为 id 是订单表自己的主键,而 uid 才是订单表中关联用户表的外键。

如果写成 where id = #{id},表面上可能也能查出数据,但语义是错的。数据量稍微变化后,结果就会不准确。

五、一对多查询测试代码

推荐使用 try-with-resources 自动关闭 SqlSession

java 复制代码
public class WzkIcu12 {

    public static void main(String[] args) throws IOException {
        InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");

        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder()
                .build(resourceAsStream);

        try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
            UserMapper userMapper = sqlSession.getMapper(UserMapper.class);

            List<WzkUser> dataList = userMapper.findAllUserAndOrderWithAnnotation();

            dataList.forEach(System.out::println);
        }
    }
}

测试输出示例:

shell 复制代码
WzkUser(id=1, username=wzk, password=icu, birthday=Mon Nov 11 00:00:00 CST 2024, orderList=[WzkOrder(id=1, ordertime=Mon Nov 11 00:00:00 CST 2024, total=100.0, user=null)], roleList=null)

WzkUser(id=2, username=wzk2, password=icu2, birthday=Mon Nov 11 00:00:00 CST 2024, orderList=[WzkOrder(id=2, ordertime=Mon Nov 11 00:00:00 CST 2024, total=200.0, user=null)], roleList=null)

六、多对多查询:User 查询 Role

1. 查询模型

用户表和角色表是多对多关系。

业务含义是:

一个用户可以拥有多个角色,一个角色也可以被多个用户使用。

例如:

  • 用户 A 可以拥有 ADMINUSER 两个角色。
  • ADMIN 角色也可以分配给多个用户。

这种关系不能只靠用户表和角色表直接表示,通常需要一张中间表。

数据库关系可以理解为:

text 复制代码
wzk_user.id = wzk_user_role.user_id
wzk_role.id = wzk_user_role.role_id

Java 对象关系可以理解为:

java 复制代码
public class WzkUser {
    private Integer id;
    private String username;
    private String password;
    private Date birthday;
    private List<WzkOrder> orderList;
    private List<WzkRole> roleList;
}

七、多对多查询代码实现

1. UserMapper 配置

UserMapper 中新增查询用户及角色的方法:

java 复制代码
@Select("select * from wzk_user")
@Results({
        @Result(id = true, property = "id", column = "id"),
        @Result(property = "username", column = "username"),
        @Result(property = "password", column = "password"),
        @Result(property = "birthday", column = "birthday"),
        @Result(property = "roleList", column = "id",
                javaType = List.class,
                many = @Many(select = "icu.wzk.mapper.RoleMapper.findByUserIdWithAnnotation"))
})
List<WzkUser> findAllUserAndRoleWithAnnotation();

这段配置的含义是:

先查询所有用户:

sql 复制代码
select * from wzk_user

然后把每个用户的 id 传给:

java 复制代码
RoleMapper.findByUserIdWithAnnotation(uid)

最后把角色查询结果封装到当前用户对象的 roleList 中。

2. RoleMapper 配置

新建 RoleMapper

java 复制代码
public interface RoleMapper {

    @Select("select r.* " +
            "from wzk_role r " +
            "inner join wzk_user_role ur on r.id = ur.role_id " +
            "where ur.user_id = #{uid}")
    List<WzkRole> findByUserIdWithAnnotation(int uid);

}

这里推荐使用显式 inner join,不要使用老式逗号连接:

sql 复制代码
select r.*
from wzk_role r
inner join wzk_user_role ur on r.id = ur.role_id
where ur.user_id = #{uid}

这样 SQL 的关联关系更清晰,也更适合后续维护。

八、多对多查询测试代码

java 复制代码
public class WzkIcu13 {

    public static void main(String[] args) throws IOException {
        InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");

        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder()
                .build(resourceAsStream);

        try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
            UserMapper userMapper = sqlSession.getMapper(UserMapper.class);

            List<WzkUser> dataList = userMapper.findAllUserAndRoleWithAnnotation();

            dataList.forEach(System.out::println);
        }
    }
}

测试输出示例:

shell 复制代码
24/11/13 10:01:13 DEBUG UserMapper.findAllUserAndRoleWithAnnotation: <==      Total: 2

WzkUser(id=1, username=wzk, password=icu, birthday=Mon Nov 11 00:00:00 CST 2024, orderList=null, roleList=[WzkRole(id=1, rolename=ADMIN)])

WzkUser(id=2, username=wzk2, password=icu2, birthday=Mon Nov 11 00:00:00 CST 2024, orderList=null, roleList=[WzkRole(id=2, rolename=USER)])

24/11/13 10:01:13 DEBUG jdbc.JdbcTransaction: Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@1e802ef9]

九、@Many 的执行流程

以一对多查询为例:

java 复制代码
@Select("select * from wzk_user")
@Results({
        @Result(property = "orderList", column = "id",
                javaType = List.class,
                many = @Many(select = "icu.wzk.mapper.OrderMapper.findByUserIdWithAnnotation"))
})
List<WzkUser> findAllUserAndOrderWithAnnotation();

MyBatis 的执行流程是:

第一步,执行主查询:

sql 复制代码
select * from wzk_user

第二步,拿到每一条用户记录的 id

第三步,把用户 id 作为参数传给子查询:

sql 复制代码
select * from wzk_orders where uid = ?

第四步,把子查询结果封装到 orderList 中。

所以,如果用户表中有 100 条用户记录,MyBatis 可能会执行:

text 复制代码
1 次用户查询 + 100 次订单子查询

这就是典型的 N+1 查询问题。

十、N+1 查询问题说明

注解模式下使用 @Many 非常方便,但它的默认实现方式通常是嵌套子查询。

这意味着:

text 复制代码
查询用户列表 1 次
每个用户再查询订单或角色 1 次

如果用户数量很少,这种方式没有明显问题。

如果用户数量很大,例如一次查询几千个用户,就可能产生大量 SQL 请求,影响性能。

因此:

  • 小数据量、教学演示、后台管理简单页面:可以使用注解模式。
  • 大数据量、复杂列表页、性能敏感接口:建议使用 JOIN 查询或 XML 映射。
  • 多层嵌套关系:建议优先使用 XML,结构更清晰。

十一、注解模式的优点

1. 配置简单

注解模式不需要单独编写 XML 文件,简单查询可以直接写在 Mapper 接口上。

java 复制代码
@Select("select * from wzk_user")
List<WzkUser> findAll();

2. 代码集中

SQL 和 Mapper 方法放在一起,适合快速查看接口逻辑。

3. 适合简单 CRUD

对于增删改查、简单条件查询、简单关联查询,注解模式开发效率较高。

4. 适合教学和小型项目

注解模式直观,容易理解 MyBatis 的执行流程,适合入门阶段学习。

十二、注解模式的缺点

1. 复杂 SQL 可读性差

SQL 写在 Java 注解中,复杂后会变得难读。

例如多表关联、动态条件、多字段排序、分页组合时,注解会明显臃肿。

2. 动态 SQL 不如 XML 灵活

虽然 MyBatis 注解也支持部分动态 SQL,但可维护性通常不如 XML。

复杂动态 SQL 更适合写在 XML 中。

3. 容易产生 N+1 查询问题

使用 @Many 时,每一条父记录都可能触发一次额外子查询。

数据量小时问题不明显,数据量大时性能问题会非常明显。

4. SQL 与 Java 代码耦合较强

SQL 写在接口注解中,会让 Java 代码和数据库结构绑定更紧。

当 SQL 频繁变化时,维护体验不如 XML。

十三、常见错误速查卡

症状 常见原因 定位方式 修复方式
一对多查询返回 orderList=null @Resultproperty 写错 检查实体类字段名 确保写成 property = "orderList"
订单查询结果不准确 SQL 使用了 where id = #{id} 检查订单表字段含义 改成 where uid = #{uid}
多对多查询返回空角色列表 中间表无数据或关联字段写错 检查 user_role 表数据 确认 user_idrole_id 正确
报找不到 Mapper 方法 @Many(select = "...") 路径错误 检查完整包名和方法名 使用完整 Mapper 路径
IDE 报 SQL 黄色警告 IDE 无法识别注解内 SQL 检查 Data Source 配置 连接数据库或忽略非实际错误
查询性能很差 出现 N+1 查询 查看 MyBatis Debug 日志 改用 JOIN 或 XML 优化
方法名语义不清 单词拼写错误 检查方法命名 Oder 改为 Order
控制台输出字段错位 实体类字段和映射属性不一致 检查 @Result(property=...) 保持数据库字段和 Java 属性对应

十四、推荐命名规范

原方法名:

java 复制代码
findAllUserAndOderWithAnnotation

建议改为:

java 复制代码
findAllUserAndOrderWithAnnotation

原因是:

Oder 是拼写错误,正确单词是 Order

同时,也可以进一步简化命名:

java 复制代码
findAllWithOrders()
findAllWithRoles()

更贴近业务含义,可读性更好。

推荐命名如下:

java 复制代码
List<WzkUser> findAllWithOrders();

List<WzkUser> findAllWithRoles();

如果是教学文章,保留 WithAnnotation 也可以:

java 复制代码
List<WzkUser> findAllUserAndOrderWithAnnotation();

List<WzkUser> findAllUserAndRoleWithAnnotation();

十五、完整核心代码汇总

1. UserMapper

java 复制代码
public interface UserMapper {

    @Select("select * from wzk_user")
    @Results({
            @Result(id = true, property = "id", column = "id"),
            @Result(property = "username", column = "username"),
            @Result(property = "password", column = "password"),
            @Result(property = "birthday", column = "birthday"),
            @Result(property = "orderList", column = "id",
                    javaType = List.class,
                    many = @Many(select = "icu.wzk.mapper.OrderMapper.findByUserIdWithAnnotation"))
    })
    List<WzkUser> findAllUserAndOrderWithAnnotation();


    @Select("select * from wzk_user")
    @Results({
            @Result(id = true, property = "id", column = "id"),
            @Result(property = "username", column = "username"),
            @Result(property = "password", column = "password"),
            @Result(property = "birthday", column = "birthday"),
            @Result(property = "roleList", column = "id",
                    javaType = List.class,
                    many = @Many(select = "icu.wzk.mapper.RoleMapper.findByUserIdWithAnnotation"))
    })
    List<WzkUser> findAllUserAndRoleWithAnnotation();

}

2. OrderMapper

java 复制代码
public interface OrderMapper {

    @Select("select * from wzk_orders where uid = #{uid}")
    List<WzkOrder> findByUserIdWithAnnotation(int uid);

}

3. RoleMapper

java 复制代码
public interface RoleMapper {

    @Select("select r.* " +
            "from wzk_role r " +
            "inner join wzk_user_role ur on r.id = ur.role_id " +
            "where ur.user_id = #{uid}")
    List<WzkRole> findByUserIdWithAnnotation(int uid);

}

4. 测试类

java 复制代码
public class WzkIcu12 {

    public static void main(String[] args) throws IOException {
        InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");

        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder()
                .build(resourceAsStream);

        try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
            UserMapper userMapper = sqlSession.getMapper(UserMapper.class);

            List<WzkUser> userList = userMapper.findAllUserAndOrderWithAnnotation();

            userList.forEach(System.out::println);
        }
    }
}
java 复制代码
public class WzkIcu13 {

    public static void main(String[] args) throws IOException {
        InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");

        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder()
                .build(resourceAsStream);

        try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
            UserMapper userMapper = sqlSession.getMapper(UserMapper.class);

            List<WzkUser> userList = userMapper.findAllUserAndRoleWithAnnotation();

            userList.forEach(System.out::println);
        }
    }
}

十六、什么时候使用注解,什么时候使用 XML?

建议按照复杂度选择。

适合使用注解的场景:

  • 简单 CRUD
  • 简单条件查询
  • 简单一对一、一对多查询
  • 教学示例
  • SQL 变化不频繁的小型项目

适合使用 XML 的场景:

  • SQL 很长
  • 动态条件很多
  • 多表关联复杂
  • 需要复用 ResultMap
  • 需要精细控制映射关系
  • 需要长期维护的业务系统

简单判断标准:

如果一条 SQL 在注解中写出来已经影响阅读,就应该考虑改成 XML。

十七、总结

本文通过 MyBatis 注解模式完成了一对多和多对多关系查询。

一对多查询的关键是:

java 复制代码
@Result(property = "orderList", column = "id",
        many = @Many(select = "OrderMapper中的根据用户ID查询订单方法"))

多对多查询的关键是:

java 复制代码
@Result(property = "roleList", column = "id",
        many = @Many(select = "RoleMapper中的根据用户ID查询角色方法"))

需要重点注意四件事:

第一,property 必须和 Java 实体类字段一致。

第二,column 是传递给子查询的父表字段,通常是用户表主键 id

第三,子查询 SQL 要使用正确的外键字段,例如订单表中应该使用 uid,不是订单表主键 id

第四,@Many 容易产生 N+1 查询问题,数据量大时要谨慎使用。

最终结论:

MyBatis 注解模式适合简单、直观、变化少的关系查询。如果业务 SQL 较复杂,或者查询性能要求较高,更推荐使用 XML 或 JOIN 方式进行优化。


作者:武子康的个人博客

相关推荐
一 乐1 小时前
网上订餐系统|基于springboot的网上订餐系统设计与实现(源码+数据库+文档)
java·数据库·spring boot·后端·论文·毕设·网上订餐系统
XovH1 小时前
第14篇 Docker Compose 开发环境最佳实践:热重载与调试
后端
摇滚侠1 小时前
我把一个依赖安装到了本地仓库,但是IDEA 刷新 maven 提示远程私服仓库找不到,怎么解决
java·maven·intellij-idea
.Cnn1 小时前
SpringBoot 文件上传与阿里云 OSS 集成
java·spring boot·后端·阿里云
XovH1 小时前
Docker从0到1再到 Kubernetes 实战:第15篇Compose 中的服务依赖、健康检查与启动顺序
后端
Mininglamp_27181 小时前
现在入局Agent开发还来得及吗?
java·开发语言
XovH1 小时前
Docker 从 0 到 1 再到 Kubernetes 实战:第13篇 Compose 环境变量与配置管理
后端
疯狂成瘾者1 小时前
GHCR 是什么?GitHub 容器镜像仓库技术介绍
java·linux