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

如何使用MapStruct或Java Stream将嵌套Entity转换为DTO?

问题描述

在Spring Boot应用中,我常用Projection返回联表查询结果,现在尝试用Java Stream将嵌套实体映射为DTO,场景如下:

说明:Hibernate实体关系配置已完成,无需关注,聚焦实体到嵌套DTO的映射。

实体关系:Country包含多个State,State包含多个Town,实体类代码如下:

Country实体类

public class Country {
    private Long id;
    private String name;
    @OneToMany(...)
    private Set<State> states = new HashSet<>();
    // getter/setter
}

State实体类

public class State {
    private Long id;
    private Long population;
    @ManyToOne(...)
    private Country country;
    @OneToMany(...)
    private Set<Town> towns = new HashSet<>();
    // getter/setter
}

Town实体类

public class Town {
    private Long id;
    private String name;
    @ManyToOne()
    private State state;
    // getter/setter
}

需求:获取包含国家名称、该国所有州的人口总和、该国所属城镇列表的国家列表,已创建DTO但不知道如何完成映射(不想多次查询数据库,当前获取的Country列表已包含所有关联数据),DTO代码如下:

@Data
public class CountryResponse {
    private Long id;
    private String name;
    private Set<StateResponse> states;
    private Long population;
    private Set<TownResponse> towns;

    public CountryResponse(Country country) {
        this.id = country.getId();
        this.name = country.getName();
        // 此处不知道如何正确映射
        this.states = country.getStates.stream().map(StateResponse::new)
            .collect(Collectors.toSet());
        this.towns = this.states.stream().map(TownResponse::new)
            .collect(Collectors.toSet());
    }
}

请问该如何实现?如果MapStruct更适合,也可以使用。


解决方案

一、Java Stream手动映射

先完善子DTO StateResponse 和 TownResponse:

@Data
public class StateResponse {
    private Long id;
    private Long population;

    public StateResponse(State state) {
        this.id = state.getId();
        this.population = state.getPopulation();
    }
}

@Data
public class TownResponse {
    private Long id;
    private String name;

    public TownResponse(Town town) {
        this.id = town.getId();
        this.name = town.getName();
    }
}

修改CountryResponse的构造方法,完成所有映射逻辑:

@Data
public class CountryResponse {
    private Long id;
    private String name;
    private Set<StateResponse> states;
    private Long population;
    private Set<TownResponse> towns;

    public CountryResponse(Country country) {
        this.id = country.getId();
        this.name = country.getName();
        
        // 映射州列表
        this.states = country.getStates().stream()
                .map(StateResponse::new)
                .collect(Collectors.toSet());
        
        // 计算所有州的人口总和,处理null值避免空指针
        this.population = country.getStates().stream()
                .map(State::getPopulation)
                .filter(Objects::nonNull)
                .reduce(0L, Long::sum);
        
        // 提取所有城镇:遍历所有州的城镇集合,再映射为TownResponse
        this.towns = country.getStates().stream()
                .flatMap(state -> state.getTowns().stream())
                .map(TownResponse::new)
                .collect(Collectors.toSet());
    }
}

这样就能从已加载的Country实体中一次性完成所有数据映射,无需额外查询数据库。

二、MapStruct实现(更简洁易维护)

MapStruct适合复杂嵌套结构的实体-DTO映射,编译时自动生成映射代码,避免手动写Stream的冗余逻辑。

1. 引入Maven依赖

<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct</artifactId>
    <version>1.5.5.Final</version>
</dependency>
<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct-processor</artifactId>
    <version>1.5.5.Final</version>
    <scope>provided</scope>
</dependency>

2. 定义子DTO

和手动映射的StateResponse、TownResponse一致,无需编写构造方法。

3. 定义MapStruct映射接口

@Mapper(componentModel = "spring")
public interface CountryMapper {
    CountryMapper INSTANCE = Mappers.getMapper(CountryMapper.class);

    // 映射Country到CountryResponse,自定义计算人口总和和提取城镇的逻辑
    @Mapping(target = "population", expression = "java(calculateTotalPopulation(country))")
    @Mapping(target = "towns", expression = "java(extractAllTowns(country))")
    CountryResponse countryToCountryResponse(Country country);

    // 自动映射State到StateResponse
    StateResponse stateToStateResponse(State state);

    // 自动映射Town到TownResponse
    TownResponse townToTownResponse(Town town);

    // 自定义方法:计算国家总人口
    default Long calculateTotalPopulation(Country country) {
        return country.getStates().stream()
                .map(State::getPopulation)
                .filter(Objects::nonNull)
                .reduce(0L, Long::sum);
    }

    // 自定义方法:提取国家下所有城镇
    default Set<TownResponse> extractAllTowns(Country country) {
        return country.getStates().stream()
                .flatMap(state -> state.getTowns().stream())
                .map(this::townToTownResponse)
                .collect(Collectors.toSet());
    }
}

4. 使用方式

在业务代码中直接调用映射方法:

List<Country> countries = countryRepository.findAll(); // 确保关联数据已加载(如通过fetch join或配置即时加载)
List<CountryResponse> responses = countries.stream()
        .map(CountryMapper.INSTANCE::countryToCountryResponse)
        .collect(Collectors.toList());

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.31 09:02:57