如何用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
相关产品推荐
相关产品推荐

