[JPA] 식별관계인 복합키 매핑

파워소동·2023년 4월 30일
0

JPA

목록 보기
2/4
post-thumbnail

JPA를 이용해서 복잡한 entity를 생성해야 할 때가 종종 있다.
단순하게 erd로 보자면 아래와 같은 상황이다.
부모의 PK 값을 본인의 PK로 가지는 식별관계인 경우 entity를 생성할 때 신경을 써주어야한다.

example ERD

할머니 엔티티의 pk를 부모 엔티티가 복합키로 가지고 있고 부모 엔티티의 복합키를 자식 엔티티가 그대로 가지고 있는 형태이다. 그림을 보면 이해가 빠르다.

단순히 부모 - 자식이 아닌 조상 - 할머니 - 부모 - 자식 이런식으로 몇번의 식별관계가 얽혀 있는 경우에 사용되는 예시이다.

java code

코드에서는 할머니 - 부모 - 자식 까지만 작성할것이다...

참고로 JPA는 복합키를 지원하기 위해 @IdClass@EmbeddedId 두 가지 방법을 제공하는데
@IdClass는 관계형 데이터베이스에 가까운 방법이고 @EmbeddedId는 좀 더 객체지향에 가까운 방법이다. 나는 @IdClass를 사용해서 복합키를 사용할 것이다.

참고로 IdClass를 사용할때 entity와 idClass가 동일한 변수명을 사용해야한다.

먼저 grand Entity의 코드이다.

@Entity
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Table(name = "TB_GRAND")
@TableGenerator(
        name = "generator",
        table = "sequences",
        pkColumnName = "seq",
        initialValue = 1,
		allocationSize = 100
)
public class GrandEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.TABLE, generator = "generator") // seq 생성
    @Column(name = "GRAND_PK", nullable = false)
    @Comment("할머니 PK")
    private Integer grandPrimaryKey;

    @Column
    ... (하위 생략) ...
    

내가 그린 erd기준 가장 상위의 entity에는 복합키가 없기때문에 idClass를 사용하지 않았다. 하지만 가장 상위의 entity가 복합키 처리를 해주어야겠지

그 다음은 parents entity 이다.

@Entity
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Table(name = "TB_PARENTS")
@TableGenerator(
        name = "generator",
        table = "sequences",
        pkColumnName = "seq",
        initialValue = 1,
		allocationSize = 100
)
@IdClass(ParentsEntityId.class) // 복합키
public class ParentsEntity {

	@Id
    @ManyToOne
    @JoinColumn(name = "GRAND_PK", nullable = false)
    @Comment("할머니 PK")
    private GrandEntity grand;

    @Id
    @GeneratedValue(strategy = GenerationType.TABLE, generator = "generator") // seq 생성
    @Column(name = "PARENTS_PK", nullable = false)
    @Comment("부모 PK")
    private Integer parentsPrimaryKey;
}

parents entity는 식별관계인 복합키를 사용하기 때문에 이를 지원하기 위한 IdClass가 필요하다.

복합키를 위한 ParentsEntityId 이다.

@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
public class ParentsEntityId implements Serializable {
    private Integer grand; 			   // grand entity 의 pk
    private Integer parentsPrimaryKey; // parents entity의 pk (본인)
}

여기까지는 그냥 저냥 복합 외래키 사용하듯이 하면 된다. 다음이 중요하다.

다음은 child entity 이다.

@Entity
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Table(name = "TB_CHILD")
@TableGenerator(
        name = "generator",
        table = "sequences",
        pkColumnName = "seq",
        initialValue = 1,
		allocationSize = 100
)
@IdClass(ChildEntityId.class) // 복합키
public class ChildEntity {

	@Id
    @ManyToOne
    @JoinColumns({
            @JoinColumn(name = "GRAND_PK", referencedColumnName = "GRAND_PK"),
            @JoinColumn(name = "CHILD_PK", referencedColumnName = "CHILD_PK")
    })
    @Comment("할머니PK, 부모 PK")
    private ParentsEntity parents;

	@Id
    @GeneratedValue(strategy = GenerationType.TABLE, generator = "generator") // seq 생성
    @Column(name = "CHILD_PK", nullable = false)
    @Comment("자식 PK")
    private Integer childPrimaryKey;
}

그리고 childEntity도 복합키이기 때문에 IdClass로 복합키를 선언해준다.

복합키를 위한 ChildEntityId 이다.

@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
public class ChildEntityId implements Serializable {
    private ParentsEntityId parents; // parents의 pk
    private Integer childPrimaryKey; // child entity의 pk (본인)
}

이런식으로 복합키를 가진 부모를 그대로 복합키로 받으면 할머니 PK, 부모 PK, 자식 PK 3개가 PK로 생긴다. ChildEntityId에 선언된 PK가 두개라 두개만 생성될 것 같은 느낌이지만 parentsEntityId속에 또 두개의 PK가 있기때문에 3개가 만들어 진다.

@JoinColumns

여기서 잘 봐야할 것은 @JoinColumns 어노테이션이다. @JoinColumns는 외래 키를 매핑할 때 사용하는 @JoinColumn의 복수형이며, 복수 개의 칼럼을 참조하는 조인 컬럼들을 매핑할 때 사용된다. 요걸로 식별관계인 복합키 매핑을 할 수 있다.

주의할 점

@Joincolumns

@Joincolumns 어노테이션을 사용할 때 referencedColumnName없이 name만 이용해서 사용하는 경우 순서가 바뀌어서 외래 키가 저장되는 경우가 있기 때문에 주의가 필요하다.
PK 이름은 parents_pk인데 실제로 외래 키로 매핑되는 값은 grand_pk가 될 수 있다는 말이다. (나도 일할 때 몇 번 그랬다... 힝...)
그래서 @JoinColumns 쓸 때에는 순서를 잘 맞춰서 쓰거나 referencedColumnName을 써서 이런 이슈를 방지하는 게 좋다고 한다.

테이블이 생성된 후 복합 키가 정상적으로 생성이 됐는지 외래 키 매핑이 잘 되어있는지 확인이 꼭 필요하다!

save()

우리의 JPA는 save를 치기 전 id가 null 이면 바로 insert를 때리지만 null이 아닌 경우 select 후 값이 있으면 update를 없으면 insert를 한다.

외래키를 가진 entity의 경우 한개를 저장 할 때에는 속도가 크게 상관 없을 수 있다. 하지만 100개의 insert를 하는 경우라면? select 쿼리 + insert 쿼리 2개의 쿼리가 나가기 때문에 200개의 쿼리가 발생하고 결과적으로는 개많은 IO가 발생하게 되어 속도가 굉장히 느려진당..

이 문제점은 아래 글에서 확인할 수 있다.
JPA 복합 외래키 사용시 insert 속도 개선

구럼 끗

0개의 댓글