springhibernatespring-data-jpaopen-session-in-view

LazyInitializationException on bi-directional one-to-many relationship in Spring Boot


The following Spring Boot service method throws a Hibernate's LazyInitializationException when trying to add a Comment to a Post in post.addComment(comment) :

    @Service
    public class CommentService {

        @Autowired
        private PostRepository postRepository;

        @Autowired
        private CommentRepository commentRepository;


//.....
        /**
         * Creates a new comment
         *
         * @param newCommentDto data of new comment
         * @return id of the created comment
         *
         * @throws IllegalArgumentException if there is no blog post for passed newCommentDto.postId
         */
        public Long addComment(NewCommentDto newCommentDto) {
            try {
                Post post = postRepository.findById(newCommentDto.getPostId()).get();

                Comment comment = new Comment();
                comment.setComment(newCommentDto.getContent());

                post.addComment(comment);
                comment = commentRepository.save(comment);
                return comment.getId();
            } catch (Exception e) {
                throw new IllegalArgumentException("There's no posts for given ID.");
            }

        }

The entities are mapped as follows:

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.OneToMany;

@Entity
public class Post {

    @Id
    @GeneratedValue
    private Long id;

    private String title;

    @Column(length = 4096)
    private String content;

    private LocalDateTime creationDate;

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    public LocalDateTime getCreationDate() {
        return creationDate;
    }

    public void setCreationDate(LocalDateTime creationDate) {
        this.creationDate = creationDate;
    }

    public Long getId() {
        return id;
    }

    @OneToMany(
            mappedBy = "post",
            cascade = CascadeType.ALL,
            orphanRemoval = true
        )
    private List<Comment> comments = new ArrayList<>();


    public void addComment(Comment comment) {
        comments.add(comment);
        comment.setPost(this);
    }

    public List<Comment>getComments() {
        return this.comments;
    }



}

    import java.time.LocalDateTime;

import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;

import org.hibernate.annotations.OnDelete;
import org.hibernate.annotations.OnDeleteAction;

    @Entity
    public class Comment {

        @Id
        @GeneratedValue
        private Long id;

        private String comment;

        private String author;

        private LocalDateTime creationDate;

        @ManyToOne(fetch = FetchType.EAGER, optional = false)
        @JoinColumn(name = "post_id", nullable = false)
        @OnDelete(action = OnDeleteAction.CASCADE)
        private Post post;

        public Post getPost() {
            return post;
        }

        public void setPost(Post post) {
            this.post = post;
        }

        public Long getId() {
            return id;
        }

        public void setId(Long id) {
            this.id = id;
        }

        public String getComment() {
            return comment;
        }

        public void setComment(String comment) {
            this.comment = comment;
        }

        public String getAuthor() {
            return author;
        }

        public void setAuthor(String author) {
            this.author = author;
        }

        public LocalDateTime getCreationDate() {
            return creationDate;
        }

        public void setCreationDate(LocalDateTime creationDate) {
            this.creationDate = creationDate;
        }

    }

The ManyToOne fetch type of EAGER or LAZY on the Comment's post reference doesn't seem to make a difference.

What am I doing wrong here? How does one resolve this error?

With the change suggested by @mckszcz, now the following method on the same service throws some reflection exception when trying to get the comments from a post:

/**
 * Returns a list of all comments for a blog post with passed id.
 *
 * @param postId id of the post
 * @return list of comments sorted by creation date descending - most recent first
 */
public List<CommentDto> getCommentsForPost(Long postId) {
    List<Comment> comments = postRepository.getOne(postId).getComments();
    List<CommentDto> result = new ArrayList<>();
    comments.forEach(comment -> {
        result.add(new CommentDto(comment.getId(), comment.getComment(), comment.getAuthor(), comment.getCreationDate()));
    });
    return result;
}

What does that need to be changed into to return a list of all comments belonging to a post as described in the method's javadoc?


Solution

  • Turns out that in addition to rearranging the code in the addComment method to eradicate the LazyInitializationException (thrown by JPA due to the wrong owning side property, as pointed out by @mckszcz):

    /**
         * Creates a new comment
         *
         * @param newCommentDto data of new comment
         * @return id of the created comment
         *
         * @throws IllegalArgumentException if there is no blog post for passed newCommentDto.postId
         */
        public Long addComment(NewCommentDto newCommentDto) {
            try {
                Post post = postRepository.findById(newCommentDto.getPostId()).get();
    
                Comment comment = new Comment();
                comment.setComment(newCommentDto.getContent());
                comment.setAuthor(newCommentDto.getAuthor());
    
    
                comment.setPost(post);
                //post.addComment(comment);
                comment = commentRepository.save(comment);
                return comment.getId();
            } catch (Exception e) {
                throw new IllegalArgumentException("There's no posts for given ID.");
            }
    
        }
    

    in order to resolve the reflective InvocationTargetException in the getCommentsForPost(Long postId) method of the service, one needs to also introduce an extra finder method (allowing to search multiple children by an ID of their containing parent) to the CommentRepository:

    @Repository
    public interface CommentRepository extends JpaRepository<Comment, Long> {
    
        List<Comment> findByPostId(Long postId);
    
    }
    

    and then introduce corresponding changes around that repository into the faulty method:

    /**
     * Returns a list of all comments for a blog post with passed id.
     *
     * @param postId id of the post
     * @return list of comments sorted by creation date descending - most recent first
     */
    public List<CommentDto> getCommentsForPost(Long postId) {
        List<Comment> commentsForPost = commentRepository.findByPostId(postId);
        //List<Comment> comments = postRepository.getOne(postId).getComments();
        List<CommentDto> result = new ArrayList<>();
        commentsForPost.forEach(comment -> {
            result.add(new CommentDto(comment.getId(), comment.getComment(), comment.getAuthor(), comment.getCreationDate()));
        });
        return result;
    }
    

    Those two measures seem to have resolved the issues.