복합 키와 식별 관계 매핑

2023. 6. 8. 17:41JPA

반응형

식별 관계 vs 비식별관계

식별관계

: 부모 테이블의 기본 키를 내려받아서 자식 테이블의 기본 키 + 외래 키로 사용하는 관계

비식별관계

: 부모 테이블의 기본 키를 받아서 자식 테이블의 외래 키로만 사용하는 관계

비식별관계는 외래 키에 NULL을 허용하는지에 따라 필수적 비식별 관계와 선택적 비식별 관계로 나눈다.

  • 필수적 비식별 관계(Mandatory): 외래 키에 NULL을 허용하지 않는다. 연관관계를 필수적으로 맺어야 한다.
  • 선택적 비식별 관계(Optional): 외래 키에 NULL을 허용한다. 연관관계를 맺을지 말지 선택할 수 있다.

 

복합 키: 비식별 관계 매핑

기본 키를 구성하는 컬럼이 하나면 다음처럼 단순하게 매핑한다.

@Entity
public class Hello() {
    @Id
    private String id;
}

 

둘 이상의 컬럼으로 구성된 복합 기본 키는 별도의 식별자 클래스를 만들어야 한다. 

JPA는 복합 키를 지원하기 위해 @IdClass와 @EmbeddedId 2가지 방법을 제공하는데 @IdClass는 관계형 데이터베이스에 가까운 방법이고 @EmbeddedId는 좀 더 객체지향에 가까운 방법이다.

 

@IdClass

//부모 클래스
@Entity
@IdClass(ParentId.class)
public class Parent {

    @Id
    @Column(name = "PARENT_ID1")
    private String id;
    
    @Id
    @Column(name = "PARENT_ID2")
    private String id2;
    
    private String name;
    ...
}

//식별자 클래스
public class ParentId implements Seriallizable {

    private String id1;
    private String id2;
    
    public ParentId() {
    }
    
    public ParentId(String id1, String id2) {
    this.id1 = id1;
    this.id2 = id2;
    }
    
    @Override
    public boolean equals(Object o) { ... }
    
    @Override
    public int hashCode() { ... }
}
  • 식별자 클래스의 속성명과 엔티티에서 사용하는 식별자의 속성명이 같아야한다.
  • Serializable 인터페이스를 구현해야 한다.
  • 기본 생성자가 있어야 한다.
  • 식별자 클래스는 public 이어야 한다.

 

//자식 클래스
public class Child {
	
    @Id
    private String id;
    
    @ManyToOne
    @JoinColumns({
    	@JoinColumn(name = "PARENT_ID1", referencedColumnName = "PARENT_ID1"),
        @JoinColumn(name = "PARENT_ID2", referencedColumnName = "PARENT_ID2")
    })
    private Parent parent;

부모 테이블의 기본 키 컬럼이 복합 키이므로 자식 테이블의 외래 키도 복합 키다. 따라서 외래 키 매핑 시 여러 컬럼을 매핑해야 하므로 @JoinColumns 어노테이션을 사용하고 각각의 외래 키 컬럼을 @JoinColumn으로 매핑한다.

 

@EmbeddedId

좀 더 객체지향적인 방법이다.

@Entity
public class Parent {
	
    @EmbeddedId
    private ParentId id;
    
    private String name;
    ...
}

Parent 엔티티에서 식별자 클래스를 직접 사용하고 @EmbeddedId 어노테이션을 적어주면 된다.

@Embeddable
public class ParentId implements Serializable {

    @Column(name = "PARENT_ID1")
    private String id1;
    
    @Column(name = "PARENT_ID2")
    private String id2;
    
    //equals and hashCode 구현
    ...
}

@IdClass와는 다르게 @EmbeddedId를 적용한 식별자 클래스는 식별자 클래스에 기본 키를 직접 매핑한다.

  • @Embeddable 어노테이션을 붙여주어야 한다.
  • Serializable 인터페이스를 구현해야 한다.
  • equals, hashCode를 구현해야 한다.
  • 기본 생성자가 있어야 한다.
  • 식별자 클래스는 public이어야 한다.

 

복합 키: 식별 관계 매핑

식별 관계 구현

위 그림을 보면 부모, 자식, 손자까지 계속 기본 키를 전달하는 식별 관계다. 식별 관계에서 자식 테이블은 부모 테이블의 기본 키를 포함해서 복합 키를 구성해야 하므로 @IdClass나 @EmbeddedId를 사용해서 식별자를 매핑해야 한다.

@IdClass와 식별 관계

//부모
@Entity
public class Parent {
	
    @Id @Column(name = "PARENT_ID")
    private String id;
    private String name;
    ...
}

//자식
@Entity
@IdClass(ChildId.class)
public class Child() {

    @Id
    @ManyToOne
    @JoinColumn(name = "PARENT_ID")
    public Parent parent;
    
    @Id @Column(name = "CHILD_ID")
    private String childId;
    
    private String name;
    ...
}

//자식 IdClass
public class ChildId implements Serializable {

    private Stirng parent;
    private String childId;
    
    //equals, hashCode
    ...
}

//손자
@Entity
@IdClass(GrandChildId.class)
public class GrandChild {

