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

时间:2020-01-23 11:01:43

标签: spring hibernate jpa recursion

当我有用户并且他们有其他用户作为朋友时,我想使应用程序类似于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;
}

2 个答案:

答案 0 :(得分:5)

为避免无限递归,您应该只使用@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(在存储库的查询方法上)与参数{{1 }}也设置为这些字段名称的数组,以在一个查询中填充其值:

attributePaths

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

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

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

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

然后将Person映射到它:

@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

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

答案 1 :(得分:1)

感谢评论部分的答案,我找到了一种方法。我用另一个日期字段创建了另一个实体InvitationsToFriends,并通过OneToMany关系将其与我的User实体联系起来。我还使用所需的字段(用户名,名字,姓氏)创建了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;
    }

UserDTO:

@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"}]