MyBatis之关联查询

发布于:2025-07-19 ⋅ 阅读:(13) ⋅ 点赞:(0)

实际开发中数据库表之间往往存在关联关系(如用户与订单、订单与商品),MyBatis的关联查询用于处理这些关系,将多表数据映射为Java对象的关联关系,相比JDBC手动处理结果集拼接,MyBatis通过resultMapassociationcollection标签,能自动完成关联数据的映射。本文我将系统讲解MyBatis关联查询的核心实现,包括一对一、一对多、多对多关系,并结合实例解析查询方式与优化技巧。

一、关联查询的基本概念

1.1 数据库表关联关系

数据库表的关联关系主要有三种:

  • 一对一:A表一条记录对应B表一条记录(如用户与身份证,一个用户对应一个身份证);
  • 一对多:A表一条记录对应B表多条记录(如用户与订单,一个用户可有多笔订单);
  • 多对多:A表多条记录对应B表多条记录(如学生与课程,一个学生可选多门课程,一门课程可有多个学生),通常通过中间表实现。

1.2 MyBatis关联查询的核心

MyBatis通过resultMap实现关联查询,核心标签:

  • association:映射一对一关系(如用户对象中包含一个身份证对象);
  • collection:映射一对多多对多关系(如用户对象中包含一个订单列表)。

关联查询有两种实现方式:

  • 嵌套查询:先查询主表数据,再根据主表字段查询关联表(多轮查询);
  • 连接查询:通过JOIN语句一次性查询多表数据(单轮查询)。

后续将通过案例详细对比这两种方式的优缺点。

二、一对一关联查询

以“用户(user)与身份证(id_card)”为例,一个用户对应一个身份证,实现一对一关联查询。

2.1 数据库表设计

