同一实体中的多对多关系存在无限递归

3
我希望制作一个类似Facebook的应用程序,当有用户并且他们之间互为好友时。因此我创建了一个实体User,它与自身具有多对多的关系,同时他们可以将彼此邀请到好友列表中。不幸的是,当我要获取具有好友邀请的用户时,发生了以下错误:
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

我缩短了User Entity类:

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

如你所见,我试过使用Lazy加载方式,还尝试了 @JsonIgnore 注解,但都没有成功。有什么建议吗?

下面是我的方法,返回一个 UserDTO (将 User 映射为 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;
}

1
你是否在生成toString()方法时考虑到了实体之间的循环引用可能会导致这个问题呢? - user06062019
@Data 注解来自 Lombok,它可以实现这个功能。 - gmexo
2
阅读错误信息。您没有序列化User的实例,而是序列化UserDTO的实例。User类完全不相关。 - JB Nizet
2
当然会获取它。懒惰并不意味着:这个变量将始终包含一个空列表。那有什么意义呢?懒惰的意思是:只有在您第一次调用此列表上的方法时,我才会执行需要填充该列表的SQL查询,并填充它。我猜你的对象映射器(因为你还没有发布相关代码)将受邀好友(用户)的列表转换为受邀好友DTO(UserDTOs)的列表。为此,它遍历列表。所以它调用了它的iterator()方法。因此,列表正在加载。 - JB Nizet
1
然后在邀请朋友的列表中使用另一个没有邀请朋友列表的DTO。或者确保不填充他们的邀请朋友列表。 - JB Nizet
显示剩余9条评论
2个回答

8
为避免无限递归,您应该只使用 @JsonIgnoreProperties 注释,但是要使用所有嵌套的多对多字段的数组,例如:
@JsonIgnoreProperties({"friends", "friendsOf", "invitedFriends", "invitedFriendsOf"})
@ManyToMany
@JoinTable(...)
private Set<Person> friends;

那么,为了避免异常 com.fasterxml.jackson.databind.JsonMappingException: failed to lazily initialize a collection of role... 产生,当你尝试在控制器中获取Person数据时,可以使用@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();
    }
}

并将人员映射到它:
@GetMapping
public List<PersonDto> getAll() {
    return personRepo.findAll().stream().map(PersonDto::new).collect(Collectors.toList());
}

由于进行了这种映射,您将避免出现异常com.fasterxml.jackson.databind.JsonMappingException
此答案中使用的实体Person:
@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;
}

这是我的工作演示 - 您可以在IDE中运行它,连接到H2数据库(使用这种方法),以查看其数据。如果您的IDE是IntelliJ IDEA,则可以直接从文件demo.http 运行演示请求。并且由于log4jdbc-spring-boot-starter,您可以在应用程序日志中看到所有SQL查询语句。


0
感谢评论区的回答,我找到了一种方法来实现这个功能。我创建了另一个名为InvitationsToFriends的实体,并添加了一个日期字段,通过OneToMany关系将其与我的User实体连接起来。此外,我还创建了ReducedUserDTO和ReducedInvitationsToFriendsDTO,只包含我需要的字段(用户名、名字、姓氏)。
我的User类:
@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"}]

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接