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

Mapper类循环依赖求助:Project与Volunteer双向映射困境

问题描述

我有两个实体类,存在@ManyToMany和@OneToMany两种关联关系:Project与项目成员是多对多关联,Project与项目所有者是一对多关联(所有者和成员都属于Volunteer类型)。之前用@JsonIdentityInfo解决了控制器的无限递归问题,但为了避免暴露用户敏感数据(比如密码、用户名),我创建了DTO对象并手动实现实体到DTO的映射逻辑,现在遇到了Mapper类循环依赖的问题:ProjectMapper映射时需要调用VolunteerMapper,VolunteerMapper映射时又要调用ProjectMapper,形成循环调用导致栈溢出。


Mapper类代码

ProjectMapper.java

public class ProjectMapper {

    public static ProjectDTO mapProjectToDTO(Project projectToMap) {

        ProjectDTO newDTO = new ProjectDTO();

        List<VolunteerDTO> listOfDTOs = projectToMap.getProjectVolunteers()
                        .stream()
                                .map(VolunteerMapper::mapVolunteerToDTO)
                                        .collect(Collectors.toList());

        newDTO.setProjectName(projectToMap.getProjectName());
        newDTO.setProjectDescription(projectToMap.getProjectDesription());
        newDTO.setProjectLocation(projectToMap.getProjectLocation());
        newDTO.setProjectStatus(projectToMap.isProjectStatus());
        newDTO.setProjectDate(projectToMap.getProjectDate());
        newDTO.setProjectOpinions(projectToMap.getProjectOpinions());
        newDTO.setProjectOwner(VolunteerMapper.mapVolunteerToDTO(projectToMap.getOwnerVolunteer()));
        newDTO.setProjectVolunteers(listOfDTOs);
        newDTO.setTasks(projectToMap.getTasks());
        newDTO.setRequiredSkills(projectToMap.getRequiredSkills());

        return newDTO;
    }

    public static void mapPropertiesToProject(Project sourceProject, Project targetProject) {

        targetProject.setProjectName(sourceProject.getProjectName());
        targetProject.setProjectDate(sourceProject.getProjectDate());
        targetProject.setProjectVolunteers(sourceProject.getProjectVolunteers());
        targetProject.setProjectDesription(sourceProject.getProjectDesription());
        targetProject.setProjectLocation(sourceProject.getProjectLocation());
        targetProject.setProjectOpinions(sourceProject.getProjectOpinions());
        targetProject.setProjectStatus(sourceProject.isProjectStatus());

    }
}

VolunteerMapper.java

public class VolunteerMapper {

    public static VolunteerDTO mapVolunteerToDTO(Volunteer volunteerToMap) {

        VolunteerDTO newDTO = new VolunteerDTO();

        List<ProjectDTO> listOfDTOs = volunteerToMap.getParticipatingProjects()
                .stream()
                .map(ProjectMapper::mapProjectToDTO)
                .collect(Collectors.toList());

        newDTO.setName(volunteerToMap.getName());
        newDTO.setSurname(volunteerToMap.getSurname());
        newDTO.setDateOfBirth(volunteerToMap.getDateOfBirth());
        newDTO.setContact(volunteerToMap.getContact());
        newDTO.setSkills(volunteerToMap.getSkills());
        newDTO.setProjects(listOfDTOs);
        newDTO.setReputation(volunteerToMap.getReputation());
        newDTO.setInterests(volunteerToMap.getInterests());

        return newDTO;
    }

    public static void mapPropertiesToVolunteer(Volunteer sourceVolunteer, Volunteer targetVolunteer) {

        targetVolunteer.setName(sourceVolunteer.getName());
        targetVolunteer.setSurname(sourceVolunteer.getSurname());
        targetVolunteer.setDateOfBirth(sourceVolunteer.getDateOfBirth());
        targetVolunteer.setContact(sourceVolunteer.getContact());
        targetVolunteer.setSkills(sourceVolunteer.getSkills());
        targetVolunteer.setParticipatingProjects(sourceVolunteer.getParticipatingProjects());
        targetVolunteer.setReputation(sourceVolunteer.getReputation());
        targetVolunteer.setInterests(sourceVolunteer.getInterests());

    }
}

