2098 字
约 6 分钟
1
第38篇-一对一一对多查询

第38篇-一对一一对多查询

来源: https://bugstack.cn/md/java/interview/2026-05-26-面经手册 · 第38篇《MyBatis 一对一、一对多怎么查?延迟加载原理和 N+1 问题怎么解?》.html


面经手册 · 第38篇《MyBatis 一对一、一对多怎么查?延迟加载原理和 N+1 问题怎么解?》

作者:小傅哥 **博客:https://bugstack.cn 沉淀、分享、成长,让自己和他人都能有所收获!😄

一、前言

实际业务中,数据表之间的关系几乎不可能是一张表独立存在的。用户有订单、订单有商品、商品有分类……关联查询是日常开发的高频操作。 MyBatis 中 association 和 collection 是处理关联查询的两大标签,延迟加载是解决性能问题的关键配置,N+1 问题更是面试必问的经典坑。

二、面试题

谢飞机,小记!,面试继续。 面试官**:MyBatis 一对一关联查询怎么配置?

谢飞机:用 association 标签。

面试官:嵌套查询和嵌套结果有什么区别?

谢飞机:嵌套查询是两条 SQL,嵌套结果是一条 SQL?

面试官:对,那嵌套查询有什么性能问题?

谢飞机:N+1?

面试官:怎么解决 N+1?

谢飞机:用延迟加载?

面试官:延迟加载原理是什么?

谢飞机:代理?动态代理?

面试官:什么代理?JDK 还是 CGLIB?代理对象是怎么创建的?

谢飞机:我……再见!ヾ( ̄▽ ̄)

三、一对一关联 — association

1. 数据模型

user(用户表)          id_card(身份证表)
┌──────┐              ┌──────────────┐
│ id   │              │ id           │
│ name │    ──── 1:1 ─│ user_id      │
│ age  │              │ card_no      │
│      │              │ address      │
└──────┘              └──────────────┘

1**2 3 4 5 6 7

2. 嵌套结果(推荐,一条 SQL)

resultMap id="userWithCardMap" type="com.example.entity.User">
    id column="id" property="id"/>
    result column="name" property="name"/>
    result column="age" property="age"/>

    association property="idCard" javaType="com.example.entity.IdCard">
        id column="card_id" property="id"/>
        result column="card_no" property="cardNo"/>
        result column="card_address" property="address"/>
    association>
resultMap>

select id="findUserWithCard" resultMap="userWithCardMap">
    SELECT
        u.id, u.name, u.age,
        c.id AS card_id, c.card_no, c.address AS card_address
    FROM user u
    LEFT JOIN id_card c ON u.id = c.user_id
    WHERE u.id = #{id}
select>

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

3. 嵌套查询(两条 SQL,需延迟加载配合)

resultMap id="userLazyMap" type="com.example.entity.User">
    id column="id" property="id"/>
    result column="name" property="name"/>

    association property="idCard"
                 javaType="com.example.entity.IdCard"
                 select="com.example.mapper.IdCardMapper.findByUserId"
                 column="id"
                 fetchType="lazy"/>
resultMap>

select id="findById" resultMap="userLazyMap">
    SELECT id, name, age FROM user WHERE id = #{id}
select>

1 2 3 4 5 6 7 8 9 10 11 12 13 14


select id="findByUserId" resultType="com.example.entity.IdCard">
    SELECT id, card_no, address FROM id_card WHERE user_id = #{userId}
select>

1 2 3 4

四、一对多关联 — collection

1. 数据模型

user(用户表)          orders(订单表)
┌──────┐              ┌──────────────┐
│ id   │              │ id           │
│ name │    ── 1:N ── │ user_id      │
│ age  │              │ order_no     │
│      │              │ amount       │
└──────┘              └──────────────┘

1 2 3 4 5 6 7

2. 嵌套结果(一条 SQL)

