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 값을 허용하지 않는 필수적 비식별 관계를 사용하는게 더 좋다고 합니다.
'TIL' 카테고리의 다른 글
JPA - QueryDSL (1) | 2024.11.15 |
---|---|
Service와 ServiceImpl 왜 나누는거지? (1) | 2024.11.13 |
미리 서명된 URL(Pre-signed url) 사용해보기 (0) | 2024.11.11 |
GCS를 이용한 이미지 업로드 기능 구현 (2) | 2024.11.08 |
AOP (0) | 2024.10.31 |