API 서버 개발 중, 복합pk 를 외래키로 사용하는 테이블을 매핑해야 하는 상황이 생겼다.
복합 키를 매핑하는 방법, 식별관계/비식별 관계를 매핑하는 방법에 대해 공부한 내용을 공유한다
(김영한 저 'JPA 프로그래밍' 책에서 발췌하였다)
fk가 pk에 포함되는지 여부에 따라,
+ (추가 지식) 필수적 / 선택적 비식별 관계
: 외래키에 Null
을 허용하면 선택적 비식별 관계, 외래키에 Null
을 허용하지 않으면 필수적 비식별 관계
기본키 칼럼이 하나면 단순히 @Id
어노테이션을 붙이면 된다.
그러나, 복합키를 기본키로 이용하게 되면 @Id
로는 힘들다 (에러남)
// @Id를 두 번 쓰면 에러가 난다
@Entity
class Wrong (
@Id
val id1: Long,
@Id
val id2: Long,
)
equals
/hash
메서드를 이용해 엔티티를 식별하게 되므로 기본키 두 개 이상을 @Id
로 지정해버리면 해당 메서드를 사용할 수 없게 된다 (어떤 메서드를 사용해야하는지 모름)@Id
를 사용하기 위해선 식별자 클래스를 새로 생성하여 복합키를 담아두어야 한다@IdClass
와 @EmbeddedId
어노테이션을 지원한다자, 이제 복합키를 사용한 비식별 관계 매핑 전략을 IdClass
와 EmbeddedId
로 풀어내보자.
위에서 설명한 대로, 복합키를 필드로 갖는 식별자 클래스를 만드는 전략이다. @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() { ... }
}
자 이제, 식별클래스 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
안에 @JoinColum
n 을 여러번 사용해서 연관관계를 매핑하면 된다.
: 비슷하긴 한데, 좀 더 객체지향적으로 엔티티를 설계할 수 있는 방법
바로 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
가 어떻게 달라졌는지 확인해보자
// Parent 와 식별 관계인 Child
// 그냥 @Id 만 붙여주면 된다.
@Entity
class Child(
@Id
@ManyToOne
@JoinColumns ({
@JoinColumn(name = , referencedColumnName = ),
@JoinColumn(name = , referencedColumnName = ),
})
val parent: Parent
)
@Id
만 @ManyToOne
에다 붙여주면 되는 모-습@EmbeddedId
로 식별관계를 구성할 땐, @Id
대신 @MapsId
어노테이션을 사용해야 한다.
@MapsId
: 외래키와 매핑한 연관관계를 기본키에도 매핑하겠다는 의미.이번엔 복합키-식별관계 @IdClass
와 Child
클래스를 비교해보자
// @EmbeddedId 를 이용한 복합키-식별관계
@Entity
class Child(
@MpasId("parentId")
@ManyToOne
@JoinColumns({
@JoinColumn, @JoinColumn
})
val parent: Parent
)