resultMap id="userWithOrdersMap" type="com.example.entity.User">
    id column="id" property="id"/>
    result column="name" property="name"/>

    collection property="orders" ofType="com.example.entity.Order">
        id column="order_id" property="id"/>
        result column="order_no" property="orderNo"/>
        result column="amount" property="amount"/>
    collection>
resultMap>

select id="findUserWithOrders" resultMap="userWithOrdersMap">
    SELECT
        u.id, u.name,
        o.id AS order_id, o.order_no, o.amount
    FROM user u
    LEFT JOIN orders o ON u.id = o.user_id
    WHERE u.id = #{id}
select>

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 注意**:ofType 指定集合中元素的类型,不是 javaType

3. 嵌套查询(两条 SQL)

resultMap id="userLazyOrdersMap" type="com.example.entity.User">
    id column="id" property="id"/>
    result column="name" property="name"/>
    collection property="orders"
                ofType="com.example.entity.Order"
                select="com.example.mapper.OrderMapper.findByUserId"
                column="id"
                fetchType="lazy"/>
resultMap>

1**2 3 4 5 6 7 8 9

五、延迟加载原理

1. 配置方式


settings>

    setting name="lazyLoadingEnabled" value="true"/>

    setting name="aggressiveLazyLoading" value="false"/>
settings>

1 2 3 4 5 6 7

2. 局部配置覆盖全局


association property="idCard" fetchType="eager" .../>

association property="idCard" fetchType="lazy" .../>

1 2 3 4 5

3. 底层原理:代理对象

延迟加载的核心:返回的不是目标对象本身,而是一个代理对象**。

正常加载(eager):
  查 user → 执行 SQL → 获取 user 对象(真实对象)

延迟加载(lazy):
  查 user → 执行 SQL → 获取 user 代理对象
  ↓ 访问 user.getIdCard() 时
  触发代理拦截 → 执行关联 SQL → 获取 idCard → 注入到 user 对象
  ↓ 再次访问
  直接返回已加载的 idCard(不再执行 SQL)

1**2 3 4 5 6 7 8 9

4. 源码追踪

代理创建**:

// org.apache.ibatis.executor.resultset.DefaultResultSetHandler
private Object createResultObject(ResultSetWrapper rsw,
        ResultMap resultMap, ...) {
    // 检查是否需要延迟加载
    if (hasNestedResultMaps(rsw, resultMap)) {
        // 创建代理对象
        return createProxyObject(resultMap, ...);
    }
    return objectFactory.create(resultType);
}

// 代理工厂(CGLIB 或 Javassist)
private Object createProxyObject(ResultMap resultMap, ...) {
    return proxyFactory.createProxy(resultObject,
        new ResultLoaderMap(), lazyLoader, configuration, ...);
}

12 3 4 5 6 7 8 9 10 11 12 13 14 15 16 代理拦截

// org.apache.ibatis.executor.loader.javassist.JavassistProxyFactory
public class JavassistProxyFactory implements ProxyFactory {

    @Override
    public Object createProxy(Object target, ...) {
        return EnhancedResultObjectProxyImpl.createProxy(target, ...);
    }
}

// 代理拦截逻辑
public static class EnhancedResultObjectProxyImpl implements MethodHandler {
    @Override
    public Object invoke(Object enhanced, Method method,
            Method proxyMethod, Object[] args) {
        String methodName = method.getName();
        Object value;

        // 检查是否是关联属性的 getter 方法
        if (lazyLoader.size() > 0 && !FINALIZE_METHOD.equals(methodName)) {
            // 触发延迟加载
            value = lazyLoader.load(finalizeMethod);
        }

        // 加载完成后调用原始方法
        return proxyMethod.invoke(target, args);
    }
}

1**2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27

5. 代理实现选择


settings>

    setting name="proxyFactory" value="JAVASSIST"/>
settings>

1 2 3 4 5 实现 特点 JAVASSIST(默认) 字节码生成,启动快,运行稍慢 CGLIB 字节码生成,功能强大,需要额外依赖

六、N+1 问题

1. 什么是 N+1

