You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

@Cacheable缓存JOOQ查询结果时DTO内部列表重复问题排查

问题原因分析

你的问题本质出在两个地方:JOOQ结果转换逻辑的缺陷,加上后续业务合并操作触发了缓存数据的重复修改。

1. JOOQ查询结果未合并父实体

你的查询是MY_TABLE和MY_INNER_TABLE的关联查询,每一条子表记录都会和父表记录生成一行结果。比如一个父表记录对应3条子表记录,查询会返回3行Record。而你的createMyDtoFromRecord方法是每一行Record都生成一个独立的MyDto对象,每个MyDto的innerDtos只包含当前行对应的子实体。这就导致第一次查询返回的List<MyDto>里,会有多个ID和名称完全相同的MyDto实例,每个实例的innerDtos只有一个子元素。

2. 缓存+业务合并导致重复元素累积

你用@Cacheable缓存了这个包含重复MyDto的List。如果后续业务代码里存在按ID合并MyDto的逻辑(比如把同ID的MyDto的innerDtos合并到同一个对象中),那么每次从缓存获取这个List时,都会重复执行合并操作:把缓存里的3个MyDto的innerDtos再次添加到同一个合并后的MyDto里,导致innerDtos的元素数量每次增加3。

至于你添加@EqualsAndHashCode后查询返回空列表,是因为@Data注解已经自动包含了@EqualsAndHashCode(默认用所有字段生成equals和hashCode),手动添加可能覆盖了默认逻辑,或者在合并/分组时,错误的equals判断导致没有匹配到同ID的MyDto,最终得到空结果。

解决方案

修复JOOQ结果转换逻辑,合并父实体

不要让每一行Record都生成新的MyDto,而是用JOOQ的fetchGroups方法按父表ID分组,自动合并子实体:

@Cacheable(CachingConfig.OPERATORS)
public List<MyDto> getAllOperatorsWithAliases() {
    // 先按父表ID分组,获取每个父ID对应的所有子记录
    Map<Long, Result<Record>> grouped = create.select(Tables.MY_TABLE.ID,
                    Tables.MY_TABLE.NAME,
                    Tables.MY_INNER_TABLE.ID,
                    Tables.MY_INNER_TABLE.ALIAS,
                    Tables.MY_INNER_TABLE.PARENT_ID,
                    Tables.MY_INNER_TABLE.IS_MAIN)
            .from(Tables.MY_TABLE)
            .join(Tables.MY_INNER_TABLE)
            .on(Tables.MY_TABLE.ID.eq(Tables.MY_INNER_TABLE.PARENT_ID))
            .fetchGroups(Tables.MY_TABLE.ID);

    // 遍历分组结果,生成合并后的MyDto
    return grouped.entrySet().stream()
            .map(entry -> {
                Long parentId = entry.getKey();
                Result<Record> records = entry.getValue();
                // 取父表的基础信息(所有子记录对应的父信息都一样)
                Record firstRecord = records.get(0);
                MyDto myDto = new MyDto();
                myDto.setId(parentId);
                myDto.setName(firstRecord.get(Tables.MY_TABLE.NAME));
                // 转换所有子记录为MyInnerDto,组成列表
                List<MyInnerDto> innerDtos = records.stream()
                        .map(record -> {
                            MyInnerDto innerDto = new MyInnerDto();
                            innerDto.setId(record.get(Tables.MY_INNER_TABLE.ID));
                            innerDto.setAlias(record.get(Tables.MY_INNER_TABLE.ALIAS));
                            innerDto.setParentId(record.get(Tables.MY_INNER_TABLE.PARENT_ID));
                            innerDto.setIsMain(record.get(Tables.MY_INNER_TABLE.IS_MAIN) == 1);
                            return innerDto;
                        })
                        .collect(Collectors.toList());
                myDto.setInnerDtos(innerDtos);
                return myDto;
            })
            .collect(Collectors.toList());
}

避免缓存后修改缓存对象

如果必须保留原有的转换逻辑,一定要确保缓存后的List和其中的MyDto、MyInnerDto都是不可变的,避免业务代码修改缓存内的对象。比如把innerDtos设置为不可变列表:

// 在createMyDtoFromRecord里用不可变列表
myDto.setInnerDtos(Collections.unmodifiableList(Collections.singletonList(myInnerDto)));

这样后续代码无法修改缓存内的列表,也就不会出现重复元素累积的问题。

内容的提问来源于stack exchange,提问作者barisc

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.20 00:50:13