【问题标题】:Infinite recursion on many to many relation in the same entity同一实体中多对多关系的无限递归
【发布时间】:2020-05-09 15:16:13
【问题描述】:

当我有用户并且他们有其他用户作为朋友时,我想制作类似于 facebook 的应用程序。所以我创建了一个实体User,它与自己有ManyToMany关系,他们也可以互相邀请到朋友列表中。不幸的是,当我想获取邀请朋友的用户时,我收到了这个错误:

Request processing failed; nested exception is org.springframework.http.converter.HttpMessageNotWritableException: Could not write JSON: Infinite recursion (StackOverflowError); nested exception is com.fasterxml.jackson.databind.JsonMappingException: Infinite recursion (StackOverflowError) (through reference chain: java.util.ArrayList[0]->com.pk.thesis.devbook.models.dto.UserDTO["invitedFriends"]->java.util.ArrayList[0]->com.pk.thesis.devbook.models.dto.UserDTO["invitedFriends"]->java.util.ArrayList[0]-
... (it goes forever)
>com.pk.thesis.devbook.models.dto.UserDTO["invitedFriends"]->java.util.ArrayList[0]with root cause

我缩短的用户实体类:

    @Data
    @Entity
    @Table( name = "users", 
            uniqueConstraints = { 
                @UniqueConstraint(columnNames = "username"),
                @UniqueConstraint(columnNames = "email") 
            })
    @JsonIdentityInfo(generator= ObjectIdGenerators.UUIDGenerator.class, property="@id")
    public class User {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;

        @NotBlank
        @Size(max = 40)
        private String username;

//other things...

        @ManyToMany(fetch = FetchType.LAZY)
        @JoinTable(name="tbl_friends",
                joinColumns=@JoinColumn(name="personId"),
                inverseJoinColumns=@JoinColumn(name="friendId")
        )
        private List<User> friends;

        @ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
        @JoinTable(name="tbl_friends",
                joinColumns=@JoinColumn(name="friendId"),
                inverseJoinColumns=@JoinColumn(name="personId")
        )
        private List<User> friendOf;

        @ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
        @JoinTable(name="tbl_invites_to_friends",
                joinColumns=@JoinColumn(name="personId"),
                inverseJoinColumns=@JoinColumn(name="invited_personId")
        )
        @JsonIgnoreProperties("invitationsToFriends")
        private List<User> invitedFriends;

        @JsonIgnore
        @ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
        @JoinTable(name="tbl_invites_to_friends",
                joinColumns=@JoinColumn(name="invited_personId"),
                inverseJoinColumns=@JoinColumn(name="personId")
        )
        @JsonIgnoreProperties("invitedFriends")
        private List<User> invitationsToFriends;
    }

如你所见,我试图让它变得懒惰,我也尝试了@JsonIgnore 注释,但没有任何效果。有什么建议吗?

我的返回 UserDTO 的方法(将用户映射到 UserDTO)

public UserDTO getUserDTO(String username) {
        return userRepository.findByUsername(username)
                .map(u -> modelMapper.map(u, UserDTO.class))
                .orElseThrow(() -> new UsernameNotFoundException("User not 
                                                                     found"));
    }

UserDTO 通过 org.modelmapper.ModelMapper 映射

public class UserDTO {

    private String username;
    private String firstname;
    private String lastname;
    private String email;
    private List<UserDTO> invitedFriends;
    private List<UserDTO> invitationsToFriends;
}

【问题讨论】:

  • 您是否正在生成 toString() 方法,因为有时对实体的循环引用也会导致此问题。
  • @user06062019 来自 Lombok 的注释 @Data 正在这样做
  • 阅读错误信息。您没有序列化 User 的实例。您正在序列化 UserDTO 的实例。 User 类是完全不相关的。
  • 当然它会获取它。懒惰并不意味着:此变量将始终包含一个空列表。重点是什么?懒惰的意思是:只有当您第一次调用此列表上的方法时,我才会执行填充该列表所需的 SQL 查询并填充它。我猜你的对象映射器(因为你还没有发布相关代码)将受邀朋友(用户)列表转换为受邀朋友 DTO(用户DTO)列表。为此,它遍历列表。所以它称之为 iterator() 方法。所以列表正在加载中。
  • 然后在受邀好友列表中使用另一个 DTO,它没有受邀好友列表。或者确保不要填写他们的邀请好友列表。

标签: spring hibernate jpa recursion


【解决方案1】:

为避免无限递归,您应该只使用 @JsonIgnoreProperties 注释,但使用所有嵌套的多对多字段的数组,例如:

@JsonIgnoreProperties({"friends", "friendsOf", "invitedFriends", "invitedFriendsOf"})
@ManyToMany
@JoinTable(...)
private Set<Person> friends;

然后,为了避免当您尝试在控制器中获取 Person 数据时出现的异常 com.fasterxml.jackson.databind.JsonMappingException: failed to lazily initialize a collection of role...,您可以使用 @EntityGraph(在您的存储库的查询方法上),参数 attributePaths 设置为这些字段名称的数组,用于在一个查询中填充它们的值:

@Transactional(readOnly = true)
public interface PersonRepo extends JpaRepository<Person, Long> {
    @EntityGraph(attributePaths = {"friends", "friendsOf", "invitedFriends", "invitedFriendsOf"})
    Optional<Person> getById(Long aLong);
}

在这种情况下,将设置所有字段值,避免递归,您将能够在控制器中获得正确的结果:

@GetMapping("/{id}")
public Person get(@PathVariable Long id) {
    return personRepo.getById(id)
           .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Person not found"));
}

那么你可能想要得到所有的人。考虑到一个人的数据非常大,将所有相关联的朋友都放在一个列表中是不正确的。最好只获取每个人的基本字段。在这种情况下,您可以使用简单的 DTO:

@Value
public class PersonDto {
    private long id;
    private String name;
    private String email;

    public PersonDto(Person person) {
        this.id = person.getId();
        this.name = person.getName();
        this.email = person.getEmail();
    }
}

并将 Person 映射到它:

@GetMapping
public List<PersonDto> getAll() {
    return personRepo.findAll().stream().map(PersonDto::new).collect(Collectors.toList());
}

由于这种映射,您也将避免com.fasterxml.jackson.databind.JsonMappingException 异常。


此答案中使用的实体人:

@Data
@EqualsAndHashCode(of = "email")
@ToString(of = {"id", "name", "email"})
@Entity
@Table(name = "people")
public class Person {
    @Id
    @GeneratedValue
    private Long id;

    @Column(nullable = false, length = 32)
    private String name;

    @NaturalId
    @Column(nullable = false, length = 32)
    private String email;

    @JsonIgnoreProperties({"friends", "friendsOf", "invitedFriends", "invitedFriendsOf"})
    @ManyToMany
    @JoinTable(name = "friends", joinColumns = @JoinColumn(name = "person_id"), inverseJoinColumns = @JoinColumn(name = "friend_id"))
    private Set<Person> friends;

    @JsonIgnoreProperties({"friends", "friendsOf", "invitedFriends", "invitedFriendsOf"})
    @ManyToMany
    @JoinTable(name = "friends", joinColumns = @JoinColumn(name = "friend_id"), inverseJoinColumns = @JoinColumn(name = "person_id"))
    private Set<Person> friendsOf;

    @JsonIgnoreProperties({"friends", "friendsOf", "invitedFriends", "invitedFriendsOf"})
    @ManyToMany
    @JoinTable(name = "invited_friends", joinColumns = @JoinColumn(name = "person_id"), inverseJoinColumns = @JoinColumn(name = "friend_id"))
    private Set<Person> invitedFriends;

    @JsonIgnoreProperties({"friends", "friendsOf", "invitedFriends", "invitedFriendsOf"})
    @ManyToMany
    @JoinTable(name = "invited_friends", joinColumns = @JoinColumn(name = "friend_id"), inverseJoinColumns = @JoinColumn(name = "person_id"))
    private Set<Person> invitedFriendsOf;
}

My working demo - 您可以在 IDE 中运行它,连接到 H2 数据库(使用 this approach)以查看其数据。如果您的 IDE 是 IntelliJ IDEA,您可以直接从文件 demo.http 运行演示请求。感谢log4jdbc-spring-boot-starter,您可以在应用程序日志中看到所有 SQL 查询。

【讨论】:

    【解决方案2】:

    感谢 cmets 部分的答案,我找到了一种方法来做到这一点。我制作了另一个带有附加日期字段的实体 InvitationsToFriends,并通过 OneToMany 关系将它与我的用户实体连接起来。我还使用我需要的字段(用户名、名字、姓氏)创建了 ReducedUserDTO 和 ReducedInvitationsToFriendsDTO。

    我的用户类:

    @Entity
          public class User implements Serializable {
                @Id
                @GeneratedValue(strategy = GenerationType.IDENTITY)
                private Long id;
    
                @Size(max = 40)
                private String username;
    
                @Size(max = 120)
                private String password;
    
                @Column
                private String firstname;
    
                @Column
                private String lastname;
    
                @OneToMany(mappedBy="to")
                private List<InvitationsToFriends> invitationsToFriends;
    
                @OneToMany(mappedBy="from")
                private List<InvitationsToFriends> invitedFriends;
    
        }
    

    邀请朋友:

    @Entity
        public class InvitationsToFriends implements Serializable{
    
            @Id
            @GeneratedValue(strategy= GenerationType.AUTO)
            private Long id;
    
            @ManyToOne( fetch = FetchType.LAZY)
            @JoinColumn(name="from_user_fk")
            private User from;
    
            @ManyToOne( fetch = FetchType.LAZY)
            @JoinColumn(name="to_user_fk")
            private User to;
    
            @Column(name = "invitation_date")
            private Date invitationDate;
        }
    

    用户DTO:

    @Data
    public class UserDTO {
    
        private String username;
        private String firstname;
        private String lastname;
        private List<ReducedInvitationsToFriendsDTO> invitedFriends;
        private List<ReducedInvitationsToFriendsDTO> invitationsToFriends;
    }
    

    ReducedInvitationsToFriendsDTO 和 ReducedUserDTO:

    @Data
    public class ReducedInvitationsToFriendsDTO {
    
        private ReducedUserDTO from;
        private ReducedUserDTO to;
    }
    
    @Data
    public class ReducedUserDTO {
    
        private String username;
        private String firstname;
        private String lastname;
    }
    

    现在响应 json 看起来像这样:

        username: "username"
        firstname: "firstname"
        lastname: "lastname"
        email: "email@email.com"
        invitedFriends: [
    from: {username: "username", firstname: "firstname", lastname: "lastname"}
    to: {username: "invitedUsername", firstname: "invitedFirstname", lastname: "invitedLastName"}]
    

    【讨论】:

      猜你喜欢
      • 2020-11-01
      • 2020-07-25
      • 2019-09-12
      • 2010-12-11
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-07-21
      相关资源
      最近更新 更多