场景:查询 10 个用户,每个用户关联一张身份证

嵌套查询模式(N+1):
  第 1 条 SQL:SELECT * FROM user(查出 10 个用户)
  第 2 条 SQL:SELECT * FROM id_card WHERE user_id = 1
  第 3 条 SQL:SELECT * FROM id_card WHERE user_id = 2
  ...
  第 11 条 SQL:SELECT * FROM id_card WHERE user_id = 10

  总共 11 条 SQL = 1 + 10 = N+1

1 2 3 4 5 6 7 8 9 10

2. 解决方案

方案一:嵌套结果(JOIN)**


select id="findUserWithCard" resultMap="userWithCardMap">
    SELECT u.*, c.* FROM user u
    LEFT JOIN id_card c ON u.id = c.user_id
select>

12 3 4 5 方案二:延迟加载


setting name="lazyLoadingEnabled" value="true"/>

12 方案三:批量查询(fetchSize)

association property="idCard"
             select="com.example.mapper.IdCardMapper.batchFindByUserIds"
             column="id"
             fetchType="lazy"/>

1**2 3 4


select id="batchFindByUserIds" resultType="com.example.entity.IdCard">
    SELECT * FROM id_card WHERE user_id IN
    foreach collection="list" item="userId" open="(" separator="," close=")">
        #{userId}
    foreach>
select>

1 2 3 4 5 6 7 方案四:二级缓存**


cache/>

1**2

3. 各方案对比

方案 SQL 数量 适用场景 注意事项 JOIN 嵌套结果 1 条 关联数据确定需要 JOIN 数据量大时性能下降 延迟加载 按需 关联数据不一定用到 仍然可能 N+1 批量 fetchSize 1 + 1 = 2 嵌套查询 需配置 batch 二级缓存 首次 N+1,后续 0 数据变更少 缓存一致性问题

七、常见面试追问

Q1:association 和 collection 能嵌套使用吗?

能。可以在 collection 中嵌套 association,实现三层甚至更深的关联。但层级越深,SQL 复杂度和性能影响越大。

Q2:延迟加载对序列化有影响吗?

有。代理对象序列化时可能丢失关联数据。解决:在序列化前先访问关联属性触发加载,或配置 serialization 代理工厂。

Q3:fetchType 优先级?

局部 fetchType 优先级高于全局 lazyLoadingEnabled 配置。fetchType="eager" 强制立即加载,fetchType="lazy" 强制延迟加载。

八、总结

记住三个核心要点:

1. 关联查询两种方式
   嵌套结果(JOIN 一条 SQL):推荐,性能好
   嵌套查询(两条 SQL):灵活,但可能 N+1

2. 延迟加载原理
   返回代理对象 → 访问关联属性时拦截 → 执行关联 SQL
   通过 Javassist/CGLIB 创建代理,invoke() 触发加载

3. N+1 问题解决
   优先用 JOIN 嵌套结果(1条SQL)
   或延迟加载 + 批量 fetchSize(2条SQL)
   二级缓存可作为辅助

1 2 3 4 5 6 7 8 9 10 11 12 13 14 面试回答模板**:

MyBatis 通过 association 和 collection 标签处理一对一和一对多关联查询。两种实现方式:嵌套结果用 JOIN 一条 SQL 查完,推荐使用;嵌套查询分两条 SQL 执行,灵活但可能产生 N+1 问题。 延迟加载的原理是返回代理对象而非真实对象,通过 Javassist 或 CGLIB 创建代理,拦截 getter 方法,在访问关联属性时才触发关联 SQL 执行。配置上全局开启 lazyLoadingEnabled=true,局部可通过 fetchType 覆盖。 N+1 问题的最优解是嵌套结果用 JOIN 查询,如果必须用嵌套查询,则配合延迟加载和批量 fetchSize 来减少 SQL 次数。

第38篇-一对一一对多查询
http://www.clxhxhhr.top/posts/983/
作者
clxstart
发布于
2026-09-18
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。