Spring) Spring JPA Circular Reference
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.