TIL

JPA - 복합키와 식별 관계 매핑

haseung22 2024. 11. 12. 21:48

JPA 심화 강의를 들으면서 코드를 보다가 엔티티 클래스에 처음보는

@EmbeddedId와 @MapsId 어노테이션이 있길래 뭔지 궁금해서 찾아봤는데

@EmbeddedId는 식별자 클래스를 생성할 때 사용하는 어노테이션이며, 

@EmbeddedId를 식별 관계로 매핑할 때 @MapsId를 사용하면 된다고한다고 하는데

이렇게만 봐서는 잘 모르겠어서  복합키와 식별 관계에 대해 알아보며,  정리한 글입니다.


식별, 비식별 관계

DB에서는 FK가 기본 키에 포함되는지 여부에 따라 식별, 비식별 관계로 구분된다.

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

  • 비식별 관계 : 부모 테이블의 기본키를 받아서 자식 테이블의 외래 키로만 사용하는 관계
    • 외래키null을 허용하는지에 따라 필수적 비식별 관계선택적 비식별 관계로 나뉜다.

  • 필수적 비식별 관계는 외래키에 Null을 허용하지 않으며, 연관관계를 필수적으로 맺어야 한다.
  • 선택적 비식별 관계는 외래키에 Null을 허용하며, 연관관계를 맺을지 말지 선택할 수 있다.

DB 테이블을 설계할 때 두 관계 중 하나를 선택해야 하지만 주로 비식별 관계를 사용하고

필요한 경우에만 식별 관계를 사용합니다.

 

복합 키를 표현할 때는 식별자 클래스를 만들어야 사용할 수 있으며,

JPA에서는 복합키를 지원하기 위해서 @IdClass & @EmbeddedId를 제공합니다.

 

비식별 관계 매핑

@IdClass

@IdClass를 사용할 때 식별자 클래스는 다음 조건을 만족해야합니다.

  • 식별자 클래스의 속성명과 복합키를 가지는 엔티티에서 사용하는 식별자의 속성명이 같아야한다.
  • Serializable 인터페이스를 구현해야한다.
  • equals(), hashCode()를 구현해야한다.
  • 기본 생성자가 필요하다.
  • 식별자 클래스는 public이어야 한다.
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@EqualsAndHashCode
@Getter
// 식별자 클래스
public class ParentId implements Serializable { 

    private String parentId1;
    private String parentId2;

    @Builder
    public ParentId(String parentId1, String parentId2){
    	this.parentId1 = parentId1;
        this.parentId2 = parentId2;
    }
}

 

@Getter
@Entity
// IdClass를 이용하여 식별자 클래스를 활용
@IdClass(ParentId.class)
public class Parent {

    @Id
    private String parentId1;

    @Id
    private String parentId2;
}

 

@Entity
public class child {
    @Id
    private String childId;

    @ManyToOne(fetch = FetchType.LAZY)
    // 복합키 매핑을 위한 JoinColumns를 이용하여 각각의 Key를 매핑
    // @JoinColumn의 순서가 바뀌어 저장될 가능성이 있으므로
    // referencedColumnName 속성으로 이름을 지정하는 것을 권장한다.
    @JoinColumns({
        @JoinColumn(name = "parent_id1", referencedColumnName = "parent_id1"),
        @JoinColumn(name = "parent_id2", referencedColumnName = "parent_id2")
    })
    private Parent parent;
}

 

@EmbeddedId

@IdClass와는 다르게 식별자 클래스에 @Embeddable 어노테이션을 달아주어야 하며,@EmbeddedId를 적용한 식별자 클래스는 식별자 클래스에 기본 키를 직접 매핑합니다.이 외 조건은 @IdClass의 첫번째 조건을 제외한 나머지와 동일한 조건입니다.

@Getter
@Entity
public class Parent {

    @EmbeddedId
    // ParentId의 필드인 parentId1과 parentId2가 pk로 설정된다.
    private ParentId id;
}

 

@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Embeddable
@EqualsAndHashCode
@Getter
public class ParentId implements Serializable {

    private String parentId1;
    private String parentId2;

    @Builder
    public ParentId(String parentId1, String parentId2){
    	this.parentId1 = parentId1;
        this.parentId2 = parentId2;
    }
}

 

식별 관계 매핑

비식별 관계와 마찬가지로 매핑 방법으로는 @IdClass & @EmbeddedId 두 가지 방식이 있습니다.

 

@IdClass

@Getter
@Entity
public class Parent {

