在 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 序列化/反序列化
类型转换异常需要额外处理