DTO类代码

ProjectDTO.java

@Data
public class ProjectDTO {

    private String projectName;
    private String projectDescription;

    @JsonFormat(pattern = "yyyy-MM-dd")
    private LocalDate projectDate;

    private List<VolunteerDTO> projectVolunteers;
    private VolunteerDTO projectOwner;
    private String projectLocation;
    private List<String> requiredSkills;
    private boolean projectStatus;
    private List<Opinion> projectOpinions;
    private List<String> tasks;
}

VolunteerDTO.java

@Data
public class VolunteerDTO {

    private String name;
    private String surname;
    private LocalDate dateOfBirth;
    private String contact;
    private List<String> skills;
    private List<ProjectDTO> projects;
    private Integer reputation;
    private List<String> interests;

}

解决方案

方法1:截断循环引用,只保留单向关联

根据业务场景决定哪一端不需要包含完整的关联对象,比如:

  • 查询Project时,返回的VolunteerDTO不需要包含关联的Project列表(或者只保留项目ID)
  • 查询Volunteer时,返回的ProjectDTO不需要包含关联的Volunteer列表

示例修改:给VolunteerMapper新增重载方法,映射不含项目的VolunteerDTO

// VolunteerMapper中新增方法
public static VolunteerDTO mapVolunteerToDTOWithoutProjects(Volunteer volunteerToMap) {
    VolunteerDTO newDTO = new VolunteerDTO();
    newDTO.setName(volunteerToMap.getName());
    newDTO.setSurname(volunteerToMap.getSurname());
    newDTO.setDateOfBirth(volunteerToMap.getDateOfBirth());
    newDTO.setContact(volunteerToMap.getContact());
    newDTO.setSkills(volunteerToMap.getSkills());
    newDTO.setReputation(volunteerToMap.getReputation());
    newDTO.setInterests(volunteerToMap.getInterests());
    // 不设置projects字段,切断循环
    return newDTO;
}

然后修改ProjectMapper中的调用:

// ProjectMapper中替换原有的VolunteerMapper调用
List<VolunteerDTO> listOfDTOs = projectToMap.getProjectVolunteers()
        .stream()
        .map(VolunteerMapper::mapVolunteerToDTOWithoutProjects)
        .collect(Collectors.toList());

newDTO.setProjectOwner(VolunteerMapper.mapVolunteerToDTOWithoutProjects(projectToMap.getOwnerVolunteer()));

方法2:使用映射框架自动处理循环引用

像MapStruct这类成熟的映射框架支持自动处理循环引用,不需要手动写繁琐的映射逻辑。通过@Named和@Mapping注解指定关联对象的映射规则,切断循环:

示例配置:

// ProjectMapper接口
@Mapper
public interface ProjectMapper {
    ProjectMapper INSTANCE = Mappers.getMapper(ProjectMapper.class);
    
    @Mapping(target = "projectVolunteers", qualifiedByName = "volunteerWithoutProjects")
    @Mapping(target = "projectOwner", qualifiedByName = "volunteerWithoutProjects")
    ProjectDTO toDTO(Project project);
    
    @Named("volunteerWithoutProjects")
    @Mapping(target = "projects", ignore = true)
    VolunteerDTO volunteerToDTOWithoutProjects(Volunteer volunteer);
}

// VolunteerMapper接口
@Mapper
public interface VolunteerMapper {
    VolunteerMapper INSTANCE = Mappers.getMapper(VolunteerMapper.class);
    
    @Mapping(target = "projects", qualifiedByName = "projectWithoutVolunteers")
    VolunteerDTO toDTO(Volunteer volunteer);
    
