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

如何用MapStruct将数据库JOIN得到的扁平列表转为结构化列表?

用MapStruct实现反扁平化映射的方案

1. 定义基础映射接口

先创建MapStruct映射接口,明确单个对象的转换规则:

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import java.util.List;
import java.util.stream.Collectors;

@Mapper(componentModel = "spring") // 可根据项目选择"default"等其他组件模型
public interface FlatToAMapper {

    // 映射Flat的a字段到A,暂时忽略children字段
    @Mapping(target = "children", ignore = true)
    A flatToA(Flat flat);

    // 映射Flat的b字段到B
    B flatToB(Flat flat);

    // 批量转换Flat为不含children的A列表
    List<A> flatsToAs(List<Flat> flats);

    // 批量转换Flat为B列表
    List<B> flatsToBs(List<Flat> flats);
}

2. 实现分组聚合逻辑

MapStruct本身不直接处理分组,需结合Java流完成相同A属性的Flat聚合:

方式一:在映射接口中添加默认方法

在FlatToAMapper接口内新增默认方法,完成分组与children填充:

default List<A> map(List<Flat> source) {
    return source.stream()
        .collect(Collectors.groupingBy(
            // 用a1/a2/a3拼接字符串作为分组键,避免重写A的equals/hashCode
            flat -> String.join("|", flat.getA1(), flat.getA2(), flat.getA3()),
            Collectors.mapping(this::flatToB, Collectors.toList())
        ))
        .entrySet()
        .stream()
        .map(entry -> {
            String[] keys = entry.getKey().split("\\|");
            A a = new A();
            a.setA1(keys[0]);
            a.setA2(keys[1]);
            a.setA3(keys[2]);
            a.setChildren(entry.getValue());
            return a;
        })
        .collect(Collectors.toList());
}

如果愿意给A类重写equals()和hashCode()(基于a1/a2/a3字段),可以简化分组逻辑:

default List<A> map(List<Flat> source) {
    return source.stream()
        .collect(Collectors.groupingBy(
            this::flatToA,
            Collectors.mapping(this::flatToB, Collectors.toList())
        ))
        .entrySet()
        .stream()
        .map(entry -> {
            A a = entry.getKey();
            a.setChildren(entry.getValue());
            return a;
        })
        .collect(Collectors.toList());
}

方式二:在业务服务中处理

若不想在映射接口中混入业务逻辑,可在服务类中注入MapStruct生成的mapper后处理:

@Service
public class FlatMappingService {

    private final FlatToAMapper mapper;

    public FlatMappingService(FlatToAMapper mapper) {
        this.mapper = mapper;
    }

    public List<A> map(List<Flat> source) {
        return source.stream()
            .collect(Collectors.groupingBy(
                // 用嵌套Entry作为分组键,无需修改A类
                flat -> new AbstractMap.SimpleEntry<>(
                    new AbstractMap.SimpleEntry<>(flat.getA1(), flat.getA2()),
                    flat.getA3()
                ),
                Collectors.mapping(mapper::flatToB, Collectors.toList())
            ))
            .entrySet()
            .stream()
            .map(entry -> {
                A a = new A();
                a.setA1(entry.getKey().getKey().getKey());
                a.setA2(entry.getKey().getKey().getValue());
                a.setA3(entry.getKey().getValue());
                a.setChildren(entry.getValue());
                return a;
            })
            .collect(Collectors.toList());
    }
}

3. 关键注意事项

  • 分组键的选择:可根据项目情况选择字符串拼接、自定义键对象,或是给A类重写equals/hashCode。
  • 组件模型:若使用Spring框架,设置componentModel = "spring"方便依赖注入;普通Java项目用"default"即可。
  • 空值处理:若Flat的a字段可能为空,需在分组逻辑中提前处理空值,避免出现异常。

内容的提问来源于stack exchange,提问作者luca.vercelli

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.17 11:44:50