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

使用MapStruct解决父DTO含自引用子对象的无限循环问题

问题背景

实体类定义如下:

@Entity
@Table(name = "flows")
public class Flow {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "id")
    private Long id;
    @Column(name = "flow_name")
    private String flowName;
    @OneToOne
    @JoinColumn(name = "folder_id", referencedColumnName = "id")
    private Folder folder;
}

@Entity
@Table(name = "folders")
public class Folder {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "id")
    private Long id;
    @Column(name = "internal_id", unique = true)
    private Long internalId;
    @Column(name = "parent_internal_id")
    private Long parentInternalId;
    @Column(name = "project_internal_id")
    private Long projectInternalId;
    @Column(name = "folder_name")
    private String folderName;

    @Column(name="root_flg")
    private Boolean rootFlg;

    @OneToMany(mappedBy = "parent")
    private List<Folder> children = new ArrayList<>();

    @ManyToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
    @JoinColumn(name = "parent_id", referencedColumnName = "id")
    private Folder parent;

    @ManyToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "project_id", referencedColumnName = "id")
    private Project project;

    @OneToOne(mappedBy = "folder")
    private Flow flow;
}

DTO类定义如下:

public class FlowDTO implements Serializable {
    private final Long id;
    private final String flowName;
    private final Long folderId;
    private final String folderName;
    private final Long parentId;
    private final String parentName;
    private final Long projectId;
    private final String projectName;
}

public class FolderDTO implements Serializable {
    private final Long id;
    private final Long internalId;
    private final Long parentInternalId;
    private final String folderName;
    private final Boolean rootFlg;
    private final Long parentId;
    private final String parentName;
    private final Long projectId;
    private final String projectName;
    private final List<FolderDTO> children;
}

FlowMapper接口定义如下:

@Mapper(componentModel = "spring")
public interface FlowMapper {
    @Mapping(target="folderId", source="folder.id")
    @Mapping(target="folderName", source="folder.folderName")
    @Mapping(target="parentId", source="folder.parent.id")
    @Mapping(target="parentName", source="folder.parent.folderName")
    @Mapping(target="projectId", source="folder.project.id")
    @Mapping(target="projectName", source = "folder.project.projectName")
    FlowDTO flowToFlowDTO(Flow flow);
    List<FlowDTO> flowsToFlowDTOs(List<Flow> flows);
    @InheritInverseConfiguration
    Flow flowDTOToFlow(FlowDTO flowDTO);
    List<Flow> flowDTOsToFlows(List<FlowDTO> flowDTOs);
}
问题描述

反向映射时MapStruct未正确将parentId映射到folder.parent.id,反而生成了无限循环的代码。生成的Mapper实现类中关键代码如下:

protected Folder flowDTOToFolder(FlowDTO flowDTO) {
    if ( flowDTO == null ) {
        return null;
    }

    Folder folder = new Folder();

    folder.setParent( flowDTOToFolder( flowDTO ) ); // 此处引发无限循环
    folder.setProject( flowDTOToProject( flowDTO ) );
    folder.setId( flowDTO.getFolderId() );
    folder.setFolderName( flowDTO.getFolderName() );

    return folder;
}

原本考虑用@BeforeMapping,想知道更简便的最佳实践方案。

补充尝试

曾尝试添加@Named方法,但未生效,报错信息:"The type of parameter "flowDTO" has no property named "parent". Please define the source property explicitly.",添加的代码如下:

@InheritInverseConfiguration
@Mapping(target="folder.parent", qualifiedByName="parentFolder")
Flow flowDTOToFlow(FlowDTO flowDTO);
List<Flow> flowDTOsToFlows(List<FlowDTO> flowDTOs);

@Named("parentFolder")
@Mapping(target = "id", source = "parentId")
@Mapping(target="folderName", source = "parentName")
Folder flowDTOToParentFolder(FlowDTO flowDTO);
解决方案

要解决这个问题,核心是明确告诉MapStruct反向映射时如何处理folder.parent,避免它自动调用递归的转换方法。以下是两种可靠的实现方式:

方式一:拆分转换逻辑,显式定义映射规则

