在 MyBatis 中处理一对多关系(一个对象包含 List<T> 属性)时,有多种解决方案。本文介绍四种常用方案:分步查询、及联查询、嵌套查询、使用 PostgreSQL 的 JSON_AGG 函数

分步查询

原理

分两次查询:先查询主对象,再根据主对象 ID 查询关联的列表数据。

代码示例

@Service
public class EarthquakeConfigService {
    @Autowired
    private EarthquakeConfigMapper configMapper;
    
    //单个对象查询
    public SearchEarthquakeRiskCumulativeConfigDetailVo getDetail(Long id) {
        // 1. 查询主表
        SearchEarthquakeRiskCumulativeConfigDetailVo detail = 
            configMapper.selectMainById(id);
        
        if (detail != null) {
            // 2. 查询关联列表
            List<String> riskCodes = 
                configMapper.selectRiskCodesByConfigId(id);
            detail.setRiskCodeList(riskCodes);
        }
        
        return detail;
    }
    
    // 批量查询(解决 N+1 问题)
    public List<SearchEarthquakeRiskCumulativeConfigDetailVo> batchGetDetails(
            List<Long> ids) {
        // 1. 批量查询主表
        List<SearchEarthquakeRiskCumulativeConfigDetailVo> details = 
            configMapper.selectMainByIds(ids);
        
        // 2. 批量查询所有关联数据
        List<RiskCodeDto> allRiskCodes = 
            configMapper.selectRiskCodesByConfigIds(ids);
        
        // 3. 组装数据
        Map<Long, List<String>> riskCodeMap = allRiskCodes.stream()
            .collect(Collectors.groupingBy(
                RiskCodeDto::getConfigId,
                Collectors.mapping(RiskCodeDto::getRiskCode, Collectors.toList())
            ));
        
        details.forEach(detail -> 
            detail.setRiskCodeList(riskCodeMap.getOrDefault(
                detail.getConfigMainId(), Collections.emptyList())));
        
        return details;
    }
}

优点

代码结构清晰,易于理解和维护

支持复杂的业务逻辑处理

便于缓存策略的实现

适用于分页查询场景

缺点

需要多次数据库查询

需要手动组装数据

嵌套查询

原理

使用 MyBatis 的 <collection> 标签在 XML 中配置关联查询,一次性查询出所有数据。

代码示例

<resultMap id="earthquakeRiskCumulativeConfigDetailMap" type="SearchEarthquakeRiskCumulativeConfigDetailVo">
        <id column="config_main_id" property="configMainId"/>
        <result column="region_name" property="regionName"/>
        <result column="geometry" property="geometry"/>
        <result column="center_lat" property="centerLat"/>
        <result column="center_lon" property="centerLon"/>
        <result column="remark" property="remark"/>
        <result column="risk_threshold_amount" property="riskThresholdAmount"/>
        <result column="operator_org_name" property="operatorOrgName"/>
        <result column="operator_user_name" property="operatorUserName"/>
        <collection property="riskCodeList" ofType="string">
            <result column="risk_code"/>
        </collection>
    </resultMap>
    
    <select id="searchEarthquakeRiskCumulativeDetail"
            resultMap="earthquakeRiskCumulativeConfigDetailMap">
        select m.config_main_id,
               m.region_name,
               m.center_lon,
               m.center_lat,
               m.risk_threshold_amount,
               m.remark,
               m.operator_org_name,
               concat(m.created_by_name, '-', m.created_by_emp_no) as operator_user_name,
               st_asgeojson(g.geometry) as geometry,
        c.risk_code
        from nvgis_risk_cumulative_config_main m
                 join nvgis_risk_cumulative_config_custom_geom g on m.config_main_id = g.config_main_id
        left join nvgis_risk_cumulative_config_class_code c on c.config_main_id=m.config_main_id
        where m.status = 1
          and m.config_main_id = #{params.configMainId}
          AND m.risk_graph_type = '4'
    </select>

优点

一次查询获取所有数据

减少数据库连接次数

MyBatis 自动处理结果映射

缺点

SQL 较复杂,特别是需要分页时

返回结果需要手动处理重复数据

不适合大量数据的分页查询

及联查询(不推荐)

原理

使用 MyBatis 的<collection> 标签在 XML 中配置关联查询,一次性查询出所有数据。

代码示例

