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

使用@ManyToOne自引用时ModelMapper映射异常求助

问题描述

Spring项目中,User实体通过@ManyToOne实现createdBy、updatedBy等自引用关联,用于记录操作人。使用ModelMapper将User实体映射到UserInfoDto时,大部分用户ID的getUserById请求正常,但请求特定管理员ID(c65df504-5394-4a10-b2c1-9efecf4b7fbb)时出现映射错误,日志显示尝试将UserInfoDto实例设置到updatedBy字段(该字段类型应为JwtUserInfo),引发参数类型不匹配异常。

相关代码

User实体类

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "users")
public class User {
    @Id
    private String id;

    @Column(unique = true)
    private String email;
    private String password;
    private int age;
    private String fullName;
    private String address;
    private String phoneNumber;

    @CreationTimestamp
    private LocalDateTime createdAt;

    @UpdateTimestamp
    private LocalDateTime updatedAt;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "created_by_id")
    private User createdBy;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "updated_by_id")
    private User updatedBy;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "deleted_by_id")
    private User deletedBy;

    @Column(name = "is_deleted")
    private boolean isDeleted = false;

    private LocalDateTime deletedAt;

    @ManyToOne
    @JoinColumn(name = "company_id")
    private Company company;

    @ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
    @JoinTable(
            name = "users_roles",
            joinColumns = @JoinColumn(name = "users_id", referencedColumnName = "id"),
            inverseJoinColumns = @JoinColumn(name = "roles_id", referencedColumnName = "id")
    )
    private List<Role> roles = new ArrayList<>();

    @PrePersist
    private void setRandomId() {
        this.id = generateId();
    }

    private String generateId() {
        return UUID.randomUUID().toString();
    }
}

JwtUserInfo类

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class JwtUserInfo {
    private String id;
    private String fullName;
    private String email;
}

UserInfoDto类

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class UserInfoDto {
    private String id;

    @Email(message = "Email must be valid")
    @NotEmpty(message = "Email must not be empty")
    private String email;
    private String fullName;
    private String address;
    private int age;

    @NotEmpty(message = "Phone number must not be empty")
    private String phoneNumber;

    @NotEmpty(message = "Company must not be empty")
    private CompanyInfoDto company;
    private JwtUserInfo createdBy;
    private LocalDateTime createdAt;
    private JwtUserInfo updatedBy;
    private LocalDateTime updatedAt;
    private JwtUserInfo deletedBy;
    private LocalDateTime deletedAt;
    private boolean isDeleted;
}

查询逻辑代码

@Override
public UserInfoDto getUserById(String id) {
    try {
        User user = userRepository.findById(id).orElseThrow(
                () -> new ResourceNotFoundException("id", "User", id)
        );
        return modelMapper.map(user, UserInfoDto.class);
    } catch (Exception e) {
        e.printStackTrace();
        throw e;
    }
}

错误信息

错误响应

{
    "timestamp": "2023-12-05T00:45:46.731086",
    "message": "ModelMapper mapping errors:\n\n1) Error mapping com.frankie.workdev.entity.User to com.frankie.workdev.dto.user.UserInfoDto\n\n1 error",
    "details": "uri=/api/v1/users/c65df504-5394-4a10-b2c1-9efecf4b7fbb",
    "errorCode": "BAD_REQUEST"
}

核心错误日志

Caused by: org.modelmapper.MappingException: ModelMapper mapping errors:

1) Failed to set value 'com.frankie.workdev.dto.user.UserInfoDto@1dfba458' on com.frankie.workdev.dto.user.UserInfoDto.setUpdatedBy()

1 error
    at org.modelmapper.internal.Errors.toMappingException(Errors.java:258)
    ...
Caused by: java.lang.IllegalArgumentException: argument type mismatch
排查分析
  • 问题根源:ModelMapper默认映射逻辑遇到循环自引用时出错。该管理员用户的updatedBy字段指向自己,映射时ModelMapper递归处理时误将正在构建的UserInfoDto实例赋值给updatedBy字段,而非预期转换为JwtUserInfo。
  • 普通用户正常的原因:普通用户的updatedBy指向其他用户,递归到第二层时,ModelMapper能匹配JwtUserInfo的字段(id、fullName、email)自动完成类型转换,不会触发循环错误。
解决方法

方法1:全局配置ModelMapper的类型映射规则

创建ModelMapper配置类,明确指定User到JwtUserInfo的转换逻辑,强制关联字段使用正确的类型:

@Configuration
public class ModelMapperConfig {
    @Bean
    public ModelMapper modelMapper() {
        ModelMapper modelMapper = new ModelMapper();
        
        // 定义User到JwtUserInfo的映射规则
        modelMapper.createTypeMap(User.class, JwtUserInfo.class)
                .addMapping(User::getId, JwtUserInfo::setId)
                .addMapping(User::getFullName, JwtUserInfo::setFullName)
                .addMapping(User::getEmail, JwtUserInfo::setEmail);
        
        // 定义User到UserInfoDto的映射,指定关联字段的转换
        modelMapper.createTypeMap(User.class, UserInfoDto.class)
                .addMapping(User::getCreatedBy, UserInfoDto::setCreatedBy)
                .addMapping(User::getUpdatedBy, UserInfoDto::setUpdatedBy)
                .addMapping(User::getDeletedBy, UserInfoDto::setDeletedBy);
        
        return modelMapper;
    }
}

方法2:使用自定义转换器+DTO注解

在UserInfoDto的关联字段上标注映射规则,配合自定义转换器实现类型转换:

  1. 实现自定义转换器
public class UserToJwtUserInfoConverter implements Converter<User, JwtUserInfo> {
    @Override
    public JwtUserInfo convert(MappingContext<User, JwtUserInfo> context) {
        User source = context.getSource();
        if (source == null) return null;
        
        JwtUserInfo target = new JwtUserInfo();
        target.setId(source.getId());
        target.setFullName(source.getFullName());
        target.setEmail(source.getEmail());
        return target;
    }
}
  1. 在UserInfoDto中指定转换器
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class UserInfoDto {
    // 其他字段...
    
    @Mapping(source = "createdBy", target = "createdBy", converter = UserToJwtUserInfoConverter.class)
    private JwtUserInfo createdBy;
    
    @Mapping(source = "updatedBy", target = "updatedBy", converter = UserToJwtUserInfoConverter.class)
    private JwtUserInfo updatedBy;
    
    @Mapping(source = "deletedBy", target = "deletedBy", converter = UserToJwtUserInfoConverter.class)
    private JwtUserInfo deletedBy;
    
    // 其他字段...
}
  1. 注册转换器到ModelMapper
modelMapper.addConverter(new UserToJwtUserInfoConverter());

方法3:禁用递归映射(不推荐)

通过关闭ModelMapper的深度复制功能避免循环引用,但可能影响其他嵌套关联字段的正常映射:

modelMapper.getConfiguration().setDeepCopyEnabled(false);

内容的提问来源于stack exchange,提问作者Tay Nguyen Duong Chanh

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.04 22:50:40