0

Note: Please don't mark it as duplicate before reading completely

Case : I have three classes named User, Post and Tag

User <-> Post (OneToMany Bi-directional)

Post <-> Tag (ManyToMany)

Solution I want :

  • Mapping should work like If i call getUserById, I should get posts related to the user and tags related to the posts.

  • Same with Posts and Tags, If I call getPostById I should get the user and tags and if I call getTagByName I should get all posts related to tags

Solutions I have tried :

  • @JsonMappedReference, @JsonBackReference - Worked for read operations but failed for creating/writing

  • @JsonIdentityInfo - Did not worked

  • @JsonIgnore - Worked but I don't want to ignore as am not getting desired solution mentioned above

  • @ToString.Exclude, @EqualsAndHashCode.Exclude - Did not worked

  • Also tried with my own getters and setters and @ToString methods - Did not worked either

This is a springboot project

Here are my classes

User.java


@Entity
@Table(name = "user")
@Data
public class User {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = "id")
  private int id;

  @Column(name = "username")
  private String userName;

  @Column(name = "password")
  private String password;

  @Column(name = "first_name")
  private String firstName;

  @Column(name = "last_name")
  private String lastName;

  @Column(name = "created_at")
  @CreationTimestamp
  private Timestamp createdAt;

  @Column(name = "updated_at")
  @UpdateTimestamp
  private Timestamp updatedAt;

  @OneToMany(
      mappedBy = "user",
      cascade = {CascadeType.MERGE, CascadeType.DETACH, CascadeType.REFRESH})
  private List<Post> posts;

Post.java


@Entity
@Table(name = "post")
@Data
public class Post {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = "id")
  private int id;

  @ManyToOne(
      cascade = {CascadeType.MERGE, CascadeType.DETACH, CascadeType.REFRESH})
  @JoinColumn(name = "user_id")
  private User user;

  @Column(name = "title")
  private String postTitle;

  @Column(name = "content")
  private String postContent;

  @Column(name = "status")
  private String postStatus;

  @Column(name = "created_at")
  @CreationTimestamp
  private Timestamp createdAt;

  @Column(name = "updated_at")
  @UpdateTimestamp
  private Timestamp updatedAt;

  
  @ManyToMany(
      cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.DETACH, CascadeType.REFRESH})
  @JoinTable(
      name = "post_tag",
      joinColumns = @JoinColumn(name = "post_id"),
      inverseJoinColumns = @JoinColumn(name = "tag_id"))
  private List<Tag> tags;

Tag.java


@Entity
@Table(name = "tag")
@Data
public class Tag {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = "id")
  private int id;

  @Column(name = "name")
  private String tagName;

  @Column(name = "created_at")
  @CreationTimestamp
  private Timestamp createdAt;

  @Column(name = "updated_at")
  @UpdateTimestamp
  private Timestamp updatedAt;

  @ManyToMany(
      cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.DETACH, CascadeType.REFRESH})
  @JoinTable(
      name = "post_tag",
      joinColumns = @JoinColumn(name = "tag_id"),
      inverseJoinColumns = @JoinColumn(name = "post_id"))
  private List<Post> posts;

So with above classes I ran into infinite loop problem, If I use getUserById post object is user object is showing Unable to evaluate the expression Method threw 'org.hibernate.LazyInitializationException' exception. If call getAllPosts OR getAllTags tags object in post object is showing the same error or vice versa

LAS
  • 113
  • 1
  • 2
  • 14
  • I think there is no bidirectional mapping in user and post and post and tag. – vivekdubey Aug 31 '20 at 05:04
  • Good job in presentation of question. – Adarsh Aug 31 '20 at 05:07
  • @vivekdubey there's bi-directional mapping – LAS Aug 31 '20 at 05:23
  • I did not see @MappedBy so I have doubt about bidirectional mapping – vivekdubey Aug 31 '20 at 09:21
  • Sorry @vivekdubey I forgot to add in the question, user class is updated now – LAS Aug 31 '20 at 09:37
  • 1
    I'm sorry but the question is pretty chaotic. The title reads 'infinite loop', the content asks about `LazyInitializationException`, you then mention you 'ran into infinite loop problem' - ran into it *where exactly*? You also said you tried `@JsonIgnore`, `@ToString.Exclude`, `@EqualsAndHashCode.Exclude` - *how exactly* are those related to the problem? They do very different things, so I have to ask: *what* problem were you trying to solve? – crizzis Sep 02 '20 at 18:14
  • @crizzis When getting User it has Post object as well which intern has User, So there is infinite loop and also `LazyInitializationException` showing on Post object in User Object – LAS Sep 04 '20 at 14:06

6 Answers6

0

