본문 바로가기

JPA

JPA(hibernate) 연관관계 정리

@ManyToOne

 

가장 흔한 어노테이션으로 관계형 데이터베이스가 관계를 맺는 방식과 유사합니다.

 

ex)

 

@ManyToOne 간단 예제

@ManyToOne 연관관계가 설정이 되면, 하이버네이트가 데이터베이스 외래키를 설정하게 됩니다.

 

 

@OneToMany

 

@OneToMany 연관관계는 부모 엔티티에서 하나 이상의 자식 엔티티를 참조하는 연관관계 입니다.

 

만약 자식 엔티티 쪽에 @ManyToOne 연관관계가 있다면 양방향이고, 그렇지 않다면 단방향입니다.

 

 

단방향 @OneToMany

 

단방향 @OneToMany를 사용한다면, 하이버네이트가 어쩔 수 없이 두 개의 엔티티와 연결된 링크 테이블을 추가로 만들게 됩니다.

 

ex)

 

단방향 @OneToMany 연관관계 설정

이 엔티티들을 이용해 데이터베이스 스키마를 생성하게 되면 아래와 같은 결과가 나옵니다.

 

단방향 @OneToMany 스키마 생성 결과

Person , Phone 테이블과 별개로 Person_Phone 테이블이 새로 생성되는 것을 볼 수 있습니다.

 

하이버네이트의 입장에서 본다면 외래키를 가진 자식 엔티티쪽에서 제어권이 없기 때문에 새로운 테이블을 만들어 관리하는 것 같습니다..

 

단방향 @OneToMany를 이용한 데이터 삽입과 삭제

 

Person, Phone 데이터 삽입 및 삭제

방금 위에서 나온 단방향 @OneToMany Person, Phone을 이용하여 삽입 삭제를 위와 같이 해보겠습니다.

 

@OneToMany 연관은 단방향이든 양방향이든, 부모쪽에서 정의되고 부모쪽 연관관계만 자식에게 엔티티 상태를 전이시킬 수 있기 때문에 아래와 같은 결과가 나오게 됩니다.

 

단방향 @OneToMany Person, Phone DB 삽입 및 삭제 결과

 

삽입 같은 경우 예상한 대로 Person, Phone 그리고 링크 테이블인 Person_Phone에 잘 들어가는 걸 볼 수 있습니다. 

 

그런데 빨간색 박스로 된 phone을 제거 하는 쪽이 쿼리를 이상하게 날리는 것을 볼 수 있습니다.

 

상식적으로 Phone 하나를 제거했기 때문에 Person_Phone에서 ( pseron_id = 1 , phones_id = 2 ) , Phone에서 id = 2인 데이터만 지우면 되는데

 

Person_Phone에서 뜬금없이 Person_id = 1인 것을 전부 지우고, ( person_id = 1 , phones_id = 3 ) 을 다시 삽입하는 이상한? 짓을 하고 있는 것입니다.

 

아마 참조를 가지고 있는 Person 쪽에 외래키가 없기 때문에 이런 현상이 발생한 것으로 보입니다.

 

결론적으로 단방향 @OneToMany의 경우 링크 테이블이 새로 생기는 문제, 데이터 삭제시 비효율적으로 동작하는 문제 등으로 보았을 때 효율적인 방법은 아니라고 생각됩니다.

 

양방향 @OneToMany

 

양방향 @OneToMany의 경우 자식 엔티티쪽에 @ManyToOne 연관관계를 필요로 합니다. 그리고 관계형 DB는 단방향일 때와는 다르게 링크 테이블을 생성하지 않고 자식 테이블 쪽에 외래키만을 가지게 됩니다.

 

ex)

 

양방향 @OneToMany 연관관계 설정

 

양방향 관계의 경우 개발자들이 양방향 싱크를 확실히 맞추어야 합니다.

 

이에 따라서 부모 엔티티쪽에 addPhone() 이나 removePhone() 같은 헬퍼 메소드를 구현해놓으면 관리하기가 매우 편합니다.

 

그리고, 위 설정에 따라 생성된 스키마는 아래와 같습니다.

 

양방향 @OneToMany 스키마 생성 결과

 

양방향 @OneToMany를 이용한 데이터 삽입과 삭제

 

위의 예제에서 설정한 양방향 @OneToMany를 가지고 데이터 삽입 및 삭제를 해보겠습니다.

 

양방향 @OneToMany 데이터 삽입 및 삭제

위 코드에 따른 결과는 아래와 같습니다.

 

