Spring Jpa ManyToOne 관계 알아보기
Spring Boot Persistence Best Practices 정리 글입니다.
- 부모 테이블, 엔티티를 부모라고 하겠습니다.
- 자식 테이블, 엔티티를 자식이라고 하겠습니다.
ManyToOne 관계
ManyToOne 이란 어떤것을 의미하는 것일까?
- OneToMany : 한 명의 저자는 여러 권의 책을 가질 수 있습니다.
- ManyToOne : 여러 권의 책을 쓴 한 명의 저자가 있다고 할 수 있습니다.
예시를 보면 부모는 Author 이고, 자식은 Book 입니다. Book 은 현재 Author 와 단방향 ManyToOne 관계를 가지고 있습니다.
@Entity
@Table(name = "author")
class Author(
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long? = null,
val name: String,
val genre: String,
)
@Entity
@Table(name = "book")
class Book(
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long? = null,
var title: String,
@ManyToOne
@JoinColumn(name = "author_id")
val author: Author
)
단방향 ManyToOne 에서의 테이블 생성
단방향 ManyToOne 관계를 맺으면 자식 테이블에 부모의 외래키가 같이 포함됩니다. 따라서 외래키 정보는 자식이 관리하게 됩니다. 단방향 OneToMany 를 맺을 때 중간테이블이 생길 수 있다는 것과는 다릅니다.
이유를 살펴보면 ManyToOne 에는 자식의 각레코드가 부모 정보를 가지면 되지만, 단방향 OneToMany 에서는 부모레코드가 모든 자식 정보를 표현하려면 중간테이블을 만들 수 밖에 없기 때문입니다. (물론 단방향 OneToMany 에서도 자식테이블에 부모정보를 포함할 수 있도록 설정할 수 있습니다. 다만 비효율적이기 때문에 양방향을 맺는것을 권장합니다.)
단방향 ManyToOne 에서 select, insert, update 쿼리 살펴보기.
Author(부모) 와 Book(자식) 을 예시로 쿼리를 살펴보겠습니다. Book(자식) 클래스에서는 Author(부모) 클래스를 알고 있고 Author(부모) 는 일반적으로 이미 생성된 레코드이기 때문에 Book(자식) 이 저장될 때 어떤 외래키값을 지정해야하는지 알 수 있습니다. 따라서 Book(자식) 에서 Author(부모) 정보를 설정하면 insert 쿼리는 한 개만 생성됩니다. 마찬가지로 update, delete 도 하나의 쿼리만 생성됩니다.
단방향 OneToMany 에서는 부모는 새로 만들 자식의 id 를 insert 하기 전까지 알 수 없습니다. 따라서 insert 한 후 id 정보를 가져와 update 를 하게됩니다. 다만 이미 만들어진 자식을 부모에 매핑시키는 것이라면 insert and update 가 일어나진 않습니다.
select 쿼리
단방향 ManyToOne 에서는 특별한 설정을 하지 않는 한 Eager fetch 가 발생하기 때문에 하나의 select statement 가 발생하게 됩니다.
fun manyToOneSelect() {
val book = bookRepository.findById(1)
println(book.get().author.name)
}
위 코드를 실행하면 아래와 같은 sql statement 가 발생합니다.
Hibernate: select b1_0.id,a1_0.id,a1_0.genre,a1_0.name,b1_0.title from book b1_0 left join author a1_0 on a1_0.id=b1_0.author_id where b1_0.id=?
insert 쿼리
단방향 ManyToOne 에서 외래키 관리를 자식이 하기 때문에 자식을 저장할 때 부모의 id 를 알 수 있다면 자식을 생성할 때 한 번의 insert stetement 만 발생하게 됩니다.
@Transactional
fun manyToOneInsert() {
val author = authorRepository.findById(1).get()
val newBook = Book(title = "newBook", author = author)
bookRepository.save(newBook)
}
위 코드를 실행하면 아래와 같은 sql statement 가 발생합니다.
Hibernate: select a1_0.id,a1_0.genre,a1_0.name from author a1_0 where a1_0.id=?
Hibernate: insert into book (author_id,title) values (?,?)
update 쿼리
단방향 ManyToOne 에서 외래 참조 정보를 바꾸는 것을 포함해 정보를 변경하는 것은 한 번의 update 만 발생합니다.
@Transactional
fun manyToOneUpdate() {
val author = authorRepository.findById(2).get()
val book = bookRepository.findById(1).get()
book.author = author
}
위 코드를 실행하면 아래와 같은 sql statement 가 발생합니다.
Hibernate: select a1_0.id,a1_0.genre,a1_0.name from author a1_0 where a1_0.id=?
Hibernate: select b1_0.id,a1_0.id,a1_0.genre,a1_0.name,b1_0.title from book b1_0 left join author a1_0 on a1_0.id=b1_0.author_id where b1_0.id=?
Hibernate: update book set author_id=?,title=? where id=?