개요
JPA를 사용하여 1:m, n:1 관계 테이블을 엔티티 매핑할 때 복합 키(Composite Primary Keys)를 가진 테이블을 엔티티 매핑 시, 제가 겪은 문제점과 해결방안을 정리해보겠습니다.
문제
문제는 아래의 [USR_TERMS_AGRMT] 테이블을 엔티티 매핑하던 중 발생했습니다. 아래와 같이 1:m, n:1 관계 테이블은 참조하는 테이블들의 PK를 PFK로 사용하여 구성되는 경우가 종종 있습니다.
저는 처음에 아래와 같이 [USR_TERMS_AGRMT] 객체 연관관계에 @Id 어노테이션을 붙여 엔티티 매핑을 시도하였습니다.
@Entity
@Table(name = "USR_TERMS_AGRMT")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class UsrTermsAgrmtEntity implements Serializable {
@Id
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "MEM_NO")
private UsrEntity usr;
@Id
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "AGRMT_ITEM_CD")
private UsrTermsItemEntity usrTermsItem;
@Column(name = "AGRMT_YN")
private String agrmtYn;
@Column(name = "AGRMT_DTM")
private LocalDateTime agrmtDtm;
...
}
위와 같은 시도는 컴파일 시 오류를 발생시키지 않지만 다음과 같은 상황에 문제가 발생합니다.
public interface UsrTermsAgrmtRepository extends JpaRepository<UsrTermsAgrmtEntity, ?> {
}
위와 같이 Spring Data Jpa의 JpaRepository를 정의할 때, ID 제네릭에 ID가 어떤 타입인지에 대한 Object를 주입해줄 수 없는 상황이 발생합니다.
해결책
이 문제를 해결하기 위해 자바 진영의 JPA에서는 @IdClass와 @EmbeddedId 두 어노테이션을 옵션으로 제공하고 있습니다.
두 방식 모두 공통적으로 복합키(composite primary key)를 정의할 때 아래와 같은 규칙들을 준수해주어야 합니다.
- The composite primary key class must be public.
- It must have a no-arg constructor.
- It must define the equals() and hashCode() methods.
- It must be Serializable.
그럼 @IdClass, @EmbeddedId 각 두 어노테이션을 사용한 해결법을 한 번 살펴보겠습니다.
IdClass 어노테이션
위 규칙을 준수하여 composite primary key class를 아래와 같이 정의해줍니다.
@NoArgsConstructor
@AllArgsConstructor
@Getter
@EqualsAndHashCode
public class UsrTermsAgrmtID implements Serializable {
private Integer usr;
private Integer usrTermsItem;
}
- 필드 Type은 각 매핑될 식별관계 엔티티의 Id 타입으로 매핑시켜줌.
- 필드명은 엔티티의 식별관계를 매핑한 필드명과 동일하게 맞춰주어 매핑
그리고 엔티티 클래스를 아래와 같이 작성해줍니다.
@Entity
@IdClass(UsrTermsAgrmtID.class)
@Table(name = "USR_TERMS_AGRMT")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class UsrTermsAgrmtEntity {
@Id
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "MEM_NO")
private UsrEntity usr;
@Id
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "AGRMT_ITEM_CD")
private UsrTermsItemEntity usrTermsItem;
...
}
- IdClass 어노테이션의 value 필드에 UsrTermsAgrmtID 클래스를 주입하여 줍니다.
EmbeddedId 어노테이션
EmbeddedId 어노테이션 사용시에도 JPA PK 매핑 명세를 준수하여 복합키 클래스를 아래와 같이 정의해줍니다.
@Embeddable
@NoArgsConstructor
@AllArgsConstructor
@Getter @Setter
@EqualsAndHashCode
public class UsrTermsAgrmtID implements Serializable {
@Column(name = "MEM_NO")
private Integer memNo;
@Column(name = "AGRMT_ITEM_CD")
private Integer agrmtItemCd;
}
- IdClass 어노테이션을 사용할 때 정의했던 UsrTermsAgrmtID와의 차이점은 @Embeddable을 클래스에 붙여줘야한다는 점
- @Column 어노테이션을 사용하고자 한다면 복합키 클래스에 정의해준다.
@Entity
@Table(name = "USR_TERMS_AGRMT")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class UsrTermsAgrmtEntity {
@EmbeddedId
private UsrTermsAgrmtID usrTermsAgrmtID;
...
}
또한 EmbeddedId 사용 시 객체 연관관계 매핑 방법을 알아보겠습니다.
@Embeddable
@NoArgsConstructor
@AllArgsConstructor
@Getter @Setter
@EqualsAndHashCode
public static class UsrTermsAgrmtID implements Serializable {
@Column(name = "MEM_NO")
private Integer memNo;
@Column(name = "AGRMT_ITEM_CD")
private Integer agrmtItemCd;
}
@Entity
@Table(name = "USR_TERMS_AGRMT")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class UsrTermsAgrmtEntity {
@EmbeddedId
private UsrTermsAgrmtID usrTermsAgrmtID;
// other cloumn ..
@ManyToOne(fetch = FetchType.LAZY)
@MapsId("memNo")
private UsrEntity usr;
@ManyToOne(fetch = FetchType.LAZY)
@MapsId("agrmtItemCd")
private UsrTermsItemEntity usrTermsItem;
...
}
- @IdClass와 다른 점은 @Id 대신 @MapsId를 사용하는 점
- @MapsId의 속성 값은 @EmbeddedId를 사용한 식별자 클래스의 기본 키 필드명을 주입하여 매핑하면 됨.
- @MapsId는 외래 키와 매핑한 연관관계를 기본 키에도 매핑하겠다는 뜻
JpaRepsotiroy 정의
위 두 방식을 사용하여 매핑이 완료 되었다면 마침내 아래와 같이 ID 제네릭 타입에 복합키 클래스를 주입함으로써 JpaRepository를 정의해줄 수 있습니다!
public interface UsrTermsAgrmtRepository extends JpaRepository<UsrTermsAgrmtEntity, UsrTermsAgrmtID> {
}
두 방식의 차이점
@IdClass
는 같은 형태의 칼럼을 복합키 클래스와 엔티티 클래스에 두 번 정의해줘야합니다. @EmbeddedId
를 사용하는 경우에는 복합키 클래스에만 칼럼을 정의해주면 되는 차이점이 있습니다.
또한 JPQL 쿼리 사용 시에도 차이점이 드러나게 됩니다.
@IdClass
를 사용하는 경우, 단순하게 PK 칼럼을 색인할 수 있다.
SELECT usrTermsAgrmt.memNo FROM UsrTermsAgrmtEntity usrTermsAgrmt
@EmbeddedId
를 사용하는 경우, PK 칼럼 색인 시 한 계층 더 들어가야함.
SELECT usrTermsAgrmt.usrTermsAgrmtID.memNo FROM UsrTermsAgrmtEntity usrTermsAgrmt
그리고 @IdClass
의 경우 복합키 클래스를 활용하여 객체지향적인 프로그래밍이 불가능하다는 특징이 있어 복합키 칼럼에 대한 수정이 이루어지지 않는 엔티티에서 활용하기 좋은 특징이 있으며, @EmbeddedId
는 복합키 클래스를 활용하여 객체지향적인 프로그래밍을 통해 복합키 칼럼들을 다룰 수 있다는 특징이 있습니다.
참조
Composite Primary Keys in JPA - https://www.baeldung.com/jpa-composite-primary-keys
Failed to convert request element in entity with @IdClass - https://stackoverflow.com/questions/39185977/failed-to-convert-request-element-in-entity-with-idclass
JPA - Composite key with @EmbeddedId involving @ManyToOne relationship - https://www.logicbig.com/tutorials/java-ee-tutorial/jpa/embedded-id-with-many-to-one-relation.html
'Study > spring' 카테고리의 다른 글
Spring - 컨트롤러 메소드 파라미터에 대한 공통 처리 로직을 한 번에! HandlerMethodArgumentResolver 기본 정리 (0) | 2021.03.27 |
---|---|
Spring Web MVC - Multipart 요청 다루기 (0) | 2021.02.23 |
Spring Cache Abstraction 정리 (0) | 2021.02.09 |
Spring REST Docs 개념 및 간단한 예제 (0) | 2021.02.02 |
QueryDSL(3) - 프로젝션, distinct, 동적 쿼리, 벌크 연산 (0) | 2021.01.26 |