Бесконечная рекурсия по отношению ко многим ко многим в одной и той же сущности - PullRequest
2 голосов
/ 23 января 2020

Я хочу сделать приложение похожим на Facebook, когда у меня есть пользователи, а у них есть другие пользователи в друзьях. Итак, я создал Entity 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

Мой сокращенный класс 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;
    }

, как вы видите, я пытался сделайте это Ленивым, также я попробовал аннотации @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 ]

4 голосов
/ 28 января 2020

Чтобы избежать бесконечной рекурсии, вы должны просто использовать аннотацию @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"));
}

Тогда вы можете собрать всех людей. Учитывая, что данные одного человека достаточно велики, неправильно объединять всех людей со всеми связанными друзьями в один список. Лучше получать только базовые c поля каждого человека. В этом случае вы можете использовать простой 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 .


Entity 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 . А благодаря log4jdb c -spring-boot-starter вы можете увидеть все SQL запросы в журнале приложений.

0 голосов
/ 25 января 2020

Благодаря ответам в разделе комментариев я нашел способ сделать это. Я создал еще одну сущность 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;

    }

InvitationsToFriends:

@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"}]
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...