<resultMap id="earthquakeRiskCumulativeConfigDetailMap" type="SearchEarthquakeRiskCumulativeConfigDetailVo">
        <id column="config_main_id" property="configMainId"/>
        <result column="region_name" property="regionName"/>
        <result column="geometry" property="geometry"/>
        <result column="center_lat" property="centerLat"/>
        <result column="center_lon" property="centerLon"/>
        <result column="remark" property="remark"/>
        <result column="risk_threshold_amount" property="riskThresholdAmount"/>
        <result column="operator_org_name" property="operatorOrgName"/>
        <result column="operator_user_name" property="operatorUserName"/>
        <collection property="riskCodeList" column="config_main_id" select="selectRiskCodeList" fetchType="eager"/>
    </resultMap>
    
    <select id="selectRiskCodeList" resultType="java.lang.String">
        select c.risk_code from nvgis_risk_cumulative_config_class_code c where config_main_id=#{configMainId}
    </select>

<select id="searchEarthquakeRiskCumulativeDetail"
            resultMap="earthquakeRiskCumulativeConfigDetailMap">
        select m.config_main_id,
               m.region_name,
               m.center_lon,
               m.center_lat,
               m.risk_threshold_amount,
               m.remark,
               m.operator_org_name,
               concat(m.created_by_name, '-', m.created_by_emp_no) as operator_user_name,
               st_asgeojson(g.geometry) as geometry
        from nvgis_risk_cumulative_config_main m
                 join nvgis_risk_cumulative_config_custom_geom g on m.config_main_id =
                                                                    g.config_main_id
        where m.status = 1
          and m.config_main_id = #{params.configMainId}
          AND m.risk_graph_type = '4'
    </select>

优点

代码清晰,易于维护

支持延迟加载(Lazy Loading)

避免复杂 JOIN 的数据膨胀

灵活的参数传递

支持复杂业务逻辑

缺点

N+1 查询问题(最严重的问题)

性能差,特别是大数据量时

数据库连接压力大

分页问题复杂

调试困难

使用PostgreSQL的JSON函数

原理

使用 PostgreSQL 的 JSON_AGG 函数将关联数据聚合为 JSON 数组,然后使用 JacksonTypeHandler 自动转换为 List

代码示例

<resultMap id="earthquakeRiskCumulativeConfigDetailMap" type="SearchEarthquakeRiskCumulativeConfigDetailVo">
        <id column="config_main_id" property="configMainId"/>
        <result column="region_name" property="regionName"/>
        <result column="geometry" property="geometry"/>
        <result column="center_lat" property="centerLat"/>
        <result column="center_lon" property="centerLon"/>
        <result column="remark" property="remark"/>
        <result column="risk_threshold_amount" property="riskThresholdAmount"/>
        <result column="operator_org_name" property="operatorOrgName"/>
        <result column="operator_user_name" property="operatorUserName"/>
        <result property="riskCodeList" column="risk_codes_json"
                typeHandler="com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler"/>
    </resultMap>

<select id="searchEarthquakeRiskCumulativeDetail"
            resultMap="earthquakeRiskCumulativeConfigDetailMap">
        select m.config_main_id,
               m.region_name,
               m.center_lon,
               m.center_lat,
               m.risk_threshold_amount,
               m.remark,
               m.operator_org_name,
               concat(m.created_by_name, '-', m.created_by_emp_no) as operator_user_name,
               st_asgeojson(g.geometry) as geometry,
        COALESCE(
        JSON_AGG(c.risk_code) FILTER (WHERE c.risk_code IS NOT NULL),
        '[]'::json
        ) as risk_codes_json
        from nvgis_risk_cumulative_config_main m
                 join nvgis_risk_cumulative_config_custom_geom g on m.config_main_id =
                                                                    g.config_main_id
        left join nvgis_risk_cumulative_config_class_code c on c.config_main_id=m.config_main_id
        where m.status = 1
          and m.config_main_id = #{params.configMainId}
          AND m.risk_graph_type = '4'
        group by m.config_main_id,g.geometry
    </select>

优点

一次查询完成,性能好

代码简洁,易于维护

自动类型转换,无需手动处理结果集

支持复杂嵌套结构

缺点

依赖 PostgreSQL 数据库特性

需要处理 JSON 序列化/反序列化

类型转换异常需要额外处理