0

im trying to return to a JPA data (converted to DTO, ofcourse) where it has a @OneToMany and @ManyToOne bidirectional relationship. Im currently apply thing fix. The problem is that the output is recusrive. comments has post has comments then has posts (comments -> post -> coments -> so on..).

I only wnat to have something like this

{
    "post_id": 1
    "user": {
        // user data
    },
    "description": "some description",
    "images": "{images,images}",
    "tags": "{tags, tags}",
    "comments": [
     {
        //some comments data

     },
     {
        //some comments data
     }
    ]
    "lastUpdated": "2020-04-08T14:23:18.000+00:00"
}

Here are my code

This is my Posts.class

@Data
@Entity
@Table(name = "posts")
@EntityListeners(AuditingEntityListener.class)
public class Posts {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "post_id")
    private Long post_id;

    @ManyToOne
    @JoinColumn(name = "user_id", nullable = false)
    private Users user;

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

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

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

    @OneToMany(
        fetch = FetchType.LAZY,
        mappedBy = "post",
        cascade = CascadeType.ALL,
        orphanRemoval = true
    )
    @JsonIgnoreProperties(value = { "comments" ,"hibernateLazyInitializer", "handler" }, allowSetters = true)
    //@JsonManagedReference
    private List<Comments> comments;


    @LastModifiedDate
    @Column(name = "last_updated")
    private Date lastUpdated;


    public void addComments(Comments comment) {
        this.comments.add(comment);
    }
}

Here is my PostDTO.class

@Data
public class PostDTO {

    private Long post_id;
    private UserDTO user;
    private String description;
    private String images;
    private String tags;
    private List<CommentsDTO> comments;
    private Date lastUpdated;


}

This is my Comments.class

@Data
@Entity
@Table(name = "comments")
@EntityListeners(AuditingEntityListener.class)
public class Comments {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name="comment_id")
    private Long comment_id;

    @OneToOne
    @JoinColumn(name = "user_id")
    private Users user;

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

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

    @ManyToOne
    @JoinColumn(name ="post_id" , nullable = false)
    @JsonIgnoreProperties(value = { "post" ,"hibernateLazyInitializer", "handler" }, allowSetters = true)
    //@JsonBackReference
    private Posts post;

    @Column(name="last_updated")
    @LastModifiedDate
    private Date lastUpdated;

}

Here is my CommentsDTO.class

@Data
public class CommentsDTO {

    private Long comment_id;

    private UserDTO user;

    private String description;

    private PostDTO post;

    private String images;

    private Date lastUpdated;
}

Here is my REST Controller

@GetMapping
public @ResponseBody ResponseEntity<List<PostDTO>>  getAll() throws Exception {

    return new ResponseEntity<List<PostDTO>>(service.getAll(), HttpStatus.OK);
}

Here is my service

public List<PostDTO> getAll() throws Exception  {
    return repo.findAll()
               .stream()
               .map(this::convert)
               .collect(Collectors.toList());
}

private PostDTO convert(Posts e) {
    return  mapper.map(e, PostDTO.class);
}

Hope someone can shed light on my issue. Kinda lost as of this time.

lemoncodes
  • 2,371
  • 11
  • 40
  • 66

3 Answers3

1

Problem is when you convert Post into PostDTO then by using ModelMapper it calls all field's getter of Post for PostDTO. So this happened recursively for this mapper.map(e, PostDTO.class)

So, just remove private PostDTO post from CommentDTO , then modelmapper don't try to set PostDTO->comment-> post field. And you don't need bidirectional relation in DTO. DTO is all about what you want to show in response.