-- 用户表
CREATE TABLE `user` (
  `id` int NOT NULL AUTO_INCREMENT,
  `username` varchar(50) NOT NULL,
  `age` int DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 身份证表(与用户一对一关联)
CREATE TABLE `id_card` (
  `id` int NOT NULL AUTO_INCREMENT,
  `card_no` varchar(20) NOT NULL, -- 身份证号
  `user_id` int NOT NULL, -- 关联用户ID(外键)
  PRIMARY KEY (`id`),
  UNIQUE KEY `user_id` (`user_id`) -- 一对一:user_id唯一
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

2.2 实体类设计

// 用户类(包含一个身份证对象)
@Data
public class User {
    private Integer id;
    private String username;
    private Integer age;
    // 一对一关联:用户包含一个身份证
    private IdCard idCard; 
}

// 身份证类
@Data
public class IdCard {
    private Integer id;
    private String cardNo;
    private Integer userId;
}

2.3 一对一查询实现

2.3.1 方式1:连接查询(推荐)

通过JOIN语句一次性查询用户和身份证数据,再通过association映射关联对象。

Mapper接口

// 根据用户ID查询用户及关联的身份证
User selectUserWithIdCardById(Integer id);

Mapper XML

<!-- 定义resultMap:映射用户及身份证 -->
<resultMap id="UserWithIdCardMap" type="User">
    <!-- 用户表字段映射 -->
    <id column="id" property="id"/>
    <result column="username" property="username"/>
    <result column="age" property="age"/>
    
    <!-- 一对一关联:association映射IdCard对象 -->
    <association property="idCard" javaType="IdCard">
        <id column="card_id" property="id"/> <!-- 注意:避免与user.id字段冲突 -->
        <result column="card_no" property="cardNo"/>
        <result column="user_id" property="userId"/>
    </association>
</resultMap>

<!-- 连接查询:一次性查询用户和身份证 -->
<select id="selectUserWithIdCardById" resultMap="UserWithIdCardMap">
    SELECT 
        u.id, u.username, u.age,
        ic.id AS card_id, ic.card_no, ic.user_id
    FROM user u
    LEFT JOIN id_card ic ON u.id = ic.user_id
    WHERE u.id = #{id}
</select>

核心说明

  • associationproperty:对应User类中的idCard属性;
  • javaType:指定关联对象的类型(IdCard);
  • 表连接时需通过AS为关联表字段起别名(如ic.id AS card_id),避免与主表字段(u.id)冲突。
2.3.2 方式2:嵌套查询

先查询用户数据,再通过用户ID查询身份证(分两次查询)。

步骤1:查询身份证的Mapper

// IdCardMapper接口
IdCard selectById(Integer id);
<select id="selectById" resultType="IdCard">
    SELECT id, card_no, user_id FROM id_card WHERE id = #{id}
</select>

步骤2:查询用户并嵌套查询身份证

<resultMap id="UserWithIdCardNestedMap" type="User">
    <id column="id" property="id"/>
    <result column="username" property="username"/>
    <result column="age" property="age"/>
    
    <!-- 嵌套查询:通过select属性指定查询关联对象的方法 -->
    <association 
        property="idCard" 
        javaType="IdCard"
        column="id" <!-- 将用户id作为参数传递给关联查询 -->
        select="com.example.mapper.IdCardMapper.selectByUserId"/> <!-- 关联查询的Mapper方法 -->
</resultMap>

<select id="selectUserWithIdCardNestedById" resultMap="UserWithIdCardNestedMap">
    SELECT id, username, age FROM user WHERE id = #{id}
</select>

核心说明

  • associationselect:指定查询关联对象的Mapper方法(全类名+方法名);
  • column:将主查询的id(用户ID)作为参数传递给selectByUserId方法。
2.3.3 两种方式对比
方式 优点 缺点 适用场景
连接查询 单轮查询,性能好 SQL较复杂(多表JOIN) 关联数据必须查询,且表数据量不大
嵌套查询 SQL简单,逻辑清晰 多轮查询(N+1问题),性能较差 关联数据可选查询(按需加载)

三、一对多关联查询

以“用户(user)与订单(order)”为例,一个用户可有多笔订单,实现一对多关联查询。

3.1 数据库表设计

-- 订单表(与用户一对多关联)
CREATE TABLE `order` (
  `id` int NOT NULL AUTO_INCREMENT,
  `order_no` varchar(20) NOT NULL, -- 订单号
  `total_amount` decimal(10,2) NOT NULL, -- 总金额
  `user_id` int NOT NULL, -- 关联用户ID
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

3.2 实体类设计

// 用户类(包含订单列表)
@Data
public class User {
    private Integer id;
    private String username;
    private Integer age;
    // 一对多关联:用户包含多个订单
    private List<Order> orders; 
}

// 订单类
@Data
public class Order {
    private Integer id;
    private String orderNo;
    private BigDecimal totalAmount;
    private Integer userId;
}

3.3 一对多查询实现

连接查询为例(推荐,单轮查询性能更好):

Mapper接口

// 查询用户及关联的所有订单
User selectUserWithOrdersById(Integer id);

Mapper XML

<!-- 定义resultMap:映射用户及订单列表 -->
<resultMap id="UserWithOrdersMap" type="User">
    <!-- 用户表字段 -->
    <id column="id" property="id"/>
    <result column="username" property="username"/>
    <result column="age" property="age"/>
    
    <!-- 一对多关联:collection映射订单列表 -->
    <collection property="orders" ofType="Order"> <!-- ofType指定集合元素类型 -->
        <id column="order_id" property="id"/> <!-- 订单ID(别名避免冲突) -->
        <result column="order_no" property="orderNo"/>
        <result column="total_amount" property="totalAmount"/>
        <result column="user_id" property="userId"/>
    </collection>
</resultMap>

<!-- 连接查询:用户与订单 -->
<select id="selectUserWithOrdersById" resultMap="UserWithOrdersMap">
    SELECT 
        u.id, u.username, u.age,
        o.id AS order_id, o.order_no, o.total_amount, o.user_id
    FROM user u
    LEFT JOIN `order` o ON u.id = o.user_id
    WHERE u.id = #{id}
</select>

核心说明

  • collectionproperty:对应User类中的orders属性(List类型);
  • ofType:指定集合中元素的类型(Order),区别于javaType(用于指定属性类型,如List);
  • 主表与关联表的字段需通过别名区分(如o.id AS order_id),避免映射混乱。

四、多对多关联查询

以“学生(student)与课程(course)”为例,一个学生可选多门课程,一门课程可有多个学生,通过中间表student_course实现关联。

4.1 数据库表设计

-- 学生表
CREATE TABLE `student` (
  `id` int NOT NULL AUTO_INCREMENT,
  `name` varchar(50) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 课程表
CREATE TABLE `course` (
  `id` int NOT NULL AUTO_INCREMENT,
  `name` varchar(50) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 中间表(多对多关联)
CREATE TABLE `student_course` (
  `id` int NOT NULL AUTO_INCREMENT,
  `student_id` int NOT NULL,
  `course_id` int NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_stu_course` (`student_id`,`course_id`) -- 避免重复关联
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

4.2 实体类设计

// 学生类(包含课程列表)
@Data
public class Student {
    private Integer id;
    private String name;
    // 多对多关联:学生包含多个课程
    private List<Course> courses;
}

// 课程类
@Data
public class Course {
    private Integer id;
    private String name;
}

4.3 多对多查询实现

多对多查询本质是一对多的扩展(通过中间表连接),以连接查询为例:

Mapper接口

// 查询学生及所选课程
Student selectStudentWithCoursesById(Integer id);

Mapper XML

<resultMap id="StudentWithCoursesMap" type="Student">
    <id column="id" property="id"/>
    <result column="name" property="name"/>
    
    <!-- 多对多:collection映射课程列表 -->
    <collection property="courses" ofType="Course">
        <id column="course_id" property="id"/>
        <result column="course_name" property="name"/>
    </collection>
</resultMap>

<select id="selectStudentWithCoursesById" resultMap="StudentWithCoursesMap">
    SELECT 
        s.id, s.name,
        c.id AS course_id, c.name AS course_name
    FROM student s
    LEFT JOIN student_course sc ON s.id = sc.student_id
    LEFT JOIN course c ON sc.course_id = c.id
    WHERE s.id = #{id}
</select>

核心说明

  • 多对多通过“主表→中间表→关联表”的JOIN实现;
  • collection标签用法与一对多相同(均映射集合),区别在于表连接逻辑。

五、关联查询的优化与最佳实践

5.1 避免N+1查询问题

N+1问题:嵌套查询时,若查询N个主表记录,会触发1次主表查询+N次关联表查询,导致性能下降。

示例:查询所有用户及其订单(嵌套查询方式):

<!-- 1次主表查询:查询所有用户 -->
<select id="selectAllUser" resultMap="UserWithOrdersNestedMap">
    SELECT id, username, age FROM user
</select>

<!-- 每个用户触发1次订单查询(若有100个用户,触发100次) -->
<collection property="orders" select="selectOrdersByUserId" column="id"/>

解决方案

  • 优先使用连接查询(单轮查询,无N+1问题);
  • 若需嵌套查询,开启MyBatis二级缓存,缓存关联查询结果;
  • 限制查询数量(如分页查询),减少关联查询次数。

5.2 合理使用别名避免字段冲突

多表查询时,若主表与关联表有同名字段(如idname),需通过别名区分,否则映射结果会被覆盖。

-- 错误:未用别名,o.id会覆盖u.id
SELECT u.id, u.name, o.id, o.name 
FROM user u JOIN order o ON u.id = o.user_id

-- 正确:用别名区分
SELECT 
    u.id AS user_id, u.name AS user_name,
    o.id AS order_id, o.name AS order_name

5.3 按需查询关联数据

并非所有场景都需要查询关联数据(如列表页展示用户基本信息,无需查询订单),应根据场景设计不同查询:

  • 简单查询:仅查询主表数据(无关联);
  • 详情查询:查询主表+关联数据(通过连接查询)。

5.4 延迟加载(按需加载关联数据)

MyBatis支持延迟加载(懒加载):查询主表数据时不加载关联数据,仅当访问关联属性时才触发关联查询,适合“大部分场景不需要关联数据”的场景。

开启延迟加载(在MyBatis配置文件中):

<settings>
    <setting name="lazyLoadingEnabled" value="true"/> <!-- 全局开启延迟加载 -->
    <setting name="aggressiveLazyLoading" value="false"/> <!-- 按需加载(访问时才加载) -->
</settings>

使用场景:详情页默认展示用户信息,点击“查看订单”按钮才加载订单数据(通过代码触发关联属性访问)。

六、常见问题与避坑指南

6.1 关联对象为null(映射失败)

问题:关联对象(如idCard)为null,但数据库存在关联数据。

原因

  • resultMapcolumn与SQL查询的字段名不匹配(如SQL用card_idresultMapcolumn="id");
  • 表连接条件错误(如JOIN条件不正确,导致关联数据未查询到);
  • 关联表无匹配数据(正常情况,如用户未绑定身份证,idCardnull)。

解决方案

  • 检查resultMapcolumn是否与SQL查询的字段(含别名)一致;
  • 单独执行SQL,确认关联数据是否被正确查询;
  • 若允许关联数据为null,无需处理(正常逻辑)。

6.2 集合数据重复(一条数据被多次映射)

问题collection映射的列表中,同一条数据被重复添加(如一个订单出现多次)。

原因

  • 未正确配置id标签:resultMap中未用id标签指定关联对象的唯一标识(如订单的id),MyBatis无法判断数据是否重复;
  • SQL查询返回重复数据(如JOIN导致主表数据被关联表数据重复)。

解决方案

  • 为关联对象配置id标签(collection内的id),指定唯一标识字段:
<collection property="orders" ofType="Order">
    <id column="order_id" property="id"/> <!-- 关键:指定订单唯一标识 -->
    <!-- 其他字段 -->
</collection>
  • 优化SQL,避免返回重复数据(如使用DISTINCT或调整JOIN逻辑)。

6.3 嵌套查询参数传递错误

问题:嵌套查询时,column传递的参数不正确,导致关联查询无结果。

解决方案

  • 确保column的值与主查询返回的字段名一致:
<!-- 主查询返回字段为user_id -->
<select id="selectUser" resultMap="UserMap">
    SELECT id AS user_id, username FROM user
</select>

<!-- 嵌套查询需用主查询的字段名作为参数 -->
<association select="selectOrder" column="user_id"/> <!-- 正确:column="user_id" -->
  • 传递多个参数时,用column="{key1=col1, key2=col2}"
<association select="selectByParams" column="{id=user_id, name=user_name}"/>

总结:关联查询的核心要点
MyBatis关联查询通过resultMapassociationcollection标签,实现了多表数据到Java对象关联关系的映射,核心要点如下:

  1. 标签选择
  • 一对一用association(映射单个对象);
  • 一对多/多对多用collection(映射集合)。
  1. 查询方式
  • 优先用连接查询(单轮JOIN查询,无N+1问题,性能好);
  • 嵌套查询仅用于“关联数据按需加载”场景(需注意N+1问题)。
  1. 优化技巧
  • 用别名区分同名字段,避免映射冲突;
  • 配置id标签确保关联数据不重复;
  • 避免查询无关字段,减少数据传输量。

若这篇内容帮到你,动动手指支持下!关注不迷路,干货持续输出!
ヾ(´∀ ˋ)ノヾ(´∀ ˋ)ノヾ(´∀ ˋ)ノヾ(´∀ ˋ)ノヾ(´∀ ˋ)ノ


网站公告

今日签到

点亮在社区的每一天
去签到