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

gme*_*exo 3 recursion spring hibernate jpa

当我有用户并且他们有其他用户作为朋友时,我想制作类似于 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
Run Code Online (Sandbox Code Playgroud)

我缩短的用户实体类:

    @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;
    }
Run Code Online (Sandbox Code Playgroud)

正如你所看到的,我试图让它变得懒惰,我也尝试了 @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"));
    }
Run Code Online (Sandbox Code Playgroud)

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;
}
Run Code Online (Sandbox Code Playgroud)

Cep*_*pr0 6

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

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

然后,为了避免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);
}
Run Code Online (Sandbox Code Playgroud)

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

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

那么你可能想要得到所有的人。考虑到单个人的数据相当大,将所有相关联的朋友的所有人都放在一个列表中是不正确的。最好只获取每个 Person 的基本字段。在这种情况下,您可以使用简单的 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();
    }
}
Run Code Online (Sandbox Code Playgroud)

并将 Person 映射到它:

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

由于这种映射,您也将避免异常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;
}
Run Code Online (Sandbox Code Playgroud)

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