Eklavya
  • 17,618
  • 4
  • 28
  • 57
  • ahh i see, it worked, but how do i apply this to the actual entity since i cannot just remove the other due to the `OneToMany` relationship? – lemoncodes Apr 09 '20 at 07:55
  • you are removing from DTO not from entity so your relationship remains – Eklavya Apr 09 '20 at 07:57
  • Yes, i know, i was just curios how do you do the same with entity without removing the post field on `Comments` entitiy. Because when i try to return `Posts` entity on `ResponseEntity` the same recusion issue occurs. – lemoncodes Apr 09 '20 at 07:58
  • Why you need this with entity because you are going to call post getter for comments entity, JPA fetch data when you call the gettter for lazy – Eklavya Apr 09 '20 at 08:02
  • When you try to return Posts entity on ResponseEntity that recursion happened becasuse of deserialization call all getter of Posts. Thats why it is a good practice to use DTO for response – Eklavya Apr 09 '20 at 08:09
  • I see, so the constraint when using Entity is that due to the annotated relationship between entities it will cause deserialization issue, that is to control the constraint another level is introduced that is the DTOs – lemoncodes Apr 09 '20 at 08:18
  • Yes, by using DTO you show what you want to in response. Recursively fetch is feature not a bug. – Eklavya Apr 09 '20 at 08:20
0

You are breaking the json serialization cycle on the wrong class.

You are sending a list of PostDTO, but applied JsonBackReference to Comments and JsonManagedReference to Posts

Update

Note that ObjectMapper class, JsonManagedReference and JsonBackReference may from 2 packages

  • com.fasterxml.jackson.databind.ObjectMapper
  • com.fasterxml.jackson.annotation.JsonManagedReference
  • com.fasterxml.jackson.annotation.JsonBackReference

or:

  • org.codehaus.jackson.map.ObjectMapper
  • org.codehaus.jackson.annotate.JsonManagedReference
  • org.codehaus.jackson.annotate.JsonBackReference

If you are not consistent between all of them, the mis-matched annotation will be ignored and you will still experience infinite loop during serialization.

Lesiak
  • 22,088
  • 2
  • 41
  • 65
  • I don't think PostDTO` has a problem because i tried directly sending `Posts` back. Both has the same issue. I think there is the problem with Back and Managed Reference which i am at a lost. – lemoncodes Apr 09 '20 at 05:43
  • am using fasterxml for both – lemoncodes Apr 09 '20 at 07:23
  • also, i have no `codehaus` dependencies in my dep tree – lemoncodes Apr 09 '20 at 07:29
  • Have you tried to serialize manually: `String result = new ObjectMapper().writeValueAsString(posts);` and check if that reproduces the issue? – Lesiak Apr 09 '20 at 07:32
  • yes, tried this part on my controller prior to return `ResponseEntity`and confirmed it via debug mode and surrounding it with `try-catch`, that line produces the same issue – lemoncodes Apr 09 '20 at 07:46
  • Cannot reproduce. Works perfectly on mine machine. – Lesiak Apr 09 '20 at 07:53
0

Use JsonIgnoreProperties along with Manage and backreference .

 @Data
    @Entity
    @Table(name = "comments")
    @EntityListeners(AuditingEntityListener.class)
    public class Comments {

        @Id
        @GeneratedValue(strategy = GenerationType.AUTO)
        @Column(name="comment_id")
        private Long comment_id;

        @OneToOne
        @JoinColumn(name = "user_id")
        private Users user;

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

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

        @ManyToOne
        @JoinColumn(name ="post_id" , nullable = false)
        @JsonIgnoreProperties(value = { "comments" ,"hibernateLazyInitializer", "handler" }, allowSetters = true)
       @JsonBackReference
        private Posts post;

        @Column(name="last_updated")
        @LastModifiedDate
        private Date lastUpdated;

    }

And Your Post class should be

@Data
@Entity
@Table(name = "posts")
@EntityListeners(AuditingEntityListener.class)
public class Posts {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "post_id")
    private Long post_id;

    @ManyToOne
    @JoinColumn(name = "user_id", nullable = false)
    private Users user;

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

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

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

    @OneToMany(
        fetch = FetchType.LAZY,
        mappedBy = "post",
        cascade = CascadeType.ALL,
        orphanRemoval = true
    )
    @JsonIgnoreProperties(value = { "post" ,"hibernateLazyInitializer", "handler" }, allowSetters = true)
    @JsonManagedReference
    private List<Comments> comments;


    @LastModifiedDate
    @Column(name = "last_updated")
    private Date lastUpdated;


    public void addComments(Comments comment) {
        this.comments.add(comment);
    }
}

Also I would suggest to Use Getter and Setter annotation instead of @Data .Because Tostring() method will cause recursion also .

soorapadman
  • 4,451
  • 7
  • 35
  • 47