JPA의 관계는 양방향으로 설정하자.
JPA의 일대다 단방향 매핑시 발생하는 문제 정리
Author -< Book 과 같은 일대 다 관계에서 생각없이 Author쪽에만 Book List에 Mapping을 걸때 어떤 문제가 생길까? 예시와 함께 알아보자
샘플 Class
1. Author
@Entity
public class Author {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "author_id")
private List<Book> books = new ArrayList<>();
// 생성자, 게터/세터 등 생략
public void addBook(Book book) {
books.add(book);
}
public void removeBook(Book book) {
books.remove(book);
}
}
2. Book
@Entity
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
// 생성자, 게터/세터 등 생략
@Override
public String toString() {
return "Book [id=" + id + ", title=" + title + "]";
}
}
예시 코드는 Author 엔티티와 Book 엔티티 간의 일대다 관계를 보여주며 Author 엔티티는 books라는 컬렉션으로 Book 엔티티와 관계를 맺고 있다.
테스트
다음과 같이 간단한 테스트를 실행해보자
@Test
public void testOneToManyRelationship() {
Author author = new Author();
author.setName("John Doe");
Book book1 = new Book();
book1.setTitle("Book 1");
Book book2 = new Book();
book2.setTitle("Book 2");
author.addBook(book1);
author.addBook(book2);
// 2개의 Book을 가진 Author 생성
EntityManager entityManager = entityManagerFactory.createEntityManager();
entityManager.getTransaction().begin();
entityManager.persist(author); // A. 생성시점
entityManager.getTransaction().commit();
entityManager.close();
entityManager = entityManagerFactory.createEntityManager();
Author savedAuthor = entityManager.find(Author.class, author.getId());
// 1개의 Book 삭제
savedAuthor.removeBook(book1);
entityManager.getTransaction().begin();
entityManager.merge(savedAuthor); // B. 삭제시점
entityManager.getTransaction().commit();
entityManager.close();
}
A. 생성시점의 persist() 에서는 단순하게 Author Insert 1회, Book Insert 2회가 발생할 것으로 예상할 수 있다.
하지만 예상과 다르게 Book Update 2회가 불필요하게 발생한다.
Hibernate: insert into author (name) values (?)
Hibernate: insert into book (id, title) values (null, ?)
Hibernate: insert into book (id, title) values (null, ?)
Hibernate: update book set author_id=? where id=?
Hibernate: update book set author_id=? where id=?
B. 삭제시점의 merge() 메소드를 호출하면 JPA는 Author 엔티티의 books 컬렉션에서 Book을 제거한 상태를 감지하고 데이터베이스에 해당 변경을 반영하기 위해 SQL UPDATE 문을 실행한다.
하지만 이 경우, 일대다 단방향 매핑에서는 Author 엔티티만이 Book 엔티티를 관리하고, Book 엔티티는 자신을 소유하는 엔티티에 대한 정보를 가지고 있지 않기 때문에, Author 엔티티의 books 컬렉션에서 Book을 제거하는 것만으로는 변경 사항을 정확히 표현할 수 없다.
실제로는 다음과 같은 행위가 일어난다.
Author의 Book 2개 모두 delete 되면서 Book1을 Book2가 Insert된다. 그리고 마지막에 Book 테이블에서 Book1 delete가 실행된다.
만약 Author의 Book이 수백 수천개라면? 수백 수천회의 Insert가 일어날 수 있다.
단순히 Author를 삭제한다고 해도 다음과 같은 결과가 나온다.
Hibernate: update book set author_id=null where author_id=? and id=?
Hibernate: delete from book where id=?
결론
일대다 매핑은 사용하지 말고 양방향으로 매핑하여 사용하자. 다른 매핑 관계 설정도 모두 쌍방으로 하자.
@Entity
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
// 생성자, 게터/세터 등 생략
// Author 필드 추가
@ManyToOne
@JoinColumn(name = "author_id")
private Author author;
@Override
public String toString() {
return "Book [id=" + id + ", title=" + title + "]";
}
}