7.3 복합 키와 식별 관계 매핑
7.3.1 식별 관계 vs 비식별 관계
식별관계
- 부모 테이블의 기본 키를 내려받아서 자식 테이블의 기본 키 + 외래 키로 사용한다.
비식별 관계
- 부모 테이블의 기본 키를 받아서 자식 테이블의 외래 키로만 사용하는 관계다.
- 필수적 비식별 관계(Mandatory)
- 외래 키에 NULL을 허용하지 않는다.
- 반드시 연관관계를 맺어야 한다.
- 선택적 비식별 관계(Optional)
- 외래 키에 NULL을 허용한다.
- 연관관계를 맺을지 말지 선택할 수 있다.
- 최근 트렌드는 비식별 관계를 주로 사용하고 꼭 필요한 곳에만 식별 관계를 사용하는 추세다.
7.3.2 복합 키: 비식별 관계 매핑
- 둘 이상의 컬럼으로 구성된 복합 키는 별도의 식별 클래스를 정의해야한다.
- 식별자를 구분하기 위해서
equals
와hashcode
메소드를 통해서 동등성을 비교한다. - 식별자 필드가 1개일때는 자바에서 제공하는 타입을 사용하기 때문에 직접
equals
와hashcode
를 사용할 필요가 없으나, 직접 식별 클래스를 정의할때는equals
와hashcode
를 직접 작성 해줘야한다. - JPA는 복합키를 위해
@EmbeddedId
와@IdClass
를 제공한다. @IdClass
는 데이터베이스에 맞춘 방법이고,@EmbeddedId
는 좀 더 객체지향적인 방법이다.
@IdClass
- 아래와 같이 복합키 클래스를 작성한다.
Serializable
인터페이스를 구현해야한다.equals
와hashCode
를 작성해야한다.- 기본 생성자가 있어야한다.
- 식별자 클래스는 public이어야 한다.
- 아래와 같이
ParentId
클래스를 이용하는 Entity를 생성한다. - 주의할 점은
Parent
클래스의 식별자 필드명이ParentId
의 필드명과 같아야 한다.
- 간단한 예제를 살펴보자.
- Parent 엔티티를 저장할때 ParentId를 전혀 사용하지 않는 모습을 확인할 수 있다.
- Parent 엔티티를
persist
하게되면 내부적으로 ParentId를 만들어서 영속성 컨텍스트의 엔티티 키로 사용한다.
1Parent parent = new Parent();
2parent.setId1("myId1");
3parent.setId2("myId2");
4parent.setName("soojong");
5em.persist(parent);
6
7ParentId parentId = new ParentId("myId1", "myId2");
8Parent findParent = em.find(Parent.class, parentId);
9
10System.out.println("findParent.getId1() = " + findParent.getId1());
11System.out.println("findParent.getId2() = " + findParent.getId2());
12System.out.println("findParent.getName() = " + findParent.getName());
- Child Entity를 정의해보자.
- Parent와 연관관계를 맺으려고 하는데 Parent 클래스의 PK가 2개인 경우 아래와 같이
@JoinColumns
어노테이션을 사용하면 된다.
@EmbeddedId
ParentId
클래스를 아래와 같이 작성한다.@IdClass
방식과 달리 ParentId에 직접 기본키 컬럼명을 작성한다.- 마찬가지로 몇가지 지켜야할 사항이 있는데 아래와 같다.
- Serializable 인터페이스를 구현해야한다.
equals
와hashCode
를 구현해야한다.- 기본 생성자가 있어야한다.
- 식별자 클래스는 public 이어야한다.
Parent
엔티티가 덕분에 심플해진다.
- 사용 예제를 살펴보자.
- 이전과 달리 직접
ParentId
를 정의하고 세팅해줘야한다.
1Parent parent = new Parent();
2ParentId parentId = new ParentId("myId1", "myId2");
3parent.setId(parentId); // 직접 세팅
4parent.setName("parentName");
5em.persist(parent);
6
7ParentId findParentId = new ParentId("myId1", "myId2");
8Parent findParent = em.find(Parent.class, findParentId);
9
10System.out.println("findParent.getName() = " + findParent.getName());
복합 키와 equals(), hashCode()
- 복합키를 사용하는 경우 직접
equals
와hashCode
메소드를 구현해야한다. - Object가 제공하는
equals
의 내부 동작 방식은 참조값 비교인==
(동일성) 비교 이다. - 영속성 컨텍스트에 있는 엔티티의 키를 찾을때는
equals
를 수행하는데 복합키 클래스를 참조 비교하지 않도록 재작성 해야한다. (동등성 비교를 수행하도록 수정 필요) - 따라서 복합키 클래스는
equals
메소드를 적절히 오버라이딩해서 작성해야한다.
1ParentId id1 = new ParentId("myId1","myId2");
2ParentId id2 = new ParentId("myId1","myId2");
3
4System.out.println("id1.equals(id2) = " + id1.equals(id2));
5System.out.println("id1.hashCode() = " + id1.hashCode());
6System.out.println("id2.hashCode() = " + id2.hashCode());
@IdClass vs @EmbeddedId
- 두개의 방식이 각각 장단점이 있기때문에 본인의 취향에 맞는 것을 일관성 있게 사용하면된다.
7.3.3 복합 키: 식별 관계 매핑
@IdClass 방식
- 아래 그림은 부모에서부터 자식,손자까지 기본 키를 전달하는 식별관계를 나타낸다.
- 아래 관계를
@IdClass
와@EmbeddedId
를 사용해서 작성해본다.
- 먼저 PARENT 테이블에 대응되는 Parent Entity를 정의한다.
- 다음 CHLID 테이블에 대응되는 Child Entity를 정의한다.
- CHILD 테이블은 복합키를 가지며, 그중 PARENT_ID는 PARENT 테이블에서 가져온다.
- JPA에선 아래와 같이 작성한다.
- Child Entity가 복합키를 갖도록 ChildId 클래스를 정의한다.
- 필드명은 Child Entity의 필드명을 그대로 작성해준다.
- 데이터 타입은 실제 Id의 타입을 따르도록 작성한다. ( Parent Entity의 키 타입을 따르도록한다.)
- GRANDCHILD 테이블에 대응되는 GrandChild Entity를 작성한다.
- 물리적으로 총 3개의 키를 가지는 복합키이다.
- 복합키와 연관관계를 맺을 수 있도록 아래와 같이 작성한다.
- 복합키를 사용할 수 있도록 GrandChildId 클래스를 정의한다.
- 마찬가지로 데이터 타입은 Child Entity의 데이터 타입을 따르도록 작성한다.
- 필드명은 GrandChild Entity에 정의되어 있는 키 필드와 동일하게 작성한다.
@EmbeddedId 방식
@EmbeddedId
방식을 사용해서 식별 관계 매핑을 구성할때는@MapsId
를 사용해야 한다.- Parent Entity를 작성한다.
- Chlid Entity를 작성한다.
@MapsId
어노테이션을 통해 ChildId 클래스에 존재하는 parentId와 매핑을 수행한다.
- ChildId 클래스를 작성한다.
@MapsId("parentId")
어노테이션과 매핑 될수 있도록 필드명을 parentId로 작성한다.
- GrandChild Entity를 작성한다.
- GrandChildId 클래스를 작성한다.
@MapsId("childId")
와 매핑하기 위해 필드명을 childId로 해야한다.
7.3.4 비식별 관계로 구현
- 7.3.3에서 사용한 식별 관계를 비식별 관계로 변형해서 구현해본다.
- Parent Entity를 작성한다.
- Child Entity를 작성한다.
- GrandChild Entity를 작성한다.
- 식별 관계의 복합키를 가지는 경우에 비해 코드가 단순해졌다.
7.3.5 일대일 식별 관계
- 일대일 식별 관계를 가지는 케이스를 살펴보자.
- 먼저 Board 테이블에 대응되는 Board Entity를 만든다.
- BoardDetail Entity를 가져올 수 있도록 연관관계를 매핑한다.
- BoardDetail 테이블에 대응되는 BoardDetail Entity를 만든다.
- 특이한 점이 있는데 Board Entity의 Key를 그대로 사용하는것이기 때문에 boardId 필드에
@GeneratedValue
어노테이션이 없다. - BoardDetail에는 식별자가 단 1개만 존재하기 때문에
@MapsId
에 별도의 파라미터를 쓰지 않아도 된다. - 이렇게 작성하면 유일한 key 필드인 boardId에 자동으로 매핑되어 정보를 가져올 수 있게된다.
7.3.6 식별, 비식별 관계의 장단점
- 식별 관계보다는 비식별 관계를 선호한다.
- 아주 특별한 경우가 아니라면 비식별 관계를 사용하고, 기본 키는 Long 타입의 대리 키를 사용하는 것이 좋다.
- Long 타입을 추천하는 이유는 자바에서 Integer는 20억 정도면 끝나버리지만, Long 타입은 290경까지 표기가 가능하다.
- 선택적 비식별 관계보단 필수적 비식별 관계를 사용하는 것이 좋다.
- 선택적인 비식별 관계는 NULL을 허용하기 때문에 Join시에 외부(Outter) Join을 사용해야한다.
- 반면에 필수적 비식별 관계는 NOT NULL이기 때문에 항상 관계가 있다는 것을 보장하므로 내부(Inner) Join만 사용해도 된다.
데이터베이스 설계 관점 - 비식별 선호
- 부모 테이블의 키를 자식 테이블로 전파하면서 자식 테이블의 키가 늘어나는 현상이 있다.
- 예시) 부모는 키 1개, 자식은 키 2개, 손자는 키 3개
- 이렇게 되면 Join시에 SQL이 복잡해지고, 인덱스의 사이즈가 커진다.
- 복합키를 구성할때 주로 비즈니스와 관련된 컬럼을 사용하는데 비즈니스 변경시에 테이블 작업량이 커진다.
- 키를 다른 테이블에서 참조하고 있기때문에 테이블 구조가 유연하지 못하게된다.
객체 관계 매핑 관점 - 비식별 선호
- 복합키를 사용하면 별도의 복합키 클래스를 만들어야 하기 때문에 작업량이 늘어난다.
- 비식별 관계의 기본 키는 주로 대리키를 사용하며 이와 관련된
@GeneratedValue
를 JPA에서 기본적으로 제공한다. (편리성 증가)
식별 관계가 가지는 미세한(?) 장점
- 식별 관계를 사용하면 키본 키 인덱스를 활용하기 좋다.
- 상우 테이블의 키를 자식이 가지고 있으므로, 특정 상황에서 Join을 사용하지 않고 하위테이블 만으로 검색이 가능하다.
- 예시) 부모의 ID가 A인 자식을 모두 조회하는 경우 자식 테이블 만으로 검색이 가능하다.