    @Named("projectWithoutVolunteers")
    @Mapping(target = "projectVolunteers", ignore = true)
    @Mapping(target = "projectOwner", ignore = true)
    ProjectDTO projectToDTOWithoutVolunteers(Project project);
}

方法3:手动控制映射深度

在映射方法中添加深度参数,当超过指定深度时停止映射关联对象,避免无限循环:

示例修改:

// ProjectMapper修改后的映射方法
public static ProjectDTO mapProjectToDTO(Project projectToMap, int depth) {
    if (depth <= 0) {
        // 深度为0时只返回基本字段,不映射关联对象
        ProjectDTO newDTO = new ProjectDTO();
        newDTO.setProjectName(projectToMap.getProjectName());
        newDTO.setProjectDescription(projectToMap.getProjectDesription());
        newDTO.setProjectLocation(projectToMap.getProjectLocation());
        newDTO.setProjectStatus(projectToMap.isProjectStatus());
        newDTO.setProjectDate(projectToMap.getProjectDate());
        newDTO.setProjectOpinions(projectToMap.getProjectOpinions());
        newDTO.setTasks(projectToMap.getTasks());
        newDTO.setRequiredSkills(projectToMap.getRequiredSkills());
        return newDTO;
    }

    ProjectDTO newDTO = new ProjectDTO();
    List<VolunteerDTO> listOfDTOs = projectToMap.getProjectVolunteers()
            .stream()
            .map(v -> VolunteerMapper.mapVolunteerToDTO(v, depth - 1))
            .collect(Collectors.toList());

    newDTO.setProjectName(projectToMap.getProjectName());
    newDTO.setProjectDescription(projectToMap.getProjectDesription());
    newDTO.setProjectLocation(projectToMap.getProjectLocation());
    newDTO.setProjectStatus(projectToMap.isProjectStatus());
    newDTO.setProjectDate(projectToMap.getProjectDate());
    newDTO.setProjectOpinions(projectToMap.getProjectOpinions());
    newDTO.setProjectOwner(VolunteerMapper.mapVolunteerToDTO(projectToMap.getOwnerVolunteer(), depth - 1));
    newDTO.setProjectVolunteers(listOfDTOs);
    newDTO.setTasks(projectToMap.getTasks());
    newDTO.setRequiredSkills(projectToMap.getRequiredSkills());

    return newDTO;
}

// VolunteerMapper修改后的映射方法
public static VolunteerDTO mapVolunteerToDTO(Volunteer volunteerToMap, int depth) {
    if (depth <= 0) {
        // 深度为0时只返回基本字段
        VolunteerDTO newDTO = new VolunteerDTO();
        newDTO.setName(volunteerToMap.getName());
        newDTO.setSurname(volunteerToMap.getSurname());
        newDTO.setDateOfBirth(volunteerToMap.getDateOfBirth());
        newDTO.setContact(volunteerToMap.getContact());
        newDTO.setSkills(volunteerToMap.getSkills());
        newDTO.setReputation(volunteerToMap.getReputation());
        newDTO.setInterests(volunteerToMap.getInterests());
        return newDTO;
    }

    VolunteerDTO newDTO = new VolunteerDTO();
    List<ProjectDTO> listOfDTOs = volunteerToMap.getParticipatingProjects()
            .stream()
            .map(p -> ProjectMapper.mapProjectToDTO(p, depth - 1))
            .collect(Collectors.toList());

    newDTO.setName(volunteerToMap.getName());
    newDTO.setSurname(volunteerToMap.getSurname());
    newDTO.setDateOfBirth(volunteerToMap.getDateOfBirth());
    newDTO.setContact(volunteerToMap.getContact());
    newDTO.setSkills(volunteerToMap.getSkills());
    newDTO.setProjects(listOfDTOs);
    newDTO.setReputation(volunteerToMap.getReputation());
    newDTO.setInterests(volunteerToMap.getInterests());

    return newDTO;
}

调用时指定深度(比如ProjectMapper.mapProjectToDTO(project, 1)),限制关联对象的映射层数,避免无限循环。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.16 00:29:50