7.3 복합 키와 식별 관계 매핑

7.3.1 식별 관계 vs 비식별 관계

식별관계

  • 부모 테이블의 기본 키를 내려받아서 자식 테이블의 기본 키 + 외래 키로 사용한다.

비식별 관계

  • 부모 테이블의 기본 키를 받아서 자식 테이블의 외래 키로만 사용하는 관계다.
  • 필수적 비식별 관계(Mandatory)
    • 외래 키에 NULL을 허용하지 않는다.
    • 반드시 연관관계를 맺어야 한다.
  • 선택적 비식별 관계(Optional)
    • 외래 키에 NULL을 허용한다.
    • 연관관계를 맺을지 말지 선택할 수 있다.
  • 최근 트렌드는 비식별 관계를 주로 사용하고 꼭 필요한 곳에만 식별 관계를 사용하는 추세다.

7.3.2 복합 키: 비식별 관계 매핑

  • 둘 이상의 컬럼으로 구성된 복합 키는 별도의 식별 클래스를 정의해야한다.
  • 식별자를 구분하기 위해서 equalshashcode 메소드를 통해서 동등성을 비교한다.
  • 식별자 필드가 1개일때는 자바에서 제공하는 타입을 사용하기 때문에 직접 equalshashcode 를 사용할 필요가 없으나, 직접 식별 클래스를 정의할때는 equalshashcode 를 직접 작성 해줘야한다.
  • JPA는 복합키를 위해 @EmbeddedId@IdClass 를 제공한다.
  • @IdClass 는 데이터베이스에 맞춘 방법이고, @EmbeddedId 는 좀 더 객체지향적인 방법이다.

@IdClass

  • 아래와 같이 복합키 클래스를 작성한다.
  • Serializable 인터페이스를 구현해야한다.
  • equalshashCode 를 작성해야한다.
  • 기본 생성자가 있어야한다.
  • 식별자 클래스는 public이어야 한다.

Untitled

  • 아래와 같이 ParentId 클래스를 이용하는 Entity를 생성한다.
  • 주의할 점은 Parent 클래스의 식별자 필드명이 ParentId 의 필드명과 같아야 한다.

Untitled

  • 간단한 예제를 살펴보자.
  • 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());

Untitled

  • Child Entity를 정의해보자.
  • Parent와 연관관계를 맺으려고 하는데 Parent 클래스의 PK가 2개인 경우 아래와 같이 @JoinColumns 어노테이션을 사용하면 된다.

Untitled

@EmbeddedId

  • ParentId 클래스를 아래와 같이 작성한다.
  • @IdClass 방식과 달리 ParentId에 직접 기본키 컬럼명을 작성한다.
  • 마찬가지로 몇가지 지켜야할 사항이 있는데 아래와 같다.
  • Serializable 인터페이스를 구현해야한다.
  • equalshashCode 를 구현해야한다.
  • 기본 생성자가 있어야한다.
  • 식별자 클래스는 public 이어야한다.

Untitled

  • Parent 엔티티가 덕분에 심플해진다.

Untitled

  • 사용 예제를 살펴보자.
  • 이전과 달리 직접 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());

Untitled

복합 키와 equals(), hashCode()

  • 복합키를 사용하는 경우 직접 equalshashCode 메소드를 구현해야한다.
  • 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());

Untitled

Untitled

@IdClass vs @EmbeddedId

  • 두개의 방식이 각각 장단점이 있기때문에 본인의 취향에 맞는 것을 일관성 있게 사용하면된다.

7.3.3 복합 키: 식별 관계 매핑

@IdClass 방식

  • 아래 그림은 부모에서부터 자식,손자까지 기본 키를 전달하는 식별관계를 나타낸다.
  • 아래 관계를 @IdClass@EmbeddedId 를 사용해서 작성해본다.

Untitled

  • 먼저 PARENT 테이블에 대응되는 Parent Entity를 정의한다.

Untitled

  • 다음 CHLID 테이블에 대응되는 Child Entity를 정의한다.
  • CHILD 테이블은 복합키를 가지며, 그중 PARENT_ID는 PARENT 테이블에서 가져온다.
  • JPA에선 아래와 같이 작성한다.

Untitled

  • Child Entity가 복합키를 갖도록 ChildId 클래스를 정의한다.
  • 필드명은 Child Entity의 필드명을 그대로 작성해준다.
  • 데이터 타입은 실제 Id의 타입을 따르도록 작성한다. ( Parent Entity의 키 타입을 따르도록한다.)

Untitled

  • GRANDCHILD 테이블에 대응되는 GrandChild Entity를 작성한다.
  • 물리적으로 총 3개의 키를 가지는 복합키이다.
  • 복합키와 연관관계를 맺을 수 있도록 아래와 같이 작성한다.

Untitled

  • 복합키를 사용할 수 있도록 GrandChildId 클래스를 정의한다.
  • 마찬가지로 데이터 타입은 Child Entity의 데이터 타입을 따르도록 작성한다.
  • 필드명은 GrandChild Entity에 정의되어 있는 키 필드와 동일하게 작성한다.

Untitled

@EmbeddedId 방식

Untitled

  • @EmbeddedId 방식을 사용해서 식별 관계 매핑을 구성할때는 @MapsId 를 사용해야 한다.
  • Parent Entity를 작성한다.

Untitled

  • Chlid Entity를 작성한다.
  • @MapsId 어노테이션을 통해 ChildId 클래스에 존재하는 parentId와 매핑을 수행한다.

Untitled

  • ChildId 클래스를 작성한다.
  • @MapsId("parentId") 어노테이션과 매핑 될수 있도록 필드명을 parentId로 작성한다.

Untitled

  • GrandChild Entity를 작성한다.

Untitled

  • GrandChildId 클래스를 작성한다.
  • @MapsId("childId") 와 매핑하기 위해 필드명을 childId로 해야한다.

Untitled

7.3.4 비식별 관계로 구현

  • 7.3.3에서 사용한 식별 관계를 비식별 관계로 변형해서 구현해본다.

Untitled

  • Parent Entity를 작성한다.

Untitled

  • Child Entity를 작성한다.

Untitled

  • GrandChild Entity를 작성한다.

Untitled

  • 식별 관계의 복합키를 가지는 경우에 비해 코드가 단순해졌다.

7.3.5 일대일 식별 관계

  • 일대일 식별 관계를 가지는 케이스를 살펴보자.

Untitled

  • 먼저 Board 테이블에 대응되는 Board Entity를 만든다.
  • BoardDetail Entity를 가져올 수 있도록 연관관계를 매핑한다.

Untitled

  • BoardDetail 테이블에 대응되는 BoardDetail Entity를 만든다.
  • 특이한 점이 있는데 Board Entity의 Key를 그대로 사용하는것이기 때문에 boardId 필드에 @GeneratedValue 어노테이션이 없다.
  • BoardDetail에는 식별자가 단 1개만 존재하기 때문에 @MapsId 에 별도의 파라미터를 쓰지 않아도 된다.
  • 이렇게 작성하면 유일한 key 필드인 boardId에 자동으로 매핑되어 정보를 가져올 수 있게된다.

Untitled

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인 자식을 모두 조회하는 경우 자식 테이블 만으로 검색이 가능하다.