使用@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的关联字段上标注映射规则,配合自定义转换器实现类型转换:
- 实现自定义转换器
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; } }
- 在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; // 其他字段... }
- 注册转换器到ModelMapper
modelMapper.addConverter(new UserToJwtUserInfoConverter());
方法3:禁用递归映射(不推荐)
通过关闭ModelMapper的深度复制功能避免循环引用,但可能影响其他嵌套关联字段的正常映射:
modelMapper.getConfiguration().setDeepCopyEnabled(false);
内容的提问来源于stack exchange,提问作者Tay Nguyen Duong Chanh
相关产品推荐
相关产品推荐

