3

I want to make application similar to facebook when I have users and they have other users as friends. So I made a Entity User which has ManyToMany relation with itself, also they can invite each other to friends list. Unfortunately I'm getting this error when I want to get user which has invitations to friends:

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

My shortened User Entity class:

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

as you can see I tried to make it Lazy, also I tried @JsonIgnore annotations and nothing works. Any suggestions?

My method which returns UserDTO (mapping User to UserDTO)

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

UserDTO is mapped via 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;
}
Slava Rozhnev
  • 9,510
  • 6
  • 23
  • 39
gmexo
  • 1,892
  • 2
  • 14
  • 19
  • 1
    are you generating toString() method as sometimes the cyclic reference to the entities can cause this issue as well. – user06062019 Jan 24 '20 at 13:37
  • @user06062019 Annotation `@Data` from Lombok are doing this – gmexo Jan 24 '20 at 16:07
  • 2
    Read the error message. You're not serializing instances of User. You're serializing instances of UserDTO. The User class is completely irrelevant. – JB Nizet Jan 25 '20 at 11:09
  • @JBNizet yes because I'm mapping User to UserDTO after retrieving it from database. When I want to return User.class I'm getting this error: Infinite recursion (StackOverflowError); nested exception is JsonMappingException: Infinite recursion (StackOverflowError) (through reference chain: com.pk.thesis.devbook.models.entity.InvitationsToFriends["from"]->com.pk.thesis.devbook.models.entity.User["invitedFriends"]->org.hibernate.collection.internal.PersistentBag[0]>com.pk.thesis.devbook.models.entity.InvitationsToFriends["from"] with root cause – gmexo Jan 25 '20 at 11:17
  • I never said you should return User. I said that the code you posted is irrelevant to your question, since the problem is with UserDTO, and you posted User. Post the **relevant** code. But before that, read the error message. It explains what is wrong: you have an inifinitely recursive structure, and you're trying to serialize it to JSON. – JB Nizet Jan 25 '20 at 11:18
  • this is my method to return userDTO ` public UserDTO getUserDTO(String username) { return userRepository.findByUsername(username) .map(u -> modelMapper.map(u, UserDTO.class)) .orElseThrow(() -> new UsernameNotFoundException("User not found")); }` – gmexo Jan 25 '20 at 11:19
  • @JBNizet I added a method to the question. I know that I'm serializing infinitely recursive structure, I don't know why it is. I made it fetch type to LAZY, and still it fetching it all. – gmexo Jan 25 '20 at 11:26
  • @gmexo try by adding `@JsonIgnoreProperties(value = {"invitationsToFriends","invitedFriends"})` above invitedFriends field. – GnanaJeyam Jan 25 '20 at 11:29
  • 2
    Of course it fetches it. Lazy doesn't mean: this variable will always contain an empty list. What would be the point? Lazy means: it's only when you call a method on this list for the first time that I will execute the SQL query needed to populate that list, and populate it. Your object mapper, I guess (since you still haven't posted the relevant code) transforms the list of invited friends (Users) to a list of invited firend DTOs (UserDTOs). To do that, it iterates through the list. So it calls it's iterator() method. So the list is being loaded. – JB Nizet Jan 25 '20 at 11:30
  • If you don't want to serialize invted friends, don't have an invitedFriends list in your DTO. A DTO contains what you want to serialize. Why add something you don't want to serialize in it? – JB Nizet Jan 25 '20 at 11:31
  • @JBNizet but I want serialize invitedFriends, however I don't want to serialize nested invitedFriends from it. User has invitedFriends which I want to receive. InvitedFriends is User object which has another invitedUsers and I don't want them. – gmexo Jan 25 '20 at 12:30
  • 1
    Then use another DTO inside of the list of invited friends, which doesn't have a list of invited friends. Or make sure not to populate their list of invited friends. – JB Nizet Jan 25 '20 at 12:47
  • have you used springboot? – ValerioMC Jan 31 '20 at 15:21
  • @ValerioMC yes, I did – gmexo Feb 02 '20 at 09:11

2 Answers2

8

To avoid an infinite recursion you should just use @JsonIgnoreProperties annotation but with an array of all nested many-to-many fields, for example:

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

Then, to avoid the exception com.fasterxml.jackson.databind.JsonMappingException: failed to lazily initialize a collection of role... which will rise when you try to get Person data in your controller, you can use @EntityGraph (on the query method of your repository) with parameter attributePaths which is set to an array of those field names as well, to fill their values in one query:

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

In this case, all field values will be set, recursion will be avoided and you will be able to get a correct result in your controller:

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

Then you may want to get all the people. Considering that the data of a single person is quite big it's not correct to get all people with all associated friends in one list. It's better to get only basic fields of each Person. In this case, you can use a simple 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();
    }
}

and map Person to it:

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

Due to this mapping, you will avoid the exception com.fasterxml.jackson.databind.JsonMappingException as well.


Entity Person which is used in this answer:

@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 - you can run it in your IDE, connect to H2 database (using this approach) to see its data. If your IDE is IntelliJ IDEA, you can run demo requests right from file demo.http. And thanks to log4jdbc-spring-boot-starter you can see all SQL queries in the application log.

Cepr0
  • 28,144
  • 8
  • 75
  • 101
0

Thanks to the answers in comments section I found a way to do this. I made another entity InvitationsToFriends with additional date field and I connected it with my User entity by OneToMany relationship. Also I created ReducedUserDTO and ReducedInvitationsToFriendsDTO with the fields I needed (username, firstname, lastname).

My User class:

@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 and ReducedUserDTO:

@Data
public class ReducedInvitationsToFriendsDTO {

    private ReducedUserDTO from;
    private ReducedUserDTO to;
}

@Data
public class ReducedUserDTO {

    private String username;
    private String firstname;
    private String lastname;
}

Now the response json looks like this:

    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"}]
gmexo
  • 1,892
  • 2
  • 14
  • 19