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=?