修改flowDTOToFlow方法,不再完全依赖@InheritInverseConfiguration,而是拆分嵌套对象的转换逻辑,明确每个层级的映射规则:

@Mapper(componentModel = "spring")
public interface FlowMapper {
    // 正向映射保持不变
    @Mapping(target="folderId", source="folder.id")
    @Mapping(target="folderName", source="folder.folderName")
    @Mapping(target="parentId", source="folder.parent.id")
    @Mapping(target="parentName", source="folder.parent.folderName")
    @Mapping(target="projectId", source="folder.project.id")
    @Mapping(target="projectName", source = "folder.project.projectName")
    FlowDTO flowToFlowDTO(Flow flow);
    List<FlowDTO> flowsToFlowDTOs(List<Flow> flows);

    // 反向映射:显式定义整体规则
    @Mapping(target="id", source="id")
    @Mapping(target="flowName", source="flowName")
    @Mapping(target="folder", source="flowDTO")
    Flow flowDTOToFlow(FlowDTO flowDTO);
    List<Flow> flowDTOsToFlows(List<FlowDTO> flowDTOs);

    // 自定义Folder转换,指定parent的转换方法
    @Mapping(target="id", source="folderId")
    @Mapping(target="folderName", source="folderName")
    @Mapping(target="parent", source="flowDTO", qualifiedByName = "parentFolder")
    @Mapping(target="project", source="flowDTO", qualifiedByName = "project")
    Folder flowDTOToFolder(FlowDTO flowDTO);

    // 单独定义parent的转换,仅映射必要字段
    @Named("parentFolder")
    @Mapping(target="id", source="parentId")
    @Mapping(target="folderName", source="parentName")
    Folder flowDTOToFolderParent(FlowDTO flowDTO);

    // 单独定义project的转换
    @Named("project")
    @Mapping(target="id", source="projectId")
    @Mapping(target="projectName", source="projectName")
    Project flowDTOToProject(FlowDTO flowDTO);
}

这种方式通过拆分转换逻辑,让MapStruct按照指定的方法处理嵌套对象,不会自动生成递归调用,从根源避免无限循环。

方式二:忽略自动映射,手动设置parent

如果不想拆分过多方法,可以通过@Mapping(ignore=true)阻止MapStruct自动处理folder.parent,再用@BeforeMapping手动设置parent字段:

@Mapper(componentModel = "spring")
public interface FlowMapper {
    // 正向映射保持不变
    @Mapping(target="folderId", source="folder.id")
    @Mapping(target="folderName", source="folder.folderName")
    @Mapping(target="parentId", source="folder.parent.id")
    @Mapping(target="parentName", source="folder.parent.folderName")
    @Mapping(target="projectId", source="folder.project.id")
    @Mapping(target="projectName", source = "folder.project.projectName")
    FlowDTO flowToFlowDTO(Flow flow);
    List<FlowDTO> flowsToFlowDTOs(List<Flow> flows);

    // 反向映射:忽略parent的自动映射
    @InheritInverseConfiguration
    @Mapping(target="folder.parent", ignore = true)
    Flow flowDTOToFlow(FlowDTO flowDTO);
    List<Flow> flowDTOsToFlows(List<FlowDTO> flowDTOs);

    // 手动设置parent字段
    @BeforeMapping
    default void setFolderParent(FlowDTO source, @MappingTarget Flow target) {
        if (source.getParentId() != null) {
            Folder parent = new Folder();
            parent.setId(source.getParentId());
            parent.setFolderName(source.getParentName());
            target.getFolder().setParent(parent);
        }
    }

    // 自定义project转换
    @Mapping(target="id", source="projectId")
    @Mapping(target="projectName", source="projectName")
    Project flowDTOToProject(FlowDTO flowDTO);
}

这种方式通过手动干预嵌套对象的赋值,避免MapStruct自动生成递归逻辑,实现起来更简洁。

关键注意点

  • 对于存在自关联的实体(如Folder.parent),避免依赖MapStruct的自动推断转换逻辑,显式定义映射规则更可靠。
  • @InheritInverseConfiguration会反转正向的所有映射,但无法识别递归边界,因此需要手动排除或指定嵌套对象的转换方式。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.11 01:50:30