Post

JPA 복합 기본 키

1. 복합 기본 키

복합 기본 키는 복합 키라고도 하며, 두 개 이상의 열을 조합하여 테이블의 기본 키를 형성하는 것이다.

JPA에서는 복합 키를 정의하는데 @IdClass@EmbeddedId 주석이라는 두 가지 옵션이 있다.

복합 기본 키를 정의하려면 몇 가지 규칙을 따라야 한다.

  • 복합 기본 키 클래스는 공개되어야 한다.

  • 인수가 없는 생성자가 있어야 한다.

  • equals()hashCode() 메서드를 정의해야 한다.

  • 직렬화가 가능 해야 한다.

2. IdClass 주석

Account라는 테이블이 있고 거기에 accountNumber와 accountType이라는 두 개의 열이 있으며, 이것이 복합 키를 형성한다고 가정한다. 이제 JPA에서 매핑해야 한다.

JPA 사양에 따라 다음과 같은 기본 키 필드를 사용하여 AccountId 클래스를 만든다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class AccountId implements Serializable {
    private String accountNumber;

    private String accountType;

    // default constructor

    public AccountId(String accountNumber, String accountType) {
        this.accountNumber = accountNumber;
        this.accountType = accountType;
    }

    // equals() and hashCode()
}

다음으로 AccountId 클래스를 엔티티 Account와 연결한다.

그렇게 하려면 @IdClass 주석으로 엔티티에 주석을 달아야 한다. 또한 엔티티 Account에서 AccountId 클래스의 필드를 선언 하고 @Id로 주석을 달아야 한다.

1
2
3
4
5
6
7
8
9
10
11
@Entity
@IdClass(AccountId.class)
public class Account {
    @Id
    private String accountNumber;

    @Id
    private String accountType;

    // other fields, getters and setters
}

3. EmbeddedId 주석

@EmbeddedId@IdClass 주석의 대안이다.

제목과 언어를 기본 키 필드로 하여 Book에 대한 일부 정보를 유지해야 하는 또 다른 예이다.

이 경우 기본 키 클래스인 BookId에 @Embeddable 주석을 추가해야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Embeddable
public class BookId implements Serializable {
    private String title;
    private String language;

    // default constructor

    public BookId(String title, String language) {
        this.title = title;
        this.language = language;
    }

    // getters, equals() and hashCode() methods
}

그런 다음 @EmbeddedId를 사용하여 이 클래스를 Book 엔터티에 포함해야 한다.

1
2
3
4
5
6
7
@Entity
public class Book {
    @EmbeddedId
    private BookId bookId;

    // constructors, other fields, getters and setters
}

4. @IdClass vs @EmbeddedId

둘 사이의 표면적 차이점은 @IdClass를 사용하면 열을 두 번 지정해야 한다는 것이다. 한 번은 AccountId에서, 또 한 번은 Account에서 지정해야 한다. 하지만 @EmbeddedId를 사용하면 그럴 필요가 없다.

하지만 그 외에도 몇 가지 단점도 있다.

예를 들어, 이러한 다양한 구조는 우리가 작성하는 JPQL 쿼리에 영향을 미친다.

@IdClass를 사용하면 쿼리가 좀 더 간단해진다.

1
SELECT account.accountNumber FROM Account account

@EmbeddedId를 사용하면 한 번의 추가 탐색을 수행해야 한다.

1
SELECT book.bookId.title FROM Book book

또한, @IdClass는 수정할 수 없는 복합 키 클래스를 사용하는 곳에서 매우 유용할 수 있다.

복합 키의 일부에 개별적으로 액세스하려는 경우 @IdClass를 사용할 수 있지만 전체 식별자를 객체로 자주 사용하는 경우 @EmbeddedId를 사용하는 것이 좋다.

5. 복합 기본 키를 사용한 카운트 쿼리

JPQLCriteriaQuery를 사용하면 내장된 ID를 기본 키로 사용하여 엔터티를 계산할 수 있다.

1) JPQL 사용

엔티티 관리자를 사용하여 엔티티에 대한 카운트를 수행하고 이를 실행하기 위해 JPQL 쿼리를 작성할 수 있다. getSingleResult() 메서드는 책 엔티티에 대한 카운트를 반환한다.

1
2
3
4
5
long countBookByEmbeddedIdUsingJPQL() {
    String jpql = "SELECT count(b) FROM Book b";
    Query query = em.createQuery(jpql);
    return (long) query.getSingleResult();
}

단위 테스트를 통해 결과를 확인할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
@Test
public void givenBookWithEmbeddedId_whenCountCalled_thenReturnsCount() {
    IntStream.rangeClosed(1, 10)
      .forEach(i -> {
          BookId bookId = new BookId("Book" + i, "English");
          Book book = new Book(bookId);
          book.setDescription("Novel and Historical Fiction" + i);
          persist(book);
      });
    assertEquals(10, countBookByEmbeddedIdUsingJPQL());
    assertEquals(10, countBookByEmbeddedIdUsingCriteriaQuery());
}

2) CriteriaQuery 사용

CriteriaQuery를 사용하여 임베디드 ID를 사용하여 엔터티를 계산할 수 있다. 먼저 루트 쿼리를 빌드한 다음 getSingleResult()를 사용하여 루트 쿼리에 대한 계산을 반환한다.

1
2
3
4
5
6
7
8
long countBookByEmbeddedIdUsingCriteriaQuery() {
    CriteriaBuilder criteriaBuilder = em.getCriteriaBuilder();
    CriteriaQuery<Long> criteriaQuery = criteriaBuilder.createQuery(Long.class);
    Root<Book> root = criteriaQuery.from(Book.class);
    criteriaQuery.select(criteriaBuilder.count(root));

    return em.createQuery(criteriaQuery).getSingleResult();
}

단위 테스트를 통해 결과를 확인할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
@Test
public void givenBookWithEmbeddedId_whenCountCalled_thenReturnsCount() {
    IntStream.rangeClosed(1, 10)
      .forEach(i -> {
          BookId bookId = new BookId("Book" + i, "English");
          Book book = new Book(bookId);
          book.setDescription("Novel and Historical Fiction" + i);
          persist(book);
      });
    assertEquals(10, countBookByEmbeddedIdUsingJPQL());
    assertEquals(10, countBookByEmbeddedIdUsingCriteriaQuery());
}

[출처 및 참고]

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