Spring Data JPA 에는 Specifications 라는 것이 존재합니다. 이를 활요어하면 복잡하게 구성될 수 있는 WHERE 조건, JOIN TABLE 등의 행위를 편하게 진행할 수 있습니다.

여기서는 간단한 튜토리얼 형태로 어떻게 Join 할 수 있는 지 살펴봅니다.

JPA Specification

Spring Data JPA 에서는 Specification이라는 interface 를 제공합니다.

이것을 활용하면, 재사용 가능한 component 들을 활용하여 dynamic query 를 생성할 수 있습니다.

여기서는 AuthorBook 이라는 class가 있다고 가졍합니다.

@Entity
public class Author {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String firstName;

    private String lastName;

    @OneToMany(cascade = CascadeType.ALL)
    private List<Book> books;

    // getters and setters
}

Author entity에 대해서 dynamic query 를 작성하기위해, Specification interface를 implements 합니다.

public class AuthorSpecifications {

    public static Specification<Author> hasFirstNameLike(String name) {
        return (root, query, criteriaBuilder) ->
          criteriaBuilder.like(root.<String>get("firstName"), "%" + name + "%");
    }

    public static Specification<Author> hasLastName(String name) {
        return (root, query, cb) ->
          cb.equal(root.<String>get("lastName"), name);
    }
}

마지막으로, AuthorRepositoryJpaSpecificationExecutor 를 상속받도록 구성합니다.

@Repository
public interface AuthorsRepository extends JpaRepository<Author, Long>, JpaSpecificationExecutor<Author> {
}

이러한 방식으로, 두 개의 specification을 chain해서 query 를 생성할 수 있습니다.

@Test
public void whenSearchingByLastNameAndFirstNameLike_thenOneAuthorIsReturned() {
    
    Specification<Author> specification = hasLastName("Martin")
      .and(hasFirstNameLike("Robert"));

    List<Author> authors = repository.findAll(specification);

    assertThat(authors).hasSize(1);
}

JPA Specification을 이용한 Table Join

앞서 살펴보았던 Author entity가 Book entity와 one-to-many (일대다) 관계를 가지고 있음을 알 수 있습니다.

@Entity
public class Book {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String title;

    // getters and setters
}

Criteria Query API 를 이용해서, Specification를 만들 때, 두 개의 Table을 Join 할 수 있습니다. 이러한 방식으로 Book entity의 특정 field를 우리의 쿼리에 포함할 수 있습니다.

public static Specification<Author> hasBookWithTitle(String bookTitle) {
    return (root, query, criteriaBuilder) -> {
        Join<Book, Author> authorsBook = root.join("books");
        return criteriaBuilder.equal(authorsBook.get("title"), bookTitle);
    };
}

이제, 앞서 만들어두었던 Specification과 병합할 수 있습니다.

@Test
public void whenSearchingByBookTitleAndAuthorName_thenOneAuthorIsReturned() {

    Specification<Author> specification = hasLastName("Martin")
      .and(hasBookWithTitle("Clean Code"));

    List<Author> authors = repository.findAll(specification);

    assertThat(authors).hasSize(1);
}

생성된 쿼리를 확인하면 join이 추가되었음을 알 수 있습니다.

select 
  author0_.id as id1_1_, 
  author0_.first_name as first_na2_1_, 
  author0_.last_name as last_nam3_1_ 
from 
  author author0_ 
  inner join author_books books1_ on author0_.id = books1_.author_id 
  inner join book book2_ on books1_.books_id = book2_.id 
where 
  author0_.last_name = ? 
  and book2_.title = ?

참고자료 및 출처


Leave a comment