@ManyToOne
가장 흔한 어노테이션으로 관계형 데이터베이스가 관계를 맺는 방식과 유사합니다.
ex)
@ManyToOne 연관관계가 설정이 되면, 하이버네이트가 데이터베이스 외래키를 설정하게 됩니다.
@OneToMany
@OneToMany 연관관계는 부모 엔티티에서 하나 이상의 자식 엔티티를 참조하는 연관관계 입니다.
만약 자식 엔티티 쪽에 @ManyToOne 연관관계가 있다면 양방향이고, 그렇지 않다면 단방향입니다.
단방향 @OneToMany
단방향 @OneToMany를 사용한다면, 하이버네이트가 어쩔 수 없이 두 개의 엔티티와 연결된 링크 테이블을 추가로 만들게 됩니다.
ex)
이 엔티티들을 이용해 데이터베이스 스키마를 생성하게 되면 아래와 같은 결과가 나옵니다.
Person , Phone 테이블과 별개로 Person_Phone 테이블이 새로 생성되는 것을 볼 수 있습니다.
하이버네이트의 입장에서 본다면 외래키를 가진 자식 엔티티쪽에서 제어권이 없기 때문에 새로운 테이블을 만들어 관리하는 것 같습니다..
단방향 @OneToMany를 이용한 데이터 삽입과 삭제
방금 위에서 나온 단방향 @OneToMany Person, Phone을 이용하여 삽입 삭제를 위와 같이 해보겠습니다.
@OneToMany 연관은 단방향이든 양방향이든, 부모쪽에서 정의되고 부모쪽 연관관계만 자식에게 엔티티 상태를 전이시킬 수 있기 때문에 아래와 같은 결과가 나오게 됩니다.
삽입 같은 경우 예상한 대로 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)
양방향 관계의 경우 개발자들이 양방향 싱크를 확실히 맞추어야 합니다.
이에 따라서 부모 엔티티쪽에 addPhone() 이나 removePhone() 같은 헬퍼 메소드를 구현해놓으면 관리하기가 매우 편합니다.
그리고, 위 설정에 따라 생성된 스키마는 아래와 같습니다.
양방향 @OneToMany를 이용한 데이터 삽입과 삭제
위의 예제에서 설정한 양방향 @OneToMany를 가지고 데이터 삽입 및 삭제를 해보겠습니다.
위 코드에 따른 결과는 아래와 같습니다.
단방향 @OneToMany와는 다르게 양방향 관계가 훨씬 더 효율적으로 동작하는 걸 볼 수 있습니다.
@OneToOne
@OneToOne도 단방향 혹은 양방향 연관관계를 맺을 수 있습니다.
단방향 @OneToOne
관계를 소유하는 쪽이 외래키를 가지게 됩니다.
ex)
위 설정에 따른 DB 스키마는 아래와 같습니다.
위의 스키마에서는 관계를 소유한 쪽에 외래키를 두기 때문에 Phone에 외래키가 생긴것을 볼 수 있습니다. 하지만 Phone은 PhoneDetails의 부모이기 때문에 좀 더 자연스러운 매핑은 자식인 PhoneDetails 쪽에 외래키를 놓는 방법입니다.
그래서 이 매핑은 양방향 관계를 하는게 좀 더 자연스럽습니다.
양방향 @OneToOne
양방향 관계의 경우 부모쪽에 mappedBy를 설정해주면 됩니다.
위에 따른 스키마는 아래와 같습니다.
이 경우에도 역시, PhoneDetails가 외래키를 가지고 있고, 다른 양방향 관계와 유사하게, 부모쪽에서 자신의 라이프사이클을 자식에게 전파하고 있습니다.
양방향 @OneToOne 연관관계 데이터 삽입 및 삭제
위의 예제에서 설정한 것을 가지고 데이터 삽입 및 삭제를 해보겠습니다.
위 처럼 삽입을 하면 결과가 아래와 같습니다.
부모 데이터에 삽입하면서 자연스럽게 자식도 삽입이 된 모습입니다.
여기서 주의할점은, 하이버네이트가 양방향 @OneToOne 연관관계에서 자식을 가져올 때 유니크 제약을 건다는 것입니다.
만약 같은 부모와 둘 이상 연관을 가진 자식이 있다면 하이버네이트가 org.hibernate.exception.ConstraintViolationException을 날리게 됩니다.
이전 예제에서 이미 연관관계에 있던 phone에 PhoneDetail 자식 객체 하나를 추가한 뒤에 phone을 조회했을 때 에러가 나는 것을 볼 수 있습니다.
양방향 @OneToOne 지연 로딩
양방향 @OneToOne에서 하나 더 주의할 점은, 만약 부모쪽 연관관계에 지연로딩을 설정했을 때 제대로 동작하지 않는 다는 점입니다.
이는 하이버네이트가 자식쪽에 연관된 레코드가 있는지 없는지 알아내기 위해서 두 번째 쿼리를 날려야 하기 때문입니다.
그래서 이 경우에는 @OneToOne 대신에 @MapsId를 사용하거나
양방향 @OneToOne 및 지연로딩이 꼭 필요한 상황이라면 아래 예제 처럼 @LazyToOne 어노테이션을 사용하는게 좋습니다.
@ManyToMany
@ManyToMany 연관관계의 경우 단방향 @OneToMany처럼 참여한 두 엔티티간의 링크 테이블이 필요합니다.
@ManyToMany도 단방향 및 양방향 관계를 할 수 있습니다.
단방향 @ManyToMany
단방향 @ManyToMany의 경우 단방향 @OneToMany와 유사하게 동작합니다.
위 설정에 따른 결과는 아래와 같습니다.
단방향 @ManyToMany의 경우에도 단방향 @OneToMany 처럼 링크 테이블을 생성하는 걸 볼 수 있습니다.
그리고, 단방향 @OneToMany가 그랬던 것처럼 단방향 @ManyToMany도 데이터를 삭제할 때 부모와 연관된 모든 데이터를 전부 지운다음에 현재 리스트에 남아있는 것을 다시 삽입합니다.
ex)
위 결과를 보면 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와 연관이 되어 있기 때문에 아래와 같은 예외를 던질 것입니다.
그래서 @ManyToMany에서는 Cascade를 사용하지 않고, 대신에 단순히 부모 엔티티를 간단하게 지움으로써, 링크 데이터를 지울 수 있습니다.
양방향 @ManyToMany
양방향 @ManyToMany 역시 관계를 소유하는 쪽과, mappedBy 되는 쪽이 있습니다.
그리고, 양쪽의 데이터 싱크를 맞추기 위해서 부모쪽에서 자식 엔티티들을 추가하거나 지우는 헬퍼 메소드를 제공하는 것이 편합니다.
ex)
위 설정에 따른 결과는 아래와 같습니다.
양방향 @ManyToMany에서의 데이터 삽입 및 삭제
위에서 설정한 양방향 @ManyToMany에서 데이터 삽입 및 삭제를 해보겠습니다.
양방향 @ManyToMany의 경우 링크 테이블을 제어할 수 없기 때문에 단방향 @OneToMany에서 발생했던 데이터를 제거할 때 단점이 그대로 들어납니다. ( 콜렉션 전체를 delete 한뒤에 남아있는 콜렉션 데이터를 다시 삽입하는 문제,.. )
이러한 단점을 극복하기 위해서는, 링크 테이블을 엔티티로 노출시켜야 하며, 양방향 @ManyToMany 연관관계는 두 개의 양방향 @OneToMany 연관관계로 변경시켜야 합니다.
링크 엔티티가 포함된 두 개의 @OneToMany
데이터베이스 스키마와 동일하게 링크 테이블까지 엔티티로 생성합니다.
그리고 생성된 링크 엔티티는 양쪽 관계를 제어할 수 있는 연관관계 엔티티를 가지게 됩니다.
ex)
이에 따라 생성된 스키마는 아래와 같습니다.
이제 분리된 양방향 @OneToMany를 가지고 아래와 같이 다시 데이터를 삽입 및 삭제 해보겠습니다.
결과는 아래와 같습니다.
이 경우 아까와 다르게 효율적으로 단 하나의 delete 문만 실행되는것을 볼 수 있으며, 이는 하이버네이트가 DML을 만들기 위해서 @ManyToOne쪽의 외래키만 주시하고 있으면 되기 때문입니다.
출처
docs.jboss.org/hibernate/orm/5.4/userguide/html_single/Hibernate_User_Guide.html#associations ( 하이버네이트 레퍼런스 가이드 )
'JPA' 카테고리의 다른 글
Spring 환경에서의 하이버네이트의 개념과 영속성 컨텍스트, 그리고 트랜잭션 (0) | 2021.11.21 |
---|---|
JPA 에서 개인정보 암호화, 복호화 자동화 하기 (0) | 2021.08.27 |