본문 바로가기
Study/spring

JPA 엔티티 복합키(Composite Primary Keys) 매핑

by 유경호 2021. 9. 13.
반응형

개요

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

 

반응형