Spring) Spring JPA Circular Reference
This post was migrated from Tistory. You can find the original here.
Circular Reference
Suppose we have the ERD shown above.
When we call findAll on registered books, a problem occurs.
1
2
List<Book> all = bookRepository.findAll();
System.out.println(all);
If the Book entity has @ToString applied, toString runs automatically inside println.
The expected result would be something like:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[
{
"id": 1,
"title": "자바의정석",
"author": "남궁성",
"price": 10000,
"stock": 10,
"bookStore": {
"id": 1,
"name": "스파르타 서울",
"address": "서울시 강남구"
},
"purchase" : {
"id" : 1,
"member" : {
...
..
}
}
},
...
That’s what I expected, but what actually happened was an endless circular reference resulting in a StackOverflowError.
There’s a cycle that keeps going: book -> bookStore -> book -> bookStore -> …
findAll itself doesn’t throw an error. findAll just returns reference addresses like [com.example.jpa_relation_test.entity.Book@429a501f, com.example.jpa_relation_test.entity.Book@34d511c0, ...] — it doesn’t actually walk through the references.
1
2
3
4
5
6
7
8
9
10
11
12
13
(Book)
public BookStore getBookStore() {
return this.bookStore;
}
public List<Purchase> getPurchases() {
return this.purchases;
}
public String toString() {
Long var10000 = this.getId();
return "Book("...." + "bookStore=" + this.getBookStore() + ", purchases=" + this.getPurchases() + ")";
}
Lombok’s @ToString generates a toString like the one above, passing the referenced object through as-is. An object’s string form is produced through its own toString, and
1
2
3
4
5
6
7
8
9
(BookStore)
public List<Book> getBookList() {
return this.bookList;
}
public String toString() {
Long var10000 = this.getId();
return "BookStore("...." + "bookList=" + this.getBookList() + ")";
}
BookStore’s toString passes bookList along, and that’s where the cycle kicks off.
In other words, the circular reference happens whenever the object is converted to a String (via return or print).
How to Resolve Circular References
@JsonBackReference / @JsonManagedReference
1
2
3
4
5
6
7
8
9
(BookStore)
@OneToMany(mappedBy = "bookStore")
@JsonBackReference
private List<Book> bookList = new ArrayList<>();
(Book)
@ManyToOne
@JsonManagedReference
private BookStore bookStore;
You can add jackson annotations like this. The “one” side gets @JsonBackReference, and the “many” side gets @JsonManagedReference.
1
2
3
4
public List<BookStore> findAllBookStore() {
List<BookStore> all = bookStoreRepository.findAll();
return all;
}
After adding the jackson annotations, calling findAll on bookStore gives:
1
2
3
4
5
6
7
8
9
10
11
12
13
[
{
"id": 1,
"name": "스파르타 서울",
"address": "서울시 강남구"
},
{
"id": 2,
"name": "스파르타 부산",
"address": "부산시 해운대구"
}
...
]
You can see the string is built without triggering a circular reference.
Unless you configure it otherwise, this approach still loads the “one” side when you query an entity on the “many” side. That’s why in the result above, bookStore — the “one” side — doesn’t load book.
1
2
3
4
5
6
7
8
9
10
(BookStore)
@OneToMany(mappedBy = "bookStore")
@JsonBackReference
private List<Member> member = new ArrayList<>();
(Member)
@ManyToOne
@JoinColumn(name = "SpartaStoreId")
@JsonManagedReference
private BookStore bookStore;
Given the entity relationship above, if we load member:
1
2
3
4
5
6
7
8
9
10
11
{
"id": 1,
"email": "sparta@sparta.com",
...
"nickname": "스파르타",
"bookStore": {
"id": 1,
"name": "스파르타 서울",
"address": "서울시 강남구"
}
},
Since member is on the “many” side, it loads the corresponding bookStore on the “one” side.
Using a DTO
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class BookDto {
@Getter
@ToString
public static class Response {
private Long id;
private String title;
private String author;
private Integer price;
private Integer stock;
private BookStoreDto bookStore;
public Response(Book entity) {
this.id = entity.getId();
this.title = entity.getTitle();
this.author = entity.getAuthor();
this.price = entity.getPrice();
this.stock = entity.getStock();
this.bookStore = new BookStoreDto(entity.getBookStore());
}
}
@Getter
@ToString
private static class BookStoreDto {
private Long id;
private String name;
private String address;
public BookStoreDto(BookStore entity){
this.id = entity.getId();
this.name = entity.getName();
this.address = entity.getAddress();
}
}
}
Using a DTO, you can solve this by simply not including the field that causes the cycle.
When Book references BookStore, it’s BookStore’s private List<Book> bookList field that triggers the cycle.
From Book’s perspective, there’s no need to see BookStore’s bookList at all, so we filter it out via a DTO and just put the filtered BookStoreDto inside Book.
1
2
3
4
public List<BookDto.Response> findAllBook() {
List<Book> all = bookRepository.findAll();
return all.stream().map(BookDto.Response::new).collect(Collectors.toList());
}
Converting Book to BookDto.Response like this avoids the circular reference.