Spring JPA - How to select specific columns only with a bi-directional relationship to avoid recursion?

380 Views Asked by At

json recursion is a known issue with bi-directional relations in JPA. There are many solutions, like using @JsonIdentityInfo and @JsonManagedReference and @JsonBackReference, but all they end up doing is not returning the specific node in the json itself. My need is little different, where I do want the node to be returned but only certain fields, and not the node that causes the recursion.

To keep things simple, the tables look like this, I have 2 tables, user and article. One user can have multiple articles (the business case is little different, just modifying it here to keep things simple)

User:
user_id, first_name, last_name

Article:
article_id, article_text, user_id (foreign key to User table)

The Entity classes look like this:

@Entity
@Table (name = "user")
@Data  // lombok to generate corresponding getters and setters
public class User
{
   private int userId;
   private String firstName;
   private String lastName;

   @OneToMany ()
   @JoinColumn (name="user")
   private List<Article> articles;
}

@Entity
@Table (name = "article")
@Data  // lombok to generate corresponding getters and setters
public class Article
{
   private int articleId;
   private String articleText;

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

The problem occurs when I make a call to get User using findById (), JPA internally populates "articles" as well (it might be Lazy but if I convert the object into a JSON string, it gets populated). Each article in the list has reference back to "user" object which it populates too, which again has "articles" and this goes on causing recursion and StackOverflowException.

My need is that in this particular case, when JPA is fetching and populating User object for a given article, I want only firstName and lastName to be populated and NOT the articles field. JPA is running those join queries internally while fetching articles, and I am not calling it myself. Note, I am just calling userRepository.findById () method.

I want JSON representation to look like this (Note that the first user node includes everything, including articles, but user node that's fetched along with article should NOT contains articles):

{
  "userId": 1,
  "firstName": "John",
  "lastName": "Doe",
  "articles": [
    {
       "articleId": 100,
       "articleText": "Some sample text",
       "user": {
          "userId": 1,
          "firstName": "John",
          "lastName": "Doe"
       }
    },
    {
       "articleId": 101,
       "articleText": "Another great article",
       "user": {
          "userId": 1,
          "firstName": "John",
          "lastName": "Doe"
       }
    }
  ]
}

Is this possible?

3

There are 3 best solutions below

0
AC1 On BEST ANSWER

I ended up refactoring the code. Separated out Entity classes and serialized classes, i.e. the class that fetches data from DB via JPA and the class that returns json back in the API. Thanks everyone for the answers and nudges.

2
Ahmed Nabil On

first, you add @JsonIgnore on articles property of User like this

   @JsonIgnore
   @OneToMany ()
   @JoinColumn (name="user")
   private List<Article> articles;

this solves the Json problem and will generate the required result,

and then, you override toString() of User Entity and don't include articles in it. that will prevent stackoverflow when printing it to the console

1
Tiago Medici On

The JPA specification allows us to customize results in an object-oriented fashion. Therefore, we can use a JPQL constructor expression to set the result, using the Select NEW :

@Data
@AllArgsConstructor
@NoArgsConstructor
@Entity
public class Comment {
    @Id
    private Integer id;
    private Integer year;
    private boolean approved;
    private String content; 
}

@Data
@AllArgsConstructor
@NoArgsConstructor
public class CommentCount {
    private Integer year;
    private Long total;
}


@Query("SELECT new CommentCount(c.year, COUNT(c.year)) "
  + "FROM Comment AS c GROUP BY c.year ORDER BY c.year DESC")
List<CommentCount> countTotalCommentsByYearClass();