MyBatis 中 association 与 collection 详解

前言

在使用 MyBatis 开发 Java 项目时,我们经常会遇到这样的场景:

数据库中有一张主表,例如:

text 复制代码
车辆配置表

同时还有很多关联数据:

text 复制代码
配置
├── 对公支付控制
├── 供应商列表
├── 适用部门列表
├── 适用职级列表
├── 适用个人列表
├── 不适用个人列表
├── 不适用角色列表
├── 时间控制列表
└── 地点控制列表

最终我们希望 MyBatis 查询数据库后,直接组装成一个完整的 Java VO。

这时候,MyBatis 中的 resultMap、association 和 collection 就发挥了重要作用。

本文结合一个实际项目中的 MyBatis XML,如下:

xml 复制代码
 <!-- 根据配置id查询配置详情 -->
  <resultMap id="ConfigDetail" type="com.cmbchina.car.controller.bean.carconfig.QueryCarConfigDetailVo">
    <!-- 对公支付控制内容 -->
    <association property="corporatePayControl"
                 column="{configId = configId, userId = userId}"
                 javaType="com.cmbchina.car.controller.bean.carconfig.CorporatePayControlVo"
                 select="selectCorporatePayControl">
    </association>
    <!-- 供应商列表 -->
    <collection property="supplierList"
                column="supplier"
                ofType="com.cmbchina.car.controller.bean.carconfig.SupplierVo"
                javaType="ArrayList"
                select="selectSuppliers">
    </collection>
    <!-- 适用部门列表 -->
    <collection property="deptList"
                column="configId"
                ofType="com.cmbchina.car.controller.bean.carconfig.ConfigUseRangeVo"
                javaType="ArrayList"
                select="selectApplyDept">
    </collection>
    <!-- 适用职级列表 -->
    <collection property="rankList"
                column="configId"
                ofType="com.cmbchina.car.controller.bean.carconfig.ConfigUseRangeVo"
                javaType="ArrayList"
                select="selectApplyRank">
    </collection>
    <!-- 适用个人列表 -->
    <collection property="applyUsers"
                column="configId"
                ofType="com.cmbchina.car.controller.bean.carconfig.ConfigUseRangeVo"
                javaType="ArrayList"
                select="selectApplyUsers">
    </collection>
    <!-- 不适用个人列表 -->
    <collection property="notApplyUsers"
                column="configId"
                ofType="com.cmbchina.car.controller.bean.carconfig.ConfigUseRangeVo"
                javaType="ArrayList"
                select="selectNotApplyUsers">
    </collection>
    <!-- 不适用角色列表 -->
    <collection property="notApplyRoles"
                column="configId"
                ofType="com.cmbchina.car.controller.bean.carconfig.ConfigUseRangeVo"
                javaType="ArrayList"
                select="selectNotApplyRoles">
    </collection>
    <!-- 时间控制列表 -->
    <collection property="timeList"
                column="configId"
                ofType="com.cmbchina.car.controller.bean.carconfig.TimeControlVo"
                javaType="ArrayList"
                select="selectTimeByConfigId">
    </collection>
    <!-- 地点控制列表 -->
    <collection property="positionList"
                column="{configId = configId ,locationType = locationType}"
                ofType="com.cmbchina.car.controller.bean.carconfig.PositionControlVo"
                javaType="ArrayList"
                select="selectPositionByConfigId">
    </collection>
  </resultMap>
  
   <select id="queryCarConfigDetail" resultMap="ConfigDetail">
    select car_config_id configId,
      supplier_no supplier,
      #{userId} userId,
      car_config_id,
      car_scene_id,
      config_name,
      config_desc,
      sub_company_no,
      sub_company_name,
      car_type,
      scene_type,
      order_limit_amount,
      day_limit_amount,
      day_limit_number,
      if(order_limit_amount is null, '0', '1') enableOrderAmount,
      if(day_limit_amount is null, '0', '1') enableDayAmount,
      if(day_limit_number is null, '0', '1') enableDayNumber,
      dd_regulation_id,
      dd_scene_type,
      enable_personal_limit,
      if(state = '1', true, false) enable,
      enable_corporate_pay_control,
      enable_time_control,
      enable_position_control,
      position_type,
      location_type locationType,
      enable_apply_control,
      enable_book,
      priority
    from car_bsc_car_scene_config
    <if test="list != null and list.size()>0">
      where car_config_id in
      <foreach collection="list" item="item" separator="," open="(" close=")">
       #{item}
      </foreach>
    </if>
  </select>
  
   <select id="selectCorporatePayControl" resultType="com.cmbchina.car.controller.bean.carconfig.CorporatePayControlVo">
    with cost_center as (
      select cost_center_no,
        ' ' as cost_center_name,
        cost_center_no_upp as upp_cost_center_no,
        ' ' as upp_cost_center_name
      from car_bsc_company_emp where user_id = #{userId}
    )
    select a.*,b.*
    from car_bsc_car_scene_config_corporate_control a
    left join cost_center b on true
    where a.car_config_id = #{configId}
  </select>
  <select id="selectSuppliers" resultType="com.cmbchina.car.controller.bean.carconfig.SupplierVo">
    select supplier_no,supplier_name from car_bsc_supplier_base where position(supplier_no in #{supplier}) != 0 and state = '1'
  </select>
  <select id="selectApplyDept" resultType="com.cmbchina.car.controller.bean.carconfig.ConfigUseRangeVo">
    select car_scene_config_use_range_id,type,code,name from car_bsc_car_scene_config_use_range
    where type in ('1','5') and apply = '1' and car_config_id = #{configId} and state = '1'
  </select>
  <select id="selectApplyRank" resultType="com.cmbchina.car.controller.bean.carconfig.ConfigUseRangeVo">
    select car_scene_config_use_range_id,code,name from car_bsc_car_scene_config_use_range
    where type = '3' and apply = '1' and car_config_id = #{configId} and state = '1'
  </select>
  <select id="selectApplyUsers" resultType="com.cmbchina.car.controller.bean.carconfig.ConfigUseRangeVo">
    select car_scene_config_use_range_id,code,name from car_bsc_car_scene_config_use_range
    where type = '2' and apply = '1' and car_config_id = #{configId} and state = '1'
  </select>
  <select id="selectNotApplyUsers" resultType="com.cmbchina.car.controller.bean.carconfig.ConfigUseRangeVo">
    select car_scene_config_use_range_id,code,name from car_bsc_car_scene_config_use_range
    where type = '2' and apply = '2' and car_config_id = #{configId} and state = '1'
  </select>
  <select id="selectNotApplyRoles" resultType="com.cmbchina.car.controller.bean.carconfig.ConfigUseRangeVo">
    select car_scene_config_use_range_id,code,name from car_bsc_car_scene_config_use_range
    where type = '4' and apply = '2' and car_config_id = #{configId} and state = '1'
  </select>
  

详细介绍这几个标签的作用以及它们背后的执行机制。


一、先理解 resultMap 是什么

在 MyBatis 中,最简单的查询可能是:

xml 复制代码
<select id="queryUser" resultType="User">
    select id, name, age
    from user
</select>

如果数据库字段和 Java 属性可以直接对应,那么使用 resultType 就足够了。

例如数据库:

text 复制代码
id | name | age

Java:

java 复制代码
public class User {

    private Long id;

    private String name;

    private Integer age;
}

MyBatis 可以自动完成:

text 复制代码
数据库字段
    ↓
Java 属性

但是,如果对象结构比较复杂,例如:

java 复制代码
public class QueryCarConfigDetailVo {

    private Long configId;

    private String configName;

    private CorporatePayControlVo corporatePayControl;

    private List<SupplierVo> supplierList;

    private List<ConfigUseRangeVo> deptList;

    private List<ConfigUseRangeVo> rankList;

    private List<ConfigUseRangeVo> applyUsers;

    private List<ConfigUseRangeVo> notApplyUsers;

    private List<ConfigUseRangeVo> notApplyRoles;

    private List<TimeControlVo> timeList;

    private List<PositionControlVo> positionList;
}

那么简单的 resultType 就不够用了。

这时候可以使用:

xml 复制代码
<resultMap>

resultMap 可以理解为:

告诉 MyBatis:数据库查询结果应该如何组装成 Java 对象。


二、association 和 collection 分别是什么?

先记住最核心的一句话:

text 复制代码
association  → 一个对象
collection   → 一个集合

也可以理解为:

MyBatis 标签 Java 类型 关系
association 一个 Java 对象 一对一
collection List / Set 一对多

例如:

java 复制代码
private CorporatePayControlVo corporatePayControl;

它是一个对象,因此使用:

xml 复制代码
<association>

而:

java 复制代码
private List<ConfigUseRangeVo> deptList;

它是一个集合,因此使用:

xml 复制代码
<collection>

三、结合实际代码理解 association

例如项目中的代码:

xml 复制代码
<association property="corporatePayControl"
             column="{configId = configId, userId = userId}"
             javaType="com.cmbchina.car.controller.bean.carconfig.CorporatePayControlVo"
             select="selectCorporatePayControl">
</association>

这段代码可以拆成几个部分理解。


1. property:最终放到哪个属性

xml 复制代码
property="corporatePayControl"

表示查询结果最终要放到:

java 复制代码
QueryCarConfigDetailVo.corporatePayControl

相当于:

java 复制代码
QueryCarConfigDetailVo vo = new QueryCarConfigDetailVo();

vo.setCorporatePayControl(...);

2. javaType:这个属性是什么类型

xml 复制代码
javaType="com.cmbchina.car.controller.bean.carconfig.CorporatePayControlVo"

说明:

java 复制代码
corporatePayControl

对应:

java 复制代码
CorporatePayControlVo

也就是说:

text 复制代码
QueryCarConfigDetailVo
        │
        └── corporatePayControl
                    │
                    └── CorporatePayControlVo

3. select:调用哪个 SQL 查询

这里最关键:

xml 复制代码
select="selectCorporatePayControl"

表示:

查询主对象的时候,如果需要组装 corporatePayControl,再调用 selectCorporatePayControl 这个 SQL。

项目中对应的 SQL 是:

xml 复制代码
<select id="selectCorporatePayControl"
        resultType="com.cmbchina.car.controller.bean.carconfig.CorporatePayControlVo">

    with cost_center as (
        select cost_center_no,
               ' ' as cost_center_name,
               cost_center_no_upp as upp_cost_center_no,
               ' ' as upp_cost_center_name
        from car_bsc_company_emp
        where user_id = #{userId}
    )

    select a.*, b.*
    from car_bsc_car_scene_config_corporate_control a
    left join cost_center b on true
    where a.car_config_id = #{configId}

</select>

因此整个过程可以理解为:

text 复制代码
queryCarConfigDetail
        ↓
查询主配置
        ↓
QueryCarConfigDetailVo
        ↓
发现需要 corporatePayControl
        ↓
调用 selectCorporatePayControl
        ↓
查询 CorporatePayControlVo
        ↓
设置到 corporatePayControl

四、column 可以给嵌套查询传参数

这里还有一个非常重要的地方:

xml 复制代码
column="{configId = configId, userId = userId}"

这表示:

将当前主查询结果中的 configId 和 userId 作为参数,传递给 selectCorporatePayControl。

例如主查询得到:

text 复制代码
configId = 10001
userId   = U001

那么调用子查询时,就相当于:

text 复制代码
selectCorporatePayControl(
    configId = 10001,
    userId = U001
)

于是子 SQL 中:

sql 复制代码
where a.car_config_id = #{configId}

可以拿到:

text 复制代码
10001

而:

sql 复制代码
where user_id = #{userId}

可以拿到:

text 复制代码
U001

五、为什么这里可以传多个参数?

MyBatis 的 column 不仅可以写:

xml 复制代码
column="configId"

也可以写:

xml 复制代码
column="{configId = configId, userId = userId}"

前者表示传一个参数:

text 复制代码
configId

后者表示传多个参数:

text 复制代码
configId → configId
userId   → userId

可以把它理解成一个参数 Map:

java 复制代码
Map<String, Object> params = new HashMap<>();

params.put("configId", 当前行的configId);
params.put("userId", 当前行的userId);

然后传给:

text 复制代码
selectCorporatePayControl

因此子 SQL 可以使用:

sql 复制代码
#{configId}
#{userId}

六、主查询中的 userId 从哪里来?

项目代码中还有一个容易被忽略的地方:

sql 复制代码
select car_config_id configId,
       supplier_no supplier,
       #{userId} userId,
       car_config_id,
       ...
from car_bsc_car_scene_config

这里:

sql 复制代码
#{userId}

并不是数据库字段。

它是 MyBatis Mapper 方法传进来的参数。

例如 Mapper 可能是:

java 复制代码
List<QueryCarConfigDetailVo> queryCarConfigDetail(
        @Param("list") List<Long> list,
        @Param("userId") String userId
);

那么:

sql 复制代码
#{userId} userId

就相当于把 Java 方法参数 userId 放进 SQL 查询结果中。

例如:

text 复制代码
configId | supplier | userId
---------+----------+-------
10001    | DiDi     | U001

这样后面的:

xml 复制代码
column="{configId = configId, userId = userId}"

就能够同时获得:

text 复制代码
configId = 10001
userId   = U001

七、collection 是什么?

接下来看看:

xml 复制代码
<collection property="deptList"
            column="configId"
            ofType="com.cmbchina.car.controller.bean.carconfig.ConfigUseRangeVo"
            javaType="ArrayList"
            select="selectApplyDept">
</collection>

这就是 collection。

它与 association 最大的区别是:

text 复制代码
association → 一个对象

collection → 多个对象

例如:

java 复制代码
private List<ConfigUseRangeVo> deptList;

一个车辆配置可能对应多个部门:

text 复制代码
配置 10001
│
├── 技术部
├── 财务部
├── 人力资源部
└── 风险管理部

因此 Java 中需要:

java 复制代码
List<ConfigUseRangeVo>

而不是:

java 复制代码
ConfigUseRangeVo

所以使用:

xml 复制代码
<collection>

八、collection 中的 property

例如:

xml 复制代码
property="deptList"

表示:

查询出来的集合最终放到 QueryCarConfigDetailVo.deptList。

相当于:

java 复制代码
vo.setDeptList(list);

九、collection 中的 ofType

代码:

xml 复制代码
ofType="com.cmbchina.car.controller.bean.carconfig.ConfigUseRangeVo"

表示:

这个集合里面的元素是什么类型。

也就是说:

java 复制代码
List<ConfigUseRangeVo>

其中:

text 复制代码
List
  ↓
ConfigUseRangeVo

因此:

text 复制代码
javaType

和:

text 复制代码
ofType

可以简单理解为:

text 复制代码
javaType → 整个属性是什么类型

ofType   → 集合中的元素是什么类型

例如:

xml 复制代码
javaType="ArrayList"
ofType="ConfigUseRangeVo"

表示:

java 复制代码
ArrayList<ConfigUseRangeVo>

十、collection 中的 column

例如:

xml 复制代码
column="configId"

表示:

使用当前主查询结果中的 configId,作为参数传给子查询。

对应的子 SQL:

xml 复制代码
<select id="selectApplyDept"
        resultType="com.cmbchina.car.controller.bean.carconfig.ConfigUseRangeVo">

    select car_scene_config_use_range_id,
           type,
           code,
           name
    from car_bsc_car_scene_config_use_range
    where type in ('1','5')
      and apply = '1'
      and car_config_id = #{configId}
      and state = '1'

</select>

因此执行过程就是:

text 复制代码
主查询
configId = 10001
        ↓
selectApplyDept(10001)
        ↓
where car_config_id = 10001
        ↓
查询多个部门
        ↓
List<ConfigUseRangeVo>
        ↓
deptList

十一、项目中的多个 collection

项目中实际上使用了多个 collection:

1. 适用部门

xml 复制代码
<collection property="deptList"
            column="configId"
            select="selectApplyDept">
</collection>

2. 适用职级

xml 复制代码
<collection property="rankList"
            column="configId"
            select="selectApplyRank">
</collection>

3. 适用个人

xml 复制代码
<collection property="applyUsers"
            column="configId"
            select="selectApplyUsers">
</collection>

4. 不适用个人

xml 复制代码
<collection property="notApplyUsers"
            column="configId"
            select="selectNotApplyUsers">
</collection>

5. 不适用角色

xml 复制代码
<collection property="notApplyRoles"
            column="configId"
            select="selectNotApplyRoles">
</collection>

6. 时间控制

xml 复制代码
<collection property="timeList"
            column="configId"
            select="selectTimeByConfigId">
</collection>

7. 地点控制

地点控制稍微特殊一些:

xml 复制代码
<collection property="positionList"
            column="{configId = configId, locationType = locationType}"
            select="selectPositionByConfigId">
</collection>

这里传递了两个参数:

text 复制代码
configId
locationType

所以子 SQL 可以同时使用:

sql 复制代码
#{configId}
#{locationType}

十二、整个 resultMap 到底做了什么?

现在把整个 resultMap 放在一起看:

xml 复制代码
<resultMap id="ConfigDetail"
           type="com.cmbchina.car.controller.bean.carconfig.QueryCarConfigDetailVo">

    <association property="corporatePayControl"
                 column="{configId = configId, userId = userId}"
                 javaType="CorporatePayControlVo"
                 select="selectCorporatePayControl">
    </association>

    <collection property="supplierList"
                column="supplier"
                ofType="SupplierVo"
                javaType="ArrayList"
                select="selectSuppliers">
    </collection>

    <collection property="deptList"
                column="configId"
                ofType="ConfigUseRangeVo"
                javaType="ArrayList"
                select="selectApplyDept">
    </collection>

    <collection property="rankList"
                column="configId"
                ofType="ConfigUseRangeVo"
                javaType="ArrayList"
                select="selectApplyRank">
    </collection>

    <collection property="applyUsers"
                column="configId"
                ofType="ConfigUseRangeVo"
                javaType="ArrayList"
                select="selectApplyUsers">
    </collection>

    <collection property="notApplyUsers"
                column="configId"
                ofType="ConfigUseRangeVo"
                javaType="ArrayList"
                select="selectNotApplyUsers">
    </collection>

    <collection property="notApplyRoles"
                column="configId"
                ofType="ConfigUseRangeVo"
                javaType="ArrayList"
                select="selectNotApplyRoles">
    </collection>

    <collection property="timeList"
                column="configId"
                ofType="TimeControlVo"
                javaType="ArrayList"
                select="selectTimeByConfigId">
    </collection>

    <collection property="positionList"
                column="{configId = configId, locationType = locationType}"
                ofType="PositionControlVo"
                javaType="ArrayList"
                select="selectPositionByConfigId">
    </collection>

</resultMap>

它实际上是在描述这样一个对象:

text 复制代码
QueryCarConfigDetailVo
│
├── 基础配置字段
│
├── corporatePayControl
│       └── CorporatePayControlVo
│
├── supplierList
│       └── List<SupplierVo>
│
├── deptList
│       └── List<ConfigUseRangeVo>
│
├── rankList
│       └── List<ConfigUseRangeVo>
│
├── applyUsers
│       └── List<ConfigUseRangeVo>
│
├── notApplyUsers
│       └── List<ConfigUseRangeVo>
│
├── notApplyRoles
│       └── List<ConfigUseRangeVo>
│
├── timeList
│       └── List<TimeControlVo>
│
└── positionList
        └── List<PositionControlVo>

所以 resultMap 本质上就是:

Java 对象的组装说明书。


十三、完整的执行流程

假设主查询:

sql 复制代码
select car_config_id configId,
       supplier_no supplier,
       #{userId} userId,
       location_type locationType,
       ...
from car_bsc_car_scene_config

查询出来:

text 复制代码
configId     = 10001
supplier     = DiDi
userId       = U001
locationType = 1

MyBatis 接下来根据 resultMap 组装对象。

首先创建:

java 复制代码
QueryCarConfigDetailVo vo =
        new QueryCarConfigDetailVo();

然后处理基础字段:

java 复制代码
vo.setConfigId(10001);

接下来处理:

xml 复制代码
<association>

调用:

text 复制代码
selectCorporatePayControl

参数:

text 复制代码
configId = 10001
userId   = U001

得到:

java 复制代码
CorporatePayControlVo

然后:

java 复制代码
vo.setCorporatePayControl(...);

接下来处理:

xml 复制代码
<collection property="deptList">

调用:

text 复制代码
selectApplyDept

参数:

text 复制代码
configId = 10001

得到:

java 复制代码
List<ConfigUseRangeVo>

然后:

java 复制代码
vo.setDeptList(...);

后面继续:

text 复制代码
selectApplyRank
selectApplyUsers
selectNotApplyUsers
selectNotApplyRoles
selectTimeByConfigId
selectPositionByConfigId

最终形成一个完整的:

java 复制代码
QueryCarConfigDetailVo

十四、为什么不直接使用 JOIN?

看到这里可能会产生一个问题:

这些数据本来都是关联表,为什么不使用 LEFT JOIN 一次性查出来?

例如:

sql 复制代码
select ...
from car_bsc_car_scene_config a
left join car_bsc_car_scene_config_use_range b
    on a.car_config_id = b.car_config_id

当然可以。

但是问题在于:

一个主表记录可能同时存在多个一对多关系。

例如一个配置:

text 复制代码
3 个部门
5 个职级
10 个适用人员
2 个不适用人员
4 个角色

如果把这些表全部 JOIN:

text 复制代码
3 × 5 × 10 × 2 × 4

最终可能产生:

text 复制代码
1200

条组合记录。

但实际上我们只需要:

text 复制代码
1 个配置
3 个部门
5 个职级
10 个用户
2 个排除用户
4 个角色

也就是说:

text 复制代码
真正的数据量
≠
多表 JOIN 后产生的数据行数

这就是多表 JOIN 在复杂一对多场景中容易出现的数据膨胀问题。


十五、这种写法实际上是一种"分开查询、对象组装"的方案

现在这段 MyBatis 的结构就很好理解了:

text 复制代码
                    主配置
                      │
                      ↓
             car_bsc_car_scene_config
                      │
                      ↓
          QueryCarConfigDetailVo
                      │
       ┌──────────────┼──────────────┐
       ↓              ↓              ↓
    对象查询        集合查询        集合查询
       │              │              │
       ↓              ↓              ↓
 对公支付控制      部门列表        职级列表

优点是:

1. SQL 比较清晰

每个 SQL 只负责自己的业务。

2. Java 对象结构清晰

一个字段对应一个对象或者一个集合。

3. 避免复杂 JOIN

特别是多个一对多关系同时存在的时候。

4. 业务维护方便

例如以后增加:

text 复制代码
黑名单

只需要增加:

xml 复制代码
<collection property="blackList"
            column="configId"
            select="selectBlackList">
</collection>

而不需要修改一个非常复杂的多表 JOIN。


十六、但是这种方案存在一个非常重要的问题:N+1 查询

虽然 association + collection + select 非常方便,但是它也存在一个经典问题:

N+1 Query Problem(N+1 查询问题)

例如:

text 复制代码
主查询查询出 100 个配置

每个配置又需要查询:

text 复制代码
corporatePayControl
supplierList
deptList
rankList
applyUsers
notApplyUsers
notApplyRoles
timeList
positionList

假设有 9 个子查询。

那么可能出现:

text 复制代码
1 次主查询
+
100 × 9 次子查询
=
901 次 SQL

也就是:

text 复制代码
N + 1

这在数据量比较大的情况下可能造成严重的数据库压力。


十七、为什么 N+1 查询性能比较差?

假设一次 SQL 平均耗时:

text 复制代码
10ms

理论上:

text 复制代码
1 次查询 ≈ 10ms

但是如果执行:

text 复制代码
901 次查询

即使每次只有 10ms:

text 复制代码
901 × 10ms
≈ 9010ms
≈ 9 秒

实际项目中还要考虑:

text 复制代码
数据库连接池
网络延迟
数据库负载
锁等待
SQL 执行计划
并发请求

所以实际耗时可能更高。

因此:

association/collection + select 不是性能差,而是需要根据数据量和查询场景谨慎使用。


十八、什么时候适合使用 association + collection?

比较适合:

text 复制代码
查询单个详情

例如:

text 复制代码
根据 configId 查询配置详情

因为:

text 复制代码
一个 configId
→ 一个主对象
→ 若干关联数据

这种情况下即使执行多个子查询,数量也是有限的。

例如:

text 复制代码
1 次主查询
+
9 次子查询
=
10 次 SQL

通常是可以接受的。


十九、什么时候应该谨慎使用?

如果是:

text 复制代码
查询列表

例如:

text 复制代码
查询 1000 个配置

同时每个配置又有多个:

text 复制代码
collection

那么就要特别小心。

可能变成:

text 复制代码
1
+
1000 × 9
=
9001 次 SQL

这种情况下就应该考虑其他方案,例如:

text 复制代码
批量查询
JOIN
IN 查询
一次查询后 Java 分组

二十、一个更推荐的批量查询思路

例如主查询:

text 复制代码
查询 100 个 configId

不要:

text 复制代码
每个 configId 查询一次部门

而可以:

sql 复制代码
select *
from car_bsc_car_scene_config_use_range
where car_config_id in (...)

一次把所有部门查出来。

然后 Java 根据:

text 复制代码
car_config_id

进行分组:

java 复制代码
Map<Long, List<ConfigUseRangeVo>> deptMap;

最终:

text 复制代码
configId = 10001
    ↓
deptMap.get(10001)

configId = 10002
    ↓
deptMap.get(10002)

这样可以把:

text 复制代码
100 次 SQL

降低为:

text 复制代码
1 次 SQL

这也是实际项目中优化 N+1 查询非常常见的方法。


二十一、association 和 collection 的核心区别总结

可以通过下面这个表快速记忆:

特性 association collection
用途 关联一个对象 关联多个对象
Java 类型 Object List / Set
典型关系 一对一 一对多
示例 CorporatePayControlVo List<ConfigUseRangeVo>
常用属性 property、column、javaType、select property、column、ofType、javaType、select
是否可以嵌套查询 可以 可以
是否可能产生 N+1 是 是

二十二、最重要的几个属性

实际开发中,可以重点记住:

xml 复制代码
<association
    property="..."
    column="..."
    javaType="..."
    select="..."
/>

和:

xml 复制代码
<collection
    property="..."
    column="..."
    javaType="..."
    ofType="..."
    select="..."
/>

可以记成:

text 复制代码
property
    ↓
结果放到 Java 对象的哪个属性

column
    ↓
给嵌套 SQL 传什么参数

javaType
    ↓
这个属性本身是什么类型

ofType
    ↓
collection 中每个元素是什么类型

select
    ↓
调用哪个 SQL

二十三、用一句话理解整个机制

如果让我把这段 MyBatis 代码压缩成一句话:

主 SQL 负责查询主对象,resultMap 负责对象组装,association 负责组装一个关联对象,collection 负责组装一个关联集合,而 select 则负责告诉 MyBatis 到哪个 SQL 中查询这些关联数据。

整个过程可以抽象成:

text 复制代码
数据库
  │
  ↓
主查询
  │
  ↓
QueryCarConfigDetailVo
  │
  ├── association
  │       ↓
  │   一个关联对象
  │
  ├── collection
  │       ↓
  │   一个关联集合
  │
  ├── collection
  │       ↓
  │   一个关联集合
  │
  └── collection
          ↓
      一个关联集合

因此,看到 MyBatis 中:

xml 复制代码
<association>

你可以第一时间想到:

"这是给 Java 对象里的某个对象属性赋值。"

看到:

xml 复制代码
<collection>

就想到:

"这是给 Java 对象里的某个 List/Set 属性赋值。"

看到:

xml 复制代码
select="xxx"

就想到:

"这个属性的数据需要再调用另一个 SQL 查询。"

看到:

xml 复制代码
column="{a = a, b = b}"

就想到:

"这里是在给嵌套 SQL 传多个参数。"

掌握这几个核心概念之后,再去阅读复杂的 MyBatis resultMap,基本就不会觉得这些 XML 很难理解了。


二十四、最后总结

MyBatis 的 association 和 collection 本质上都是为了解决一个问题:

如何把数据库中的关联数据,组装成结构复杂的 Java 对象。

最核心的关系是:

text 复制代码
association
    ↓
一个对象

collection
    ↓
多个对象

而:

text 复制代码
resultMap

则负责描述整个对象的组装规则。

例如:

java 复制代码
QueryCarConfigDetailVo

可以最终被组装成:

text 复制代码
QueryCarConfigDetailVo
│
├── corporatePayControl : CorporatePayControlVo
│
├── supplierList        : List<SupplierVo>
│
├── deptList            : List<ConfigUseRangeVo>
│
├── rankList            : List<ConfigUseRangeVo>
│
├── applyUsers          : List<ConfigUseRangeVo>
│
├── notApplyUsers       : List<ConfigUseRangeVo>
│
├── notApplyRoles       : List<ConfigUseRangeVo>
│
├── timeList            : List<TimeControlVo>
│
└── positionList        : List<PositionControlVo>

这就是 MyBatis resultMap + association + collection 最典型的使用场景。

不过在实际开发中,还需要特别注意:

text 复制代码
association/collection + select

可能带来的:

text 复制代码
N+1 查询问题

因此:

详情查询可以考虑这种写法;大批量列表查询则应该重点关注 SQL 次数和数据库压力,必要时采用批量查询、JOIN 或 Java 分组的方式进行优化。

相关推荐
code2cat1 小时前
Java进阶篇之AtomicLongArray:大数累加、上限检查与溢出边界
java·开发语言
开开心心_Every1 小时前
电脑OCR识别软件支持表格识别图片转Excel
java·开发语言·微信·计算机外设·ocr·intellij-idea·excel
弯_弯1 小时前
企业知识库 RAG 落地:从 Demo 到生产,四条工程链路与参数级自查
android·java·javascript
rannn_1111 小时前
JVM 垃圾回收面试题全解析:从基础到进阶
java·开发语言·jvm·后端·面试
Wang's Blog1 小时前
Java框架 SpringCloud 快速入门: Ribbon 饥饿加载
java·spring cloud·ribbon
天空鸟_时光不老2 小时前
10-Agent安全隐患与SQL执行层加固
java·数据库·人工智能·sql·spring·maven·mybatis
Nebula_g3 小时前
JavaSE加强:IO流
java·开发语言·windows·stream·javase
rannn_1113 小时前
JVM 面试题:类加载过程详解(附高频考点)
java·jvm·后端
程序猿乐锅7 小时前
从0-1一文详解RabbitMQ
java·分布式·后端·中间件·rabbitmq·ruby