양방향 @OneToMany 데이터 삽입 및 삭제 결과

단방향 @OneToMany와는 다르게 양방향 관계가 훨씬 더 효율적으로 동작하는 걸 볼 수 있습니다.

 

 

@OneToOne

 

@OneToOne도 단방향 혹은 양방향 연관관계를 맺을 수 있습니다.

 

 

단방향 @OneToOne

 

관계를 소유하는 쪽이 외래키를 가지게 됩니다.

 

ex)

 

단방향 @OneToOne

위 설정에 따른 DB 스키마는 아래와 같습니다.

 

단방향 @OneToOne 스키마 생성 결과

위의 스키마에서는 관계를 소유한 쪽에 외래키를 두기 때문에 Phone에 외래키가 생긴것을 볼 수 있습니다. 하지만 Phone은 PhoneDetails의 부모이기 때문에 좀 더 자연스러운 매핑은 자식인 PhoneDetails 쪽에 외래키를 놓는 방법입니다.

 

그래서 이 매핑은 양방향 관계를 하는게 좀 더 자연스럽습니다.

 

 

양방향 @OneToOne

 

양방향 관계의 경우 부모쪽에 mappedBy를 설정해주면 됩니다.

 

양방향 @OneToOne 연관관계

위에 따른 스키마는 아래와 같습니다.

 

양방향 @OneToOne 연관관계 스키마

이 경우에도 역시, PhoneDetails가 외래키를 가지고 있고, 다른 양방향 관계와 유사하게, 부모쪽에서 자신의 라이프사이클을 자식에게 전파하고 있습니다.

 

 

양방향 @OneToOne 연관관계 데이터 삽입 및 삭제

 

위의 예제에서 설정한 것을 가지고 데이터 삽입 및 삭제를 해보겠습니다.

 

양방향 @OneToOne 데이터 삽입 및 삭제

위 처럼 삽입을 하면 결과가 아래와 같습니다.

양방향 @OneToOne 데이터 삽입 및 삭제 결과

부모 데이터에 삽입하면서 자연스럽게 자식도 삽입이 된 모습입니다.

 

여기서 주의할점은, 하이버네이트가 양방향 @OneToOne 연관관계에서 자식을 가져올 때 유니크 제약을 건다는 것입니다.

 

만약 같은 부모와 둘 이상 연관을 가진 자식이 있다면 하이버네이트가 org.hibernate.exception.ConstraintViolationException을 날리게 됩니다.

 

양방향 @OneToOne 유니크 제약

이전 예제에서 이미 연관관계에 있던 phone에 PhoneDetail 자식 객체 하나를 추가한 뒤에 phone을 조회했을 때 에러가 나는 것을 볼 수 있습니다.

 

양방향 @OneToOne 지연 로딩

 

양방향 @OneToOne에서 하나 더 주의할 점은, 만약 부모쪽 연관관계에 지연로딩을 설정했을 때 제대로 동작하지 않는 다는 점입니다.

 

이는 하이버네이트가 자식쪽에 연관된 레코드가 있는지 없는지 알아내기 위해서 두 번째 쿼리를 날려야 하기 때문입니다.

 

그래서 이 경우에는 @OneToOne 대신에 @MapsId를 사용하거나

 

양방향 @OneToOne 및 지연로딩이 꼭 필요한 상황이라면 아래 예제 처럼 @LazyToOne 어노테이션을 사용하는게 좋습니다.

 

양방향 @OneToOne 지연로딩에서의 @LazyToOne

 

@ManyToMany

 

@ManyToMany 연관관계의 경우 단방향 @OneToMany처럼 참여한 두 엔티티간의 링크 테이블이 필요합니다.

 

@ManyToMany도 단방향 및 양방향 관계를 할 수 있습니다.

 

 

단방향 @ManyToMany

 

단방향 @ManyToMany의 경우 단방향 @OneToMany와 유사하게 동작합니다.

 

단방향 @ManyToMany

 

위 설정에 따른 결과는 아래와 같습니다.

 

단방향 @ManyToMany 스키마 생성 결과

단방향 @ManyToMany의 경우에도 단방향 @OneToMany 처럼 링크 테이블을 생성하는 걸 볼 수 있습니다.

 

그리고, 단방향 @OneToMany가 그랬던 것처럼 단방향 @ManyToMany도 데이터를 삭제할 때 부모와 연관된 모든 데이터를 전부 지운다음에 현재 리스트에 남아있는 것을 다시 삽입합니다.

 

ex)

 