I had a similar problem with @OneToMany and @ManyToOne relations. I'm simply going to explain the route I took to fix my code. Hopefully, it will make a difference.

Add @JsonBackReference to your user class, this should resolve your loop issue. Also remove cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.DETACH, CascadeType.REFRESH} line from all of your classes. Cascades were the reason I was unable to perform update method.

Please try the codes I provided below. You should be able to see Users as a part of the Post output stream when you test it. Also, you should be able to list users without encountering the loop problem. Sadly, I'm not so sure about the many-to-many relation since I have no experince on it.

User.java

@Entity
@Table(name = "user")
@Data
public class User {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = "id")
  private int id;

  @Column(name = "username")
  private String userName;

  @Column(name = "password")
  private String password;

  @Column(name = "first_name")
  private String firstName;

  @Column(name = "last_name")
  private String lastName;

  @Column(name = "created_at")
  @CreationTimestamp
  private Timestamp createdAt;

  @Column(name = "updated_at")
  @UpdateTimestamp
  private Timestamp updatedAt;

  @JsonBackReference
  @OneToMany(mappedBy = "user")
  private List<Post> posts;

Post.java

@Entity
@Table(name = "post")
@Data
public class Post {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = "id")
  private int id;

  @ManyToOne
  @JoinColumn(name = "user_id")
  private User user;

  @Column(name = "title")
  private String postTitle;

  @Column(name = "content")
  private String postContent;

  @Column(name = "status")
  private String postStatus;

  @Column(name = "created_at")
  @CreationTimestamp
  private Timestamp createdAt;

  @Column(name = "updated_at")
  @UpdateTimestamp
  private Timestamp updatedAt;

  
  @ManyToMany
  @JoinColumn(name = "tag_id")
  private List<Tag> tags;

Tag.java

@Entity
@Table(name = "tag")
@Data
public class Tag {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = "id")
  private int id;

  @Column(name = "name")
  private String tagName;

  @Column(name = "created_at")
  @CreationTimestamp
  private Timestamp createdAt;

  @Column(name = "updated_at")
  @UpdateTimestamp
  private Timestamp updatedAt;

  @ManyToMany(mappedBy = "tags"))
  private List<Post> posts;
0

I have removed all the mappings, I can get user, post and post tags in three different calls and its working fine i have tried all the mapping explained or shown above but am getting error while read/write operations and to avoid all those i have made the change so that it does not have any mapping

LAS
  • 113
  • 1
  • 2
  • 14
-1

Most likely there is a problem with ManyToMany mapping or the way of fetching entities. Lets try to change the mapping first. As you said in the comment you'd like to use bidirectional mapping and you are free to use it of course, but show hibernate who is the owner of the relationship, by using mappedBy.

If Tag, then:

@Entity
@Table(name = "tag")
@Data
public class Tag {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = "id")
  private int id; // I'd use object instead of primitive
  
  // other columns  

  @ManyToMany(
      cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.DETACH, CascadeType.REFRESH})
  @JoinTable(
      name = "post_tag",
      joinColumns = @JoinColumn(name = "tag_id"),
      inverseJoinColumns = @JoinColumn(name = "post_id"))
  private Set<Post> posts = new HashSet<>(); // use set it is much more efficient, also worth to initialize

  // getters, setters, add and remove synchronization methods
}
@Entity
@Table(name = "post")
@Data
public class Post {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = "id")
  private int id;

  @ManyToOne(
      cascade = {CascadeType.MERGE, CascadeType.DETACH, CascadeType.REFRESH}, fetch = FetchType.LAZY) // by default EAGER, not efficient 
  @JoinColumn(name = "user_id")
  private User user;

  // other columns 

  @ManyToMany(mappedBy = "posts",
      cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.DETACH, CascadeType.REFRESH})
  private Set<Tag> tags = new HashSet<>();

  // getters, setters
}

so summing it up:

  1. Show hibernate who is the owner of the relationship
  2. Use FetchType.LAZY for @ManyToOne
  3. Use Set for @ManyToMany. In case of List, when removing entity hibernate removes all entities and re-adds those which were not removed.
  4. Add synchronization add and remove methods to User and Tag. If you use bidirectional mapping, you have to maintain both sides of the relationship.
  5. Consider adding CascadeType.ALL on every @OneToMany side as well as orphanRemoval = true
jwpol
  • 573
  • 3
  • 17
-1

you can use data transfer object to swow that field what you neen, maybe you can replace array of users in json to array users ids like this

@Transactional
public PostDto savePost(Post post) {
    Post save = postRepository.save(post);
    return Optional.of(save).map(this::transformPostEntityToDto).orElse(null);
}

@Override
public List<PostDto> getAllPosts() {
    return postRepository.findAll().stream()
            .map(this::transformPostEntityToDto).collect(Collectors.toList());
}

