Post

Spring) Spring JPA Circular Reference

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.

This post is licensed under CC BY-NC 4.0 by the author.