1. 상속 관계 매핑
관계형 DB는 상속 개념이 없다.
- 슈퍼타입 서브타입 논리 모델을 실제 테이블로 구현하는 방법
- 각각의 테이블로 변환 : 모두 테이블로 만들고 조회할 때 조인을 사용(조인 전략)
- 통합 테이블로 변환 : 테이블을 하나만 사용해서 통합 (단일 테이블 전략)
- 서브타입 테이블로 변환 : 서브 타입마다 하나의 테이블을 만든다 (구현 클래스마다 테이블 전략)
조인 전략
기본키 + 외래 키로 사용하는 전략이다. 따라서 조회할 때 조인을 자주 사용한다. 하지만 테이블은 타입 개념이 없으므로 타입을 구분하는 컬럼을 추가해야 한다.
① : 상속 매핑은 부모 클래스에 @Inheritance를 사용한다. 그리고 매핑 전략을 지정해야 하는데 여기서는 조인 전략을 사용하므로 InheritanceType.JOINED를 사용했다.
② : 부모 클래스에 구분 컬럼을 지정한다. 이 컬럼으로 자식 테이블을 구분할 수 있다. 기본값이 DTYPE이므로 @DiscriminatorColumn으로 줄여 사용해도 된다.
③ : 엔티티를 저장할 때 구분 컬럼에 입력할 값을 지정한다. 만약 영화 엔티티를 저장하면 구분 컬럼인 DTYPE에 값 M이 저장된다.
BOOK 테이블의 ITEM_ID 기본 키 컬럼명을 BOOK_ID로 변경했다.
- 장점
- 테이블이 정규화된다.
- 외래 키 참조 무결성 제약조건을 활용할 수 있다.
- 저장공간을 효율적으로 사용한다.
- 단점
- 조회할 때 조인이 많이 사용되므로 성능이 저하될 수 있다.
- 조회 쿼리가 복잡하다.
- 데이터를 등록할 INSERT SQL을 두 번 실행한다.
- 특징
- JPA 표준 명세는 구분 컬럼을 사용하도록 하지만 하이버네이트를 포함한 몇 구현체는 구분 컬럼없이도 동작한다.
단일 테이블 전략
조인을 사용하지 않으므로 일반적으로 가장 빠르지만 자식 엔티티가 매핑한 컬럼은 모두 null을 허용 해야 한다는 점이다.
- 장점
- 조인이 필요 없으므로 조회 성능이 빠르다.
- 조회 쿼리가 단순하다.
- 단점
- 자식 엔티티가 매핑한 컬럼은 모두 null을 허용해야 한다.
- 단일 테이블에 모든 것을 저장하므로 테이블이 커질 수 있다. 그러므로 상황에 따라서는 조회 성능이 오히려 느려질 수 있다.
- 특징
- 구분 컬럼을 꼭 사용해야 한다.
- @DiscriminatorValue를 지정하지 않으면 기본으로 엔티티 이름을 사용한다(ex) Movie, Album, Book)
구현 클래스마다 테이블 전략
InheritanceType.TABLE_PER_CLASS 를 선택하면 구현 클래스마다 테이블 전략을 사용한다. 일반적으로 추천하지는 않는다.
- 장점
- 서브 타입을 구분해서 처리할 때 효과적이다.
- not null 제약조건을 사용할 수 없다,
- 단점
- 여러 자식 테이블을 함께 조회할 때 성능이 느리다(SQL에 UNION을 사용해야 한다).
- 자식 테이블을 통합해서 쿼리하기 어렵다.
- 특징
- 구분 컬럼을 사용하지 않는다.
이 전략은 설계자, ORM 전문가 둘 다 추천하지 않는다. 조인이나 단일 테이블 전략을 고려하자.
2. @MappedSuperclass
지금까지 학습한 상속 관계 매핑은 부모 클래스와 자식 클래스 모두 DB 테이블과 매핑했다. 부모 클래스는 테이블과 매핑하지 않고 자식 클래스에게 매핑 정보만 제공하고 싶으면 @MappedSuperclass를 사용하면 된다. @MappedSuperclass는 추상 클래스와 비슷하지만 @Entity는 실제 테이블과 매핑되지만 @MappedSuperclass 는 실제 테이블과 매핑되지 않는다. 이것은 단순히 매핑 정보를 상속할 목적으로만 사용된다.
- 부모로부터 물려받은 매핑 정보를 재정의
@AttributeOverrides나 @AttributeOverride를 사용
- 연관관계를 재정의
@AssociationOverrides나 @AssociationOverride를 사용
- @MappedSuperclass 특징
- 테이블과 매핑되지 않고 자식 클래스에 엔티티의 매핑 정보를 상속하기 위해 사용한다.
- @MappedSuperclass로 지정한 클래스는 엔티티가 아니므로 em.find()나 JPQL에서 사용할 수 없다.
- 이 클래스를 직접 생성해서 사용할 일은 거의 없으므로 추상 클래스로 만드는 것을 권장
정리하자면 @MappedSuperclass는 테이블과는 관계가 없고 단순히 엔티티가 공통으로 사용하는 매핑 정보를 모아주는 역할을 할 뿐이다.@MappedSuperclass를 사용하면 여러 엔티티에서 공통으로 사용하는 속성을 효과적으로 관리할 수 있다.
3. 복합 키와 식별 관계 매핑
식별 관계 vs 비식별 관계
- 식별 관계
식별 관계는 부모 테이블의 기본 키를 내려받아서 자식 테이블의 기본 키 + 외래 키로 사용하는 관계다.
- 비식별 관계
비식별 관계는 부모 테이블의 기본 키를 받아서 자식 테이블의 외래 키로만 사용하는 관계다.
필수와 선택적 관계의 차이는 외래 키에 NULL을 허용하는지에 따라 구분한다.
- 필수적 비식별 관계 : 외래 키에 NULL을 허용하지 않는다. 연관관계를 필수적으로 맺어야 한다.
- 선택적 비식별 관계 : 외래 키에 NULL을 허용한다. 연관관계를 맺을지 말지 선택할 수 있다.
최근에는 비식별 관계를 주로 사용하며 꼭 필요한 곳에만 식별 관계를 사용하는 추세다.
복합 키 : 비식별 관계 매핑
- 기본 키를 구성하는 컬럼이 하나
- 둘 이상의 컬럼으로 구성된 복합 기본 키
이렇게 사용할 것 같지만 막상 해보면 매핑 오류가 발생한다. JPA에서 식별자를 둘 이상 사용하려면 별도의 식별자 클래스를 만들어야 한다. 그리고 식별자를 구분하기 위해 equals와 hashCode를 사용해서 동승성 비교를 한다. 식별자 필드가 하나일 때는 문제가 없지만, 2개 이상이면 별도의 식별자 클래스를 만들고 그곳에 equals와 hashCode를 구현해야 함. 복합 키를 지원하기 위해 @IdClass와 @EmbeddedId 2가지 방법을 제공한다. @IdClass는 DB와 가까운 방법, @EmbeddedId는 객체지향에 가까운 방법이다.
- @IdClass
복합 키 테이블은 비식별 관계고 PARENT는 복합 기본 키를 사용한다. PARENT 테이블을 보면 기본 키를 PARENT_ID1, PARENT_ID2로 묶은 복합 키로 구성했다. 따라서 복합 키를 매핑하기 위해 식별자 클래스를 별도로 만들어야 한다.
@IdClass를 사용할 때 다음 조건을 만족해야 한다.
- 식별자 클래스의 속성명과 엔티티에서 사용하는 식별자의 속성명이 같아야 한다. 위의 Parent.id1과 ParentId.id1, 그리고 Parent.id2와 ParentId.id2가 같다.
- Serializable 인터페이스를 구현해야 한다.
- equals, hashCode를 구현해야 한다.
- 기본 생성자가 있다.
- 식별자 클래스는 public이어야 한다.
ParentId는 em.persist()를 호출하면 영속성 컨텍스트에 엔티티를 등록하기 직전에 내부에서 Parent.id1, Parent.id2 값을 사용해서 식별자 클래스인 ParentId를 생성하고 영속성 컨텍스트의 키로 사용한다.
ParentId를 사용하여 엔티티를 조회한다.
부모 테이블 기본 키 컬럼이 복합 키이므로 자식 테이블의 외래 키도 복합 키다. 따라서 외래 키 매핑 시 여러 컬럼을 매핑해야 하므로 @JoinColumns 어노테이션을 사용하고 각각의 외래 키 컬럼을 @JoinColumn으로 매핑한다.
참고로 위는 @JoinColumn의 name과 referencedColumnName 속성이 같으므로 referencedColumnName은 생략해도 된다.
- @EmbeddedId
@IdClass가 DB에 맞춘 방법이라면 @EmbeddedId는 좀 더 객체지향적인 방법이다.
@IdClass와는 다르게 @EmbeddedId를 적용한 식별자 클래스는 식별자 클래스에 기본 키를 직접 매핑한다.
@EmbeddedId 만족하기 위한 조건
- @Embeddable 어노테이션을 붙여주어야 한다.
- Serializable 인터페이스를 구현해야 한다.
- equals, hashCode를 구현해야 한다.
- 기본 생성자가 있어야 한다.
- 식별자 클래스는 public이어야 한다..
@EmbeddedId를 사용하는 코드
조회 코드도 식별자 클래스 parentId를 직접 사용한다.
- 복합 키와 equals(), hashCode()
복합 키는 equals()와 hashCode() 필수로 구현해야 한다.
id1과 id2 인스턴스 둘다 같은 값을 갖고 있지만 인스턴스는 다르다. 그래서 위의 출력 값은 equals()를 적절히 오버라이딩 하지 않았으므로 거짓이 나온다. 복합 키는 equals()와 hashCode()를 필수로 구현해야 한다. 식별자 클래스는 보통 equals()와 hashCode()를 구현할 때 모든 필드를 사용한다.
@IdClass vs @EmbeddedId
두개의 어노테이션 둘다 장단점이 있으므로 적절히 활용하면 될 것이다. @EmbeddedId가 더 객체지향 적이고 중복도 없어서 좋아보이긴 하지만 특정 상황에 JQPL이 조금 더 길어질 수 있다.
복합 키 : 식별 관계 매핑
식별 관계에서 자식 테이블은 부모 테이블의 기본 키를 포함해서 복합 키를 구성해야 하므로 @IdClass나 @EmbeddedId를 사용해서 식별자를 매핑해야 한다.
- @IdClass와 식별 관계
- @EmbeddedId 와 식별 관계
@IdClass와 다른 점은 @Id 대신에 @MapId를 사용하는 점이다. @MapId는 외래 키와 매핑한 연관관계를 기본 키에도 매핑하겠다는 뜻이다. 여기서는 ChildId의 parentId 필드를 선택했다.
비식별 관계로 구현
식별 관계의 복합 키를 사용한 코드와 비교하면 매핑도 쉽고 코드도 단순하게 구현할 수 있다.
일대일 식별 관계
BoardDetail처럼 식별자가 단순히 컬럼 하나면 @MapId를 사용하고 속성 값을 비워두면 된다. 이때 @MapId는 @Id로 사용해서 식별자로 지정한 BoardDetail.boardId와 매핑된다.
식별, 비식별 관계의 장단점
DB 설계 관점에서 보면 다음과 같은 이유로 비식별 관계를 더 선호한다.
- 식별 관계는 부모 테이블의 기본 키를 자식 테이블로 전파하면서 자식 테이블의 기본 키 컬럼이 점점 늘어난다. 그렇게 된다면 결국 조인할 때 SQL이 복잡해지고 기본 키 인덱스가 불필요하게 커질 수 있다.
- 식별 관계는 2개 이상의 컬럼을 합해서 복합 키를 만들어야 하는 경우가 많다.
- 식별 관계를 사용할 때 기본 키로 비즈니스 의미가 있는 자연 키컬럼을 조합하는 경우가 많다.
- 식별 관계는 부모 테이블의 기본 키를 자식 테이블의 기본 키로 사용하므로 비식별 관계보다 테이블 구조가 유연하지 못하다.
- 비식별 관계를 선호하는 이유
- 일대일 관계를 제외하고 식별 관계는 2개 이상의 컬럼을 묶은 복합 기본키를 사용한다.
- 비식별 관계의 기본 키는 주로 대리 키를 사용하는데 JPA @GenerateValue처럼 대리 키를 생성하기 위한 편리한 방법을 제공한다.
- 식별 관계가 가지는 장점
- 기본 키 인덱스를 활용하기 좋다.
- 상위 테이블들의 기본 키 컬럼을 자식, 손자 테이블들이 가지고 있으므로 특정 상황에 조인 없이 하위 테이블만으로 검색을 완료할 수 있다.
ex) 예시
두 경우 모두 CHILD 테이블의 기본 키 인덱스를 PARENT_ID + CHILD_ID로 구성하면 별도의 인덱스를 생성할 필요 없이 기본 키 인덱스만 사용해도 된다.
ORM 신규 프로젝트 진행시 추천하는 방법은 될 수 있으면 비식별 관계를 사용하고 기본 키는 Long 타입의 대리 키를 사용하는 것이다.
4. 조인 테이블
연관관계 설정 방법
- 조인 컬럼 사용(외래 키)
- 조인 테이블 사용(테이블 사용)
- 조인 컬럼 사용
위처럼 외래 키에 Null을 허용하는 관계를 비식별 관계라 한다. 선택적 비식별 관계는 조인할 때 외부 조인(OUTER JOIN)을 사용해야 한다. 실수로 내부 조인을 사용하면 사물함과 관계가 없는 회원은 조회되지 않는다.그리고 회원과 사물함이 아주 가끔 관계를 맺는다면 외래 키 대부분이 null로 저장되는 단점이 있다.
- 조인 테이블 사용
조인 테이블은 연관관계를 관리하는 조인 테이블을 추가하고 여기서 두 테이블의 외래 키를 가지고 연관관계를 관리한다. 따라서 MEMBER , LOCKER는 연관관계를 관리하기 위한 외래 키 컬럼이 없다.
조인 테이블의 가장 큰 단점은 테이블을 하나 추가해야 한다는 점이다. 따라서 관리해야 하는 테이블이 늘어나고 회원과 사물함 두 테이블을 조인하려면 MEMBER_LOCKER 테이블까지 추가로 조인해야 한다.
결론적으로 기본은 조인 컬럼을 사용하고 필요하다고 판단되면 조인 테이블을 사용하면된다.
일대일 조인 테이블
*기본 키는 유니크 제약조건이 걸려있다.
- @JoinTable 속성
- name : 매핑할 조인 테이블 이름
- JoinColumns : 현재 엔티티를 참조하는 외래 키
- inverseJoinColumns : 반대방향 엔티티를 참조하는 외래 키
양방향으로 매핑하려면 이 코드를 추가하면 된다.
일대다 조인 테이블
다대다 조인 테이블
5. 엔티티 하나에 여러 테이블 매핑
잘 사용하지는 않지만 @SecondaryTable을 사용하여 한 엔티티에 여러 테이블을 매핑할 수 있다.
- @SecondaryTable 속성
- @SecondaryTable.name : 매핑할 다른 테이블의 이름 위에서는 테이블명을 BOARD_DETAIL로 지정
- @SecondaryTable.pkJoinColumns : 매핑할 다른 테이블의 기본 키 컬럼 속성, 위에서는 BOARD_DETAIL_ID로 지정
title필드처럼 테이블을 지정하지 않으면 기본 테이블인 BOARD에 매핑된다. 더 많은 테이블을 매핑하려면 @SecondaryTables를 사용하면 된다.
하지만 두 테이블을 하나의 엔티티에 매핑하는 방법은 추천하지 않는다.
'Spring > JPA' 카테고리의 다른 글
값 타입 (0) | 2023.07.16 |
---|---|
프록시와 연관관계 관리 (0) | 2023.07.15 |
다양한 연관관계 매핑 (0) | 2023.07.13 |
연관관계매핑 기초 (0) | 2023.07.13 |
엔티티 매핑 (0) | 2023.07.11 |