private PostDto transformPostEntityToDto(Post post) {
    return PostDto.builder()
            .id(post.getId())
            .createdAt(post.getCreatedAt())
            .postContent(post.getPostContent())
            .postStatus(post.getPostStatus())
            .postTitle(post.getPostTitle())
            .tags(Objects.nonNull(post.getTags())
                    ? post.getTags().stream().map(this::transformTagEntityToDto).collect(Collectors.toList())
                    : Collections.emptyList())
            .updatedAt(post.getUpdatedAt())
            .user(Optional.ofNullable(post.getUser()).map(this::transformUserEntityToDto).orElse(null))
            .build();
}

private TagDto transformTagEntityToDto(Tag tag) {
    return TagDto.builder()
            .id(tag.getId())
            .createdAt(tag.getCreatedAt())
            .tagName(tag.getTagName())
            .updatedAt(tag.getUpdatedAt())
            .postsIds(Objects.nonNull(tag.getPosts()) ? tag.getPosts().stream().map(Post::getId).collect(Collectors.toList())
                    : null)
            .build();
}

private UserDto transformUserEntityToDto(User user) {
    return UserDto.builder()
            .createdAt(user.getCreatedAt())
            .firstName(user.getFirstName())
            .id(user.getId())
            .lastName(user.getLastName())
            .password(user.getPassword())
            .updatedAt(user.getUpdatedAt())
            .userName(user.getUserName())
            .postsIds(Objects.nonNull(user.getPosts()) ? user.getPosts().stream().map(Post::getId).collect(Collectors.toList())
                    : null)
            .build();
}

it is flexiable but requare several dto classes for views

-1

Regarding the LazyInitializationException, it has to do with a 'fetch' mode and nothing to do with Jackson serialization. Take a look at this question: Default fetch type for one-to-one, many-to-one and one-to-many in Hibernate. Solutions for that are either setting loading to eager: https://www.baeldung.com/hibernate-lazy-eager-loading, or fetch joins: https://www.baeldung.com/jpa-join-types#fetch. If you are using hibernate without JPA abstraction over it, you can also take a look at this: Hibernate: best practice to pull all lazy collections

Regarding the infinite recursion problem when using jackson, take a look at: https://www.baeldung.com/jackson-bidirectional-relationships-and-infinite-recursion. @JsonManagedReference and @JsonBackReference are a nice option, however you cannot have a situation where you use same classes to get user with posts and same classes to get post with its user. Either you have to "ignore" user when serializing post or "ignore" post when serializing user. Otherwise you always get the infinite loop. Solution for that is using DTOs (Data Transfer Object)

Another important thing is that when using bidirectional mappings with JPA, you have to yourself set the "reference to the owner" of the collection (in this case, when adding a post to a user, make sure you also set the user reference on the post object to that user). In this example you see how in the class Post in line 22 you set the reference on the currently being added PostComment object for the post attribute to this

Michal Pasinski
  • 526
  • 6
  • 19
-1

Let's make it work step by step.

Since you're using Spring Boot, I suspect these entities are returned directly from the REST controller(s). So when you try to return a user and call getUserById() it does the following:

  • Hibernate fetches the user by id and sets the lazy collection of posts
  • Spring is trying to create a JSON of this user using Jackson, which is calling all the getters
  • since posts are not loaded yet hibernate will either
    • a. load all posts in an additional SQL SELECT if the session is still open
    • b. throw LazyInitializationExcepiton is the session is closed

So your homework task #1 is to make sure the session is still open (if you really need this). By default in the Spring Boot app Hibernate session boundaries are the same as Spring @Transactional boundaries. (HOWEVER, in case you are using Spring Boot 2+, find property spring.jpa.open-in-view. It's true by default and it registers OpenEntityManagerInViewInterceptor that gives you open Hibernate session during the whole lifetime of a web request.)

When the session is open and the lazy loading will work, the following will happen:

  • one user is loaded into the session by id
  • all posts of that user are lazy loaded when Jackson calls the getter
  • since Jackson recursively goes and calls all the getters, each post.getTags() will be called
  • now each tag.getPosts() will be called
  • and again each post.getUser() and post.getTags() will be called
  • ...

as you can see, you will load all of your DB to the application + you'll get StackOverflowException :(

So you homework task #2 is to put back @JsonMappedReference, @JsonBackReference (for instance if you load all tags for posts then you should not load all posts for tags).

I have to mention that this is not the right way to do it. It is much better to load everything you need first (for instance using join fetch) and then start building a JSON. But since you've asked... :)

Regarding the write operation, it is a bit trickier and it depends on HOW you actually do it. But at least make sure that session and transaction are open when you're trying to write data to the DB.

Taras Boychuk
  • 864
  • 5
  • 16