[JPA] 복합 키와 식별관계 매핑 (김영한 저 JPA 프로그래밍)

GuruneLee·2023년 1월 1일
0

Let's Study 공부해요~

목록 보기
36/36
post-thumbnail

문제 상황

API 서버 개발 중, 복합pk 를 외래키로 사용하는 테이블을 매핑해야 하는 상황이 생겼다.

복합 키를 매핑하는 방법, 식별관계/비식별 관계를 매핑하는 방법에 대해 공부한 내용을 공유한다
(김영한 저 'JPA 프로그래밍' 책에서 발췌하였다)

식별 관계 vs 비식별 관계

fk가 pk에 포함되는지 여부에 따라,

  • fk가 pk에포함되면 식별관계
  • fk가 pk에포함되지 않으면 비식별관계

+ (추가 지식) 필수적 / 선택적 비식별 관계
: 외래키에 Null을 허용하면 선택적 비식별 관계, 외래키에 Null을 허용하지 않으면 필수적 비식별 관계

복합키를 사용한 비식별 관계 매핑 ( CHILD의 기본키는 다른것을 사용 )

Prorequisite - 복합 기본키 매핑

기본키 칼럼이 하나면 단순히 @Id 어노테이션을 붙이면 된다.
그러나, 복합키를 기본키로 이용하게 되면 @Id로는 힘들다 (에러남)

// @Id를 두 번 쓰면 에러가 난다
@Entity
class Wrong (
    @Id
    val id1: Long,
    @Id
    val id2: Long,
)
  • 에러가 발생하는 이유는, JPA의 엔티티 매핑 전략에 기인한다.
    • JPA는 기본키의 equals/hash 메서드를 이용해 엔티티를 식별하게 되므로 기본키 두 개 이상을 @Id로 지정해버리면 해당 메서드를 사용할 수 없게 된다 (어떤 메서드를 사용해야하는지 모름)
  • 따라서 @Id 를 사용하기 위해선 식별자 클래스를 새로 생성하여 복합키를 담아두어야 한다
    • 이를 위해 JPA에선 @IdClass@EmbeddedId 어노테이션을 지원한다

자, 이제 복합키를 사용한 비식별 관계 매핑 전략을 IdClassEmbeddedId로 풀어내보자.

@IdClass

위에서 설명한 대로, 복합키를 필드로 갖는 식별자 클래스를 만드는 전략이다. @EmbeddedId에 비해 RDBS에 친화적이다

PARENT 테이블이 (ID1, ID2) 를 기본키로 갖고, 이를 CHILD테이블이 이를 외래키로 받는 상황을 가정해보자.

먼저 PARENT 의 복합 기본키를 매핑하기 위해 식별자 클래스를 작성하자

// ParentId.kt 식별자 클래스
class ParentId(
    val id1: Long,
    val id2: Long,
): Serializable() {
    override fun equals(other: Any?) { ... }
    override fun hashCode() { ... }
}
  • 식별자 클래스의 속성명 == 엔티티의 식별자 속성명
  • Serializable 인터페이스를 구현해야 함
  • equals, hashCode를 구현해야 함
  • 기본 생성자가 있어야 함
  • 식별자 클래스는 public 해야 함

자 이제, 식별클래스 ParentId를 사용하는 Parent 엔티티를 작성해보자.

// Parent.kt 복합 기본키 매핑 클래스
@Entity
@IdClass(ParentId::class.java)
class Parent (
    @Id
    @Column(name = "PARENT_ID1")
    val id1: Long,
 
    @Id
    @Column(name = "PARENT_ID2")
    val id2: Long,
)

간단하다. @IdClass(식별자클래스.class) 를 선언하고, 필드 이름을 같게 맞춰주면 된다. Parent에서 ParentId 객체를 직접 사용하지 않는점을 주목하자.

PARENT 테이블을 매핑했으니, 이번엔 요놈을 외래키로 사용하는 CHILD 테이블과 그 연관관계를 매핑해보자

// Child.kt
@Entity
class Child(
    @Id
    val id: Long,
 
    @ManyToOne
    @JoinColumns ({
        @JoinColumn(name = , referencedColumnName = ),
        @JoinColumn(name = , referencedColumnName = ),
    })
    val parent: Parent,
)

@JoinColums 안에 @JoinColumn 을 여러번 사용해서 연관관계를 매핑하면 된다.

@EmbeddedId

: 비슷하긴 한데, 좀 더 객체지향적으로 엔티티를 설계할 수 있는 방법

바로 Parent.kt 를 보자.

// Parent.kt
@Entity
class Parent (
    @EmbeddedId
    val id: ParentId
)
  • Parent 클래스 내에서 ParentId를 직접 사용한다.
    • 객체를 분해하지 않고 합성을통해 사용한다는 점에서 @IdClass 보다 객체지향에 더 가깝다.

ParentId 클래스에는 어떤 차이가 있을까?

// ParentId.kt
// Embeddable 어노테이션을 선언하였다
// @IdClass와는 다르게, 식별자 클래스에 기본키를 직접 매핑하는 모습이다.
@Embeddable
class ParentId(
    @Column
    val id1: Long,
    @Column
    val id2: Long,
): Serializable() {
    override fun equals(other: Any?) { ... }
    override fun hashCode() { ... }
}
  • @EmbeddedId 어노테이션을 추가하였다.
  • @IdClass와는 다르게, 식별자 클래스에 @Column으로 직접 매핑하는 모습이다.

Child.kt 에서 EmbeddedId를 외래키로 가져와보자.

// Child.kt
@Entity
class Child(
    @Id
    child 고유의 기본키,
 
    @ManyToOne
    @JoinColumn(name = "PARENT_ID")
    val parent: Parent
)
  • Child 객체 입장에선 복합키가 없으므로, 그냥 다대일 연관관계를 만들어주면 된다.

복합키를 사용한 식별 관계 매핑 ( PARENT의 기본키를 외래키로 가져와서 CHILD의 기본키로 사용하자 )

@IdClass

Parent 는 똑같다. Child 가 어떻게 달라졌는지 확인해보자

// Parent 와 식별 관계인 Child
// 그냥 @Id 만 붙여주면 된다.
@Entity
class Child(
    @Id
    @ManyToOne
    @JoinColumns ({
        @JoinColumn(name = , referencedColumnName = ),
        @JoinColumn(name = , referencedColumnName = ),
    })
    val parent: Parent
)
  • @Id@ManyToOne에다 붙여주면 되는 모-습

@EmbeddedId ( feat. @MapsId )

@EmbeddedId 로 식별관계를 구성할 땐, @Id 대신 @MapsId 어노테이션을 사용해야 한다.

  • @MapsId : 외래키와 매핑한 연관관계를 기본키에도 매핑하겠다는 의미.

이번엔 복합키-식별관계 @IdClassChild 클래스를 비교해보자

// @EmbeddedId 를 이용한 복합키-식별관계
@Entity
class Child(
    @MpasId("parentId")
    @ManyToOne
    @JoinColumns({
        @JoinColumn, @JoinColumn
    })
    val parent: Parent
)
profile
Today, I Shoveled AGAIN....

0개의 댓글