단방향 @ManyToMany 데이터 삽입 및 삭제
단방향 @ManyToMany 데이터 삽입 및 삭제 결과

 

위 결과를 보면 Person_Address ( person_id = 1, addresses_id = 2 )를 제거하는게 가장 효율적이지만 Person Address에서 Person_id = 1인 모든 데이터를 삭제하고 다시 Person_Address ( person_id = 1, addresses_id= 3 )을 삽입하는 것을 볼 수 있습니다.

 

또, 맨 위 자바 설정을 보면 Cascade를 ALL로 설정하지 않고, PERSIST, MERGE만 설정한 것을 볼 수 있는데, 이는 REMOVE 엔티티 Cascade 설정이 @ManyToMany에서 말이 안되기 때문입니다.

 

연관된 자식 엔티티에서 또 다른 부모 엔티티와 연관이 되어 있을 수 있기 때문에 Cascade.remove를 통해 부모 자식을 자동 제거하는 것은 ConstraintViolationException을 부르게 됩니다.

 

만약 위 설정에서 @ManyToMany(cascade = CascadeType.ALL)이 정의되어 있고, 첫 번째 person이 지워졌다면, 하이버네이트는 다른 person이 여전히 지워진 address와 연관이 되어 있기 때문에 아래와 같은 예외를 던질 것입니다.

 

ConstraintViolationException 발생

그래서 @ManyToMany에서는 Cascade를 사용하지 않고, 대신에 단순히 부모 엔티티를 간단하게 지움으로써, 링크 데이터를 지울 수 있습니다.

 

@ManyToMany에서의 제거

 

양방향 @ManyToMany

 

양방향 @ManyToMany 역시 관계를 소유하는 쪽과, mappedBy 되는 쪽이 있습니다.

 

그리고, 양쪽의 데이터 싱크를 맞추기 위해서 부모쪽에서 자식 엔티티들을 추가하거나 지우는 헬퍼 메소드를 제공하는 것이 편합니다.

 

ex)

 

양방향 @ManyToMany Person
양방향 @ManyToMany Address

위 설정에 따른 결과는 아래와 같습니다.

 

양방향 @ManyToMany 스키마 결과

 

양방향 @ManyToMany에서의 데이터 삽입 및 삭제

 

위에서 설정한 양방향 @ManyToMany에서 데이터 삽입 및 삭제를 해보겠습니다.

 

양방향 @ManyToMany에서의 데이터 삽입 및 삭제
양방향 @ManyToMany에서의 데이터 삽입 및 삭제 결과

 

양방향 @ManyToMany의 경우 링크 테이블을 제어할 수 없기 때문에 단방향 @OneToMany에서 발생했던 데이터를 제거할 때 단점이 그대로 들어납니다. ( 콜렉션 전체를 delete 한뒤에 남아있는 콜렉션 데이터를 다시 삽입하는 문제,.. )

 

이러한 단점을 극복하기 위해서는, 링크 테이블을 엔티티로 노출시켜야 하며, 양방향 @ManyToMany 연관관계는 두 개의 양방향 @OneToMany 연관관계로 변경시켜야 합니다.

 

링크 엔티티가 포함된 두 개의 @OneToMany

 

데이터베이스 스키마와 동일하게 링크 테이블까지 엔티티로 생성합니다.

 

그리고 생성된 링크 엔티티는 양쪽 관계를 제어할 수 있는 연관관계 엔티티를 가지게 됩니다.

 

ex)

 

두 개의 양방향 @OneToMany (Person)
두 개의 양방향 @OneToMany (Address)
링크 엔티티 (PersonAddress)

이에 따라 생성된 스키마는 아래와 같습니다.

 

두 개의 양방향 @OneToMany로 생성된 스키마

이제 분리된 양방향 @OneToMany를 가지고 아래와 같이 다시 데이터를 삽입 및 삭제 해보겠습니다.

 

Person 및 Address 데이터 삽입 및 삭제

결과는 아래와 같습니다.

 

두 개의 분리된 양방향 @OneToMany 데이터 삽입 및 삭제 결과

이 경우 아까와 다르게 효율적으로 단 하나의 delete 문만 실행되는것을 볼 수 있으며, 이는 하이버네이트가 DML을 만들기 위해서 @ManyToOne쪽의 외래키만 주시하고 있으면 되기 때문입니다.

 

출처

 

docs.jboss.org/hibernate/orm/5.4/userguide/html_single/Hibernate_User_Guide.html#associations ( 하이버네이트 레퍼런스 가이드 )