Post

JPA 단일 엔터티 여러 테이블 매핑

1. 테이블에 매핑

JPA는 Java 애플리케이션에서 관계형 데이터베이스 모델을 다루는 것을 덜 고통스럽게 만든다. 모든 테이블을 단일 엔티티 클래스에 매핑하면 모든 것이 간단하다.

하지만 때때로 엔터티와 테이블을 다르게 모델링해야 하는 이유가 있다.

  • 필드의 논리적 그룹을 만들려면 여러 클래스를 단일 테이블에 매핑할 수 있다.

  • 상속이 관련된 경우 클래스 계층 구조를 테이블 구조에 매핑할 수 있다.

  • 관련 필드가 여러 테이블에 분산되어 있고 해당 테이블을 단일 클래스로 모델링하려는 경우이다.

2. 데이터 모델

예를 들어, 레스토랑을 운영한다고 가정하고, 제공하는 모든 식사에 대한 데이터를 저장하고 싶다고 한다.

  • 이름

  • 설명

  • 가격

  • 어떤 종류의 알레르기 물질이 포함되어 있는지

가능한 알레르기 유발 물질이 많으므로 이 데이터 세트를 그룹화한다.

또한 다음 테이블 정의를 사용하여 이를 모델링한다.

meals

3. 여러 엔터티 생성

가장 확실한 해결책은 두 클래스에 대한 엔터티를 만드는 것이다.

MealWithMultipleEntities 엔터티를 정의한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Entity
@Table(name = "meal")
public class MealWithMultipleEntities {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    Long id;

    @Column(name = "name")
    String name;

    @Column(name = "description")
    String description;

    @Column(name = "price")
    BigDecimal price;

    @OneToOne(mappedBy = "meal")
    AllergensAsEntity allergens;

    // standard getters and setters
}

다음으로 AllergensAsEntity 엔터티를 추가한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Entity
@Table(name = "allergens")
class AllergensAsEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "meal_id")
    Long mealId;

    @OneToOne
    @PrimaryKeyJoinColumn(name = "meal_id")
    Meal meal;

    @Column(name = "peanuts")
    boolean peanuts;

    @Column(name = "celery")
    boolean celery;

    @Column(name = "sesame_seeds")
    boolean sesameSeeds;

    // standard getters and setters
}

meal_id가 기본 키이자 외래 키라는 것을 알 수 있다. 즉, @PrimaryKeyJoinColumn을 사용하여 일대일 관계 열을 정의해야 한다.

하지만 이 방법에는 두 가지 문제가 있다.

  • 항상 식사를 위해 알레르기 유발 물질을 저장하고 싶어하는데, 이 방법은 이 규칙을 강제하지 않는다.

  • 식사와 알레르기 데이터는 논리적으로 함께 속한다. 따라서 여러 테이블을 만들었더라도 이 정보를 동일한 Java 클래스에 저장하고 싶을 수 있다.

첫 번째 문제에 대한 가능한 해결책 중 하나는 MealWithMultipleEntities 엔터티의 allergens 필드에 @NotNull 주석을 추가하는 것이다. JPA는 null AllergensAsEntity가 있는 경우 MealWithMultipleEntities를 지속할 수 없다.

하지만 이것은 이상적인 해결책이 아니다. AllergensAsEntity 없이 MealWithMultipleEntities를 지속할 기회조차 없는 더 제한적인 해결책이 필요하다.

4. @SecondaryTable로 단일 엔터티 생성

@SecondaryTable 주석을 사용하면 서로 다른 테이블에 열이 있음을 지정하여 단일 엔터티를 생성할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Entity
@Table(name = "meal")
@SecondaryTable(name = "allergens", pkJoinColumns = @PrimaryKeyJoinColumn(name = "meal_id"))
class MealAsSingleEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    Long id;

    @Column(name = "name")
    String name;

    @Column(name = "description")
    String description;

    @Column(name = "price")
    BigDecimal price;

    @Column(name = "peanuts", table = "allergens")
    boolean peanuts;

    @Column(name = "celery", table = "allergens")
    boolean celery;

    @Column(name = "sesame_seeds", table = "allergens")
    boolean sesameSeeds;

    // standard getters and setters

}

백그라운드에서 JPA는 기본 테이블과 보조 테이블을 조인하고 필드를 채운다. 이 방법은 @OneToOne 관계와 비슷 하지만, 이런 방식으로 모든 속성을 같은 클래스에 둘 수 있다.

2차 테이블에 있는 열이 있는 경우 @Column 주석의 table 인수로 지정해야 한다. 1차 테이블에 있는 열의 경우 JPA가 기본적으로 1차 테이블에서 열을 찾기 때문에 table 인수를 생략할 수 있다.

또한, @SecondaryTables에 임베드하면 여러 개의 보조 테이블을 가질 수 있다. 또는 Java 8부터 반복 가능한 주석이기 때문에 여러 개의 @SecondaryTable 주석 으로 엔티티를 표시할 수 있다.

5. @SecondaryTable과 @Embedded 결합

@SecondaryTable은 여러 테이블을 동일한 엔터티에 매핑한다. 또한 @Embedded@Embeddable은 반대로 단일 테이블을 여러 클래스에 매핑한다.

@SecondaryTable@Embedded, @Embeddable을 결합하면 어떤 결과가 나오는지 확인한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
@Entity
@Table(name = "meal")
@SecondaryTable(name = "allergens", pkJoinColumns = @PrimaryKeyJoinColumn(name = "meal_id"))
class MealWithEmbeddedAllergens {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    Long id;

    @Column(name = "name")
    String name;

    @Column(name = "description")
    String description;

    @Column(name = "price")
    BigDecimal price;

    @Embedded
    AllergensAsEmbeddable allergens;

    // standard getters and setters

}

@Embeddable
class AllergensAsEmbeddable {

    @Column(name = "peanuts", table = "allergens")
    boolean peanuts;

    @Column(name = "celery", table = "allergens")
    boolean celery;

    @Column(name = "sesame_seeds", table = "allergens")
    boolean sesameSeeds;

    // standard getters and setters

}

이는 @OneToOne을 사용하여 본 것과 유사한 접근 방식이다. 그러나 몇 가지 장점이 있다.

  • JPA가 두 테이블을 함께 관리해주기 때문에 두 테이블 모두 각 식사마다 행이 있도록 할 수 있다.

  • 구성이 덜 필요하므로 코드도 조금 더 간단해졌다.

그럼에도 불구하고, 이러한 일대일 유사 솔루션은 두 테이블의 ID가 일치하는 경우에만 작동한다.

AllergensAsEmbeddable 클래스를 재사용하려면 MealWithEmbeddedAllergens 클래스에서 보조 테이블의 열을 @AttributeOverride로 정의하는 것이 더 좋다.

[출처 및 참고]

This post is licensed under CC BY 4.0 by the author.