javaspringspring-bootcircular-dependency

Mappers' circular dependency


I have two classes with two types of relations - one is @ManyToMany and the other one is @OneToMany (one relates to Project - Members of project relationship and the other is related with Project - Owner of project where owner is the same type as members). I had problem with infinite recursion in controller but soon, I managed to resolve this issue with usage of @JsonIdentityInfo. But then I realized that one of classes shows too much information about client's data - passwords, usernames, etc., so i created DTO objects and implemented logic for mapping original objects to DTOs. Logic was simple - return DTOs so I control exposed data.

But there is a problem - if I create mapper for first class to map its' objects into DTOs, then second class must be mapped to DTOs too. But - regarding to provided example - second class must also expose information about its' owner, so we need to simultaneously map first class and second. It creates circular dependency problem.

Mapper classes:

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());

    }
}
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());

    }
}

Model classes:

@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;
}
@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;

}

Solution

  • Problem

    public static ProjectDTO mapProjectToDTO(Project projectToMap) {
        List<VolunteerDTO> listOfDTOs = projectToMap.getProjectVolunteers()
                        .stream()
                        ====>> .map(VolunteerMapper::mapVolunteerToDTO) <<===
                        .collect(Collectors.toList());
    }
    
     public static VolunteerDTO mapVolunteerToDTO(Volunteer volunteerToMap) {
         List<ProjectDTO> listOfDTOs = volunteerToMap.getParticipatingProjects()
                .stream()
               ====>> .map(ProjectMapper::mapProjectToDTO) <<====
                .collect(Collectors.toList());
    }
    

    mapProjectToDto calls mapVolunteerToDTO which calls mapProjectToDto again.

    you can break that circle in different ways depending on your business case

    1- make VolunteerDto without a list of ProjectDto and whenever you need such list you can fetch it using VolunteerDto Id

    2- make ProjectDto without list of VolunteerDto (same like 1)

    3- make VolunteerDto contains list of Project Ids intead of list of ProjectDTOs

    4- make ProjectDTO contain list of VolunteerIds instead of VolunteerDtos

    I would prefer 3rd, but it's all depending on your business case