    @Id
    @ManyToOne
    @JoinColumns({
    	@JoinColumn(name = "PARENT_ID"),
        @JoinColumn(name = "CHILD_ID")
    })
    private Child child;
    
    @Id @Column(name = "GRANDCHILD_ID")
    private String id;
    
    private String name;
    ...
}

//손자 IdClass
public class GrandChildId implements Serializable {
	
    private ChildId child;
    private String id;
    
    //equals, hashCode
    ...
}

식별 관계는 기본 키와 외래 키를 같이 매핑해야 한다. 따라서 식별자 매핑인 @Id와 연관관계 매핑인 @ManyToOne을 같이 사용하면 된다.

@EmbeddedId와 식별 관계

@EmbeddedId로 식별 관계를 구성할 때는 @MapsId를 사용해야 한다.

//부모
@Entity
public class Parent {

    @Id @Column(name = "PARENT_ID")
    private String id;
    
    private String name;
    ...
}

//자식
@Entity
public class Child {

    @EmbeddedId
    private ChildId id;
    
    @MapsId("parentId") //ChildId.parentId랑 매핑
    @ManyToOne
    @JoinColumn(name = "PARENT_ID")
    public Parent parent;
    
    private String name;
    ...
}

//자식 ID
@Embaddable
public class ChildId implements Serializable {

    private String parentId;
    
    @Column(name = "CHILD_ID")
    private String id;
    
    //equals, hashCode
    ...
}

//손자
@Entity
public class GrandChild {

    @EmbeddedId
    private GrandChildId id;
    
    @MapsId("childId") //GrandChildId.childId 매핑
    @ManyToOne
    @JoinColumns({
    	@JoinColumn(name = "PARENT_ID"),
        @JoinColumn(name = "CHILD_ID")
    })
    private Child child;
    
    private String name;
    ...
}

//손자 ID
@Embeddable
public class GrandChildId implements Serializable {

    private ChildId childId
    
    @Column(name = "GRANDCHILD_ID")
    private String id;
    
    //equals, hashCode
    ...
}

@MapsId는 외래키와 매핑한 연관관계를 기본 키에도 매핑하겠다는 뜻이다. 

 

★ 식별, 비식별 관계의 장단점

데이터베이스 설계 관점에서 보면 비식별 관계를 선호한다.

  • 식별 관계는 부모 테이블의 기본 키를 자식 테이블로 전파하면서 자식 테이블의 기본 키 컬럼이 점점 늘어난다. 예를 들어 부모 테이블은 기본 키 컬럼이 하나였지만 자식 테이블은 기본 키 컬럼이 2개, 손자 테이블은 기본 키 컬럼이 3개로 점점 늘어난다. 결국 조인할 때 SQL이 복잡해지고 기본 키 인덱스가 불필요하게 커질 수 있다.
  • 식별 관계는 2개 이상의 컬럼을 합해서 복합 기본 키를 만들어야 하는 경우가 많다.
  • 식별 관계를 사용할 때 기본 키로 비즈니스 의미가 있는 자연 키 컬럼을 조합하는 경우가 많다. 반면에 비식별 관계의 기본 키는 비즈니스와 전혀 관계 없는 대리 키를 주로 사용한다. 비즈니스 요구사항은 시간이 지남에 따라 언젠가는 변한다. 식별 관계의 자연 키 컬럼들이 자식에 손자까지 전파되면 변경하기 힘들다.
  • 식별 관계는 부모 테이블의 기본 키를 자식 테이블의 기본 키로 사용하므로 비식별 관계보다 테이블 구조가 유연하지 못하다.
  • 일대일 관계를 제외하고 식별 관계는 2개 이상의 컬럼을 묶은 복합 기본 키를 사용한다. JPA에서 복합 키는 별도의 복합 키 클래스를 만들어서 사용해야 한다. 따라서 컬럼이 하나인 기본 키를 매핑하는 것보다 많은 노력이 필요하다.
  • 비식별 관계의 기본 키는 주로 대리 키를 사용하는데 JPA는 @GeneratedValue처럼 대리 키를 생성하기 위한 편리한 방법을 제공한다.

물론 식별 관계가 가지는 장점도 있다. 기본 키 인덱스를 활용하기 좋고, 상위 테이블들의 기본 키 컬럼을 자식, 손자 테이블들이 가지고 있으므로 특정 상황에 조인없이 하위 테이블만드로 검색을 완료할 수 있다.

기본 키 인덱스를 활용하는 예이다.

  • 부모 아이디가 A인 모든 자식 조회
SELECT * FROM CHILD 
WHERE PARENT_ID = 'A'
  • 부모 아이디가 A고 자식 아이디가 B인 자식 조회
SELECT * FROM CHILD
WHERE PARENT_ID = 'A' AND CHILD_ID = 'B'

 

ORM 프로젝트 진행 시 추천하는 방법은 될 수 있으면 비식별 관계를 사용하고 기본 키는 Long타입의 대리 키를 사용하는 것이다.

반응형

'JPA' 카테고리의 다른 글

@SecondaryTable : 엔티티 하나에 여러 테이블 매핑  (0) 2023.06.13
조인 테이블  (2) 2023.06.13
@MappedSuperclass  (0) 2023.05.11
상속 관계 매핑  (2) 2023.05.11
다양한 연관관계 매핑 (2)  (0) 2023.04.27