    @Id
    private String parentId;
}

 

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@EqualsAndHashCode
public class ChildId implements Serializable {
    private Parent parent; // Child.parent 매핑
    private String childId; //Child.childId 매핑

    @Builder
    public ChildId(Parent parent, String childId){
        this.parent = parent;
        this.childId = childId
    }
}

 

@Entity
@IdClass(ChildId.class)
public class child {
    @Id
    private String childId;

    @Id // 식별 관계는 기본 키와 외래 키를 같이 매핑해야한다.
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "parent_id")
    private Parent parent;
}

 

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@EqualsAndHashCode
public class GrandChildId implements Serializable {
    private String grandChildId; // GrandChild.grandChildId 매핑
    private Child child; // GrandChild.child 매핑

    @Builder
    public GrandChildId(String grandChildId, Child child){
        this.grandChildId = grandChildId;
        this.child = child;
    }
}

 

@Getter
@Entity
@Table(name = "grandchild")
@IdClass(GrandChildId.class)
public class GrandChild {
    @Id
    @Column(name = "grandchild_id")
    private String grandChildId;

    @Id
    @ManyToOne(fetch = FetchType.LAZY)
    // Child 클래스는 Child_id(Pk), Parent_id(pk,fk) 복합키로 이루어져 있기 때문에
    // @JoinColumns를 사용해서 둘 모두 매핑해줘야합니다.
    @JoinColumns({
        @JoinColumn(name = "child_id", referencedColumnName = "child_id"),
        @JoinColumn(name = "parent_id", referencedColumnName = "parent_id")
    })
    private Child child;
}

 

@EmbeddedId

@EmbeddedId로 식별관계를 구성할 때는 @MapsId를 사용해야 합니다!

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

속성값은 @EmbeddedId를 사용한 필드명을 지정합니다.

 

@Getter
@Entity
public class Parent {

    @Id
    @Column(name = "parent_id")
    private String parentId;
}

 

@Entity
public class Child {
    @EmbeddedId
    private ChildId childId;

    @MapsId("parentId")
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "parent_id")
    private Parent parent;
}

 

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@EqualsAndHashCode
@Embeddable
public class ChildId implements Serializable {
    @Column(name = "child_id")
    private String childId;
    private String parentId; // @MapsId("parentId")로 Child 에서 매핑

    @Builder
    public ChildId(String childId, String parentId) {
        this.childId = childId;
        this.parentId = parentId;
    }
}

 

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@EqualsAndHashCode
@Embeddable
public class GrandChildId implements Serializable {
    @Column(name = "grandchild_id")
    private String grandChildId;

    private ChildId childId;

    @Builder
    public GrandChildId(String grandChildId, ChildId childId) {
        this.grandChildId = grandChildId;
        this.childId = childId;
    }
}

 

@Getter
@Entity
@Table(name = "grandchild")
public class GrandChild {

    @EmbeddedId
    private GrandChildId grandChildId;

    @MapsId("childId")
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumns({
        @JoinColumn(name = "child_id", referencedColumnName = "child_id"),
        @JoinColumn(name = "parent_id", referencedColumnName = "parent_id")
    })
    private Child child;
}

 

식별관계 보다  비식별 관계를 선호하는 이유

  • 식별 관계는 부모 테이블의 기본 키를 자식 테이블로 전파하기 때문에 기본 키 컬럼이 점점 늘어나게 된다.
  • 식별 관계는 일반적으로 복합 기본 키를 만들어야 하는 경우가 많습니다.
  • 식별 관계를 사용할 때 기본 키로 비즈니스 의미가 있는 자연 키 컬럼을 조합하는 경우가 많다.
  • 비식별 관계의 기본 키는 주로 대리 키를 사용하며 JPA에서는 @GeneratedValue와 같은 대리 키를 생성하는               편리한 방법을 제공해 준다.
  • 비식별 관계를 사용 시 식별자 컬럼이 하나이기에 매핑하기 편리하다.

물론 식별 관계도 기본 키 인덱스를 사용하기 편리하며 상위 테이블의 기본 키 컬럼을 자식 테이블이 가지고 있으므로         특정 상황에 조인 필요 없이 하위 테이블만으로 검색을 할 수 있다는 장점이 있습니다.

 

결론은 가능한 비식별 관계를 사용하고 기본 키는 Long 타입의 대리키를 사용하는 것 입니다.

또한 Inner 조인을 사용할 수 있는 Null 값을 허용하지 않는 필수적 비식별 관계를 사용하는게 더 좋다고 합니다.