스프링 데이터 JPA는 스프링 프레임워크에서 JPA를 편리하게 사용할 수 있도록 지원하는 프로젝트다. 이 프로젝트는 데이터 접근 계층을 개발할 때 지루하게 반복되는 CRUD 문제를 해결해준다. 우선 CRUD를 처리하기 위한 공통 인터페이스를 제공하고, 리포지토리를 개발할 때 인터페이스만 작성하면 실행 시점에 스프링 데이터 JPA가 구현 객체를 동적으로 생성해서 주입해준다. 따라서 데이터 접근 계층을 개발할 때 구현 클래스 없이 인터페이스만 작성해도 개발을 완료할 수 있다.
스프링 데이터 JPA를 사용하면 아래와 같이 인터페이스만 작성하면 된다.
public interface MemberRepository extends JpaRepository<Member, Long> {
Member findByUsername(String username);
}
public interface ItemRepository extends JpaRepository<Item, Long> {}
CRUD를 처리하기 위한 공통 메소드는 스프링 데이터 JPA가 제공하는 org.springframework.data.jpa.repository.JpaRepository
인터페이스에 있다. 그리고 방금 언급했듯이 회원과 상품 리포지토리 인터페이스의 구현체는 애플리케이션 실행시점에 스프링 데이터 JPA가 생성해서 주입해준다. 따라서 개발자가 직접 구현체를 개발하지 않아도 된다.
클래스 다이어그램은 다음과 같다.
일반적인 CRUD 메소드는 JpaRepository 인터페이스가 공통으로 제공하므로 문제가 없다. 그런데 MemberRepository.findByUsername(..)
처럼 직접 작성한 공통으로 처리할 수 없는 메소드는 어떻게 해야할까? 놀랍게도 스프링 데이터 JPA는 메소드 이름을 분석해서 다음 JPQL을 실행한다.
select m from Member m where username = :username
스프링 데이터 JPA는 스프링 데이터 프로젝트의 하위 프로젝트 중 하나다.
스프링 데이터 프로젝트는 JPA, 몽고 DB, NEO4J, REDIS, HADOOP, GEMFIRE 같은 다양한 데이터 저장소에 대한 접근을 추상화해서 개발자 편의를 제공하고 지루하게 반복하는 데이터 접근 코드를 줄여준다.
스프링 데이터 JPA 프로젝트는 JPA에 특화된 기능을 제공한다. 스프링 프레임워크와 JPA를 함께 사용한다면 스프링 데이터 JPA 사용을 적극 추천한다.
스프링 데이터 JPA를 사용하기 위한 라이브러리와 환경 설정 방법을 알아보자.
스프링 데이터 JPA는 아래와 같이 spring-data-jpa 라이브러리가 필요하다.
dependencies {
...
implementation 'org.springframework.data:spring-data-jpa'
}
스프링 데이터 JPA는 애플리케이션을 실행할 때 basePackage에 있는 리포지토리 인터페이스들을 찾아서 해당 인터페이스를 구현한 클래스를 동적으로 생성한 다음 스프링 빈으로 등록한다. 따라서 개발자가 직접 구현 클래스를 만들지 않아도 된다.
스프링 데이터 JPA는 간단한 CRUD 기능을 공통으로 처리하는 JpaRepository 인터페이스를 제공한다. 스프링 데이터 JPA를 사용하는 가장 단순한 방법은 이 인터페이스를 상속받는 것이다. 그리고 제네릭에 엔티티 클래스와 엔티티 클래스가 사용하는 식별자 타입을 지정하면 된다.
public interface JpaRepository<T, ID extends Serializable> extends PagingAndSortingRepository<T, ID> {
...
}
public interface MemberRepository extends JpaRepository<Member, Long> {}
상속 받은 JpaRepository<Member, Long>
부분을 보면 제네릭에 회원 엔티티와 회원 엔티티의 식별자 타입을 지정했다. 이제부터 회원 리포지토리는 JpaRepository 인터페이스가 제공하는 다양한 기능을 사용할 수 있다.
JpaRepository 인터페이스의 계층 구조를 살펴보자.
위 그림을 보면 윗부분에 스프링 데이터 모듈이 있고 그 안에 Repository, CrudRepository, PagingAndSortingRepository가 있는데 이것은 스프링 데이터 프로젝트가 공통으로 사용하는 인터페이스다. 스프링 데이터 jPA가 제공하는 JpaRepository 인터페이스는 여기에 추가로 JPA에 특화된 기능을 제공한다.
JpaRepository 인터페이스를 상속받으면 사용할 수 있는 주요 메소드 몇 가지를 간단히 소개하겠다. 참고로 T는 엔티티, ID는 엔티티의 식별자 타입, S는 엔티티와 그 자식 타입을 뜻한다.
주요 메소드는 다음과 같다.
스프링 데이터 JPA가 제공하는 쿼리 메소드 기능은 크게 3가지가 있다.
@Query
어노테이션을 사용해서 레포지토리 인터페이스에 쿼리 직접 정의 이 기능들을 활용하면 인터페이스만으로 필요한 대부분의 쿼리 기능을 개발할 수 있다.
이메일과 이름으로 회원을 조회하려면 다음과 같은 메소드를 정의하면 된다.
public interface MemberRepository extends Repository<Member, Long> {
List<Member> findByEmailAndName(String email, String name);
}
인터페이스에 정의한 findByEmailAndName(...)
메소드를 실행하면 스프링 데이터 JPA는 메소드 이름을 분석해서 JPQL을 생성하고 실행한다. 실행된 jPQL은 다음과 같다.
select m from Member m where m.email = ?1 and m.name = ?2
물론 정해진 규칙에 따라서 메소드 이름을 지어야 한다. 스프링 데이터 JPA 공식 문서가 제공하는 아래 표를 보면 이 기능을 어떻게 사용해야 하는지 쉽게 이해할 수 있다.
해당 표는 스프링 데이터 JPA 공식 문서가 제공하는 쿼리 생성 기능이다.
이 기능은 엔티티의 필드명이 변경되면 인터페이스에 정의한 메소드 이름도 꼭 함께 변경해야 한다!! 그렇지 않으면 애플리케이션을 시작하는 시점에 오류가 발생한다.
스프링 데이터 JPA는 메소드 이름으로 JPA Named 쿼리를 호출하는 기능을 제공한다.
JPA Named 쿼리는 이름 그대로 쿼리에 이름을 부여해서 사용하는 방법인데 아래와 같이 어노테이션이나 XML에 쿼리를 정의할 수 있다. (상세 내용은 10.2.15 절에 있다.) 같은 방법으로 Named 네이티브 쿼리도 지원한다.
@Entity
@NamedQuery(name="Member.findByUsername", query="select m from Member m where m.username = :username")
public class Member {
...
}
이렇게 정의한 Named 쿼리를 JPA에서 직접 호출하려면 아래 코드처럼 작성해야 한다.
public class MemberRepository {
public List<Member> findByUsername(String username) {
...
List<Member> resultList = em.createNamedQuery("Member.findByUsername", Member.class).setParameter("username", "회원1").getResultList();
}
}
스프링 데이터 JPA를 사용하면 아래와 같이 메소드 이름만으로 Named 쿼리를 호출할 수 있다.
public interface MemberRepository extends JpaRepository<Member, Long> {
List<Member> findByUsername(@Param("username") String username);
}
스프링 데이터 JPA는 선언한 "도메인 클래스 + .(점) + 메소드 이름"으로 Named 쿼리를 찾아서 실행한다. 따라서 예제는 Member.findByUsername
이라는 Named 쿼리를 실행한다. 만약 실행할 Named 쿼리가 없으면 메소드 이름으로 쿼리 생성 전략을 사용한다
위에서 findByUsername()
메소드의 파라미터에 @Param
을 사용했는데 이것은 이름기반 파라미터를 바인딩할 때 사용하는 어노테이션이다. 자세한 내용은 조금 뒤의 파라미터 바인딩에서 알아본다.
리포지토리 메소드에 직접 쿼리를 정의하려면 아래와 같이 @org.springframework.data.jpa.repository.Query
어노테이션을 사용한다. 이 방법은 실행할 메소드에 정적 쿼리를 직접 작성하므로 이름 없는 Named 쿼리라 할 수 있다. 또한 JPA Named 쿼리처럼 애플리케이션 실행 시점에 문법 오류를 발견할 수 있는 장점이 있다.
public interface MemberRepository extends JpaRepository<Member, Long> {
@Query("select m from Member m whre m.username = ?1")
Member findByUsername(String username);
}
네이티브 SQL을 사용하려면 아래와 같이 @Query
어노테이션에 nativeQuery = true
를 설정한다. 참고로 스프링 데이터 JPA가 지원하는 파라미터 바인딩을 사용하면 JPQL은 위치 기반 파라미터를 1부터 시작하지만 네이티브 SQL은 아래와 같이 0부터 시작한다.
public interface MemberRepository extends JpaRepository<Member, Long> {
@Query(value = "SELECT * FROM MEMBER WHERE USERNAME = ?0", nativeQuery = true)
Member findByUsername(String username);
}
스프링 데이터 JPA는 위치 기반 파라미터 바인딩과 이름 기반 파라미터 바인딩을 모두 지원한다.
select m from Member m whre m.username = ?1 // 위치 기반
select m from Member m whre m.username = :name // 이름 기반
기본값은 위치 기반인데 파라미터 순서로 바인딩한다. 이름 기반 파라미터 바인딩을 사용하려면 아래와 같이 org.springframework.data.repository.query.Param(파라미터 이름)
어노테이션을 사용하면 된다. 코드 가독성과 유지보수를 위해 이름 기반 파라미터 바인딩을 사용하자.
public interface MemberRepository extends JpaRepository<Member, Long> {
@Query("select m from Member m where m.username = :name")
Member findByUsername(@Param("name") String username);
}
아래 코드를 통해 JPA로 작성한 벌크성 수정 쿼리부터 보자.
int bulkPriceUp(String stockAmount) {
...
String qlString = "update Product p set p.price = p.price * 1.1 where p.stockAmount < : stockAmount";
int resultCount = em.createQuery(qlString).setParameter("stockAmount", stockAmount).executeUpdate();
}
다음으로 아래의 스프링 데이터 JPA를 사용한 벌크성 수정 쿼리를 보자.
@Modifying
@Query("update Product p set p.price = p.price * 1.1 where p.stockAmount < :stockAmount")
int bulkPriceUp(@Param("stockAmount") String stockAmount);
스프링 데이터 JPA에서 벌크성 수정, 삭제 쿼리는 org.springframework.data.jpa.repository.Modifying
어노테이션을 사용하면 된다.
벌크성 쿼리를 실행하고 나서 영속성 컨텍스트를 초기화하고 싶으면 @Modifying(clearAutomatically = true)
처럼 clearAutomatically
옵션을 true
로 설정하면 된다. 참고로 이 옵션의 기본값은 false
다.
스프링 데이터 JPA는 유연한 반환 타입을 지원하는데 결과가 한 건 이상이면 컬렉션 인터페이스를 사용하고, 단건이면 반환 타입을 지정한다.
List<Member> findByName(String name); // 컬렉션
Member findByEmail(String email); // 단건
만약 조회 결과가 없으면 컬렉션은 빈 컬렉션을 반환하고 단건은 null을 반환한다. 그리고 단건을 기대하고 반환 타입을 지정했는데 결과가 2건 이상 조회되면 예외가 발생한다.
참고로 단건으로 지정한 메소드를 호출하면 스프링 데이터 JPA는 내부에서 JPQL의 Query.getSingleResult()
메소드를 호출한다. 이 메소드를 호출했을 때 조회 결과가 없으면 예외가 발생하는데 개발자 입장에서 다루기가 상당히 불편하다. 스프링 데이터 JPA는 단건을 조회할 때 이 예외가 발생하면 예외를 무시하고 대신에 null을 반환한다.
스프링 데이터 JPA는 쿼리 메소드에 페이징과 정렬 기능을 사용할 수 있도록 2가지 특별한 파라미터를 제공한다.
org.springframework.data.domain.Sort
: 정렬 기능org.springframework.data.domain.Pageable
: 페이징 기능(내부에 Sort 포함)아래와 같이 파라미터에 Pageable
을 사용하면 반환 타입으로 List나 org.springframework.data.domain.Page
를 사용할 수 있다. 반환 타입으로 Page를 사용하면 스프링 데이터 JPA는 페이징 기능을 제공하기 위해 검색된 전체 데이터 건수를 조회하는 count 쿼리를 추가로 호출한다.
// count 쿼리 사용
Page<Member> findByName(String name, Pageable pageable);
// count 쿼리 사용 안 함
List<Member> findByName(String name, pageable pageable);
List<Member> findByName(String name, Sort sort);
다음 조건으로 페이징과 정렬을 사용하는 예제 코드를 보자.
public interface MemberRepository extends JpaRepository<Member, Long> {
Page<Member> findByStartingWith(String name, Pageable pageable);
}
// 페이징 조건과 정렬 조건 설정
PageRequest pageRequest = new PageRequest(0, 10, new Sort(Direction.DESC, "name"));
Page<Member> result = memberRepository.findByNameStartingWith("김", pageRequest);
List<Member> members = result.getConent(); // 조회된 데이터
int totalPages = result.getTotalPages(); // 전체 페이지 수
boolean hasNextPage = result.hasNextPage(); // 다음 페이지 존재 여부
위 예제에서 두 번째 파라미터로 받은 Pageable
은 인터페이스다. 따라서 실제 사용할 때는 코드와 같이 해당 인터페이스를 구현한 PageRequest
객체를 사용한다. PageRequest
생성자의 첫 번째 파라미터에는 현재 페이지를, 두 번째 파라미터에는 조회할 데이터 수를 입력한다. 여기에 추가로 정렬 정보도 파라미터로 추가할 수 있다. 페이지는 0부터 시작한다.
아래를 통해 반환 타입인 Page 인터페이스가 제공하는 다양한 메소드를 보자.
지금까지 설명한 Pageable과 Page를 사용하면 지루하고 반복적인 페이징 처리를 손쉽게 개발할 수 있다.
JPA 쿼리 힌트를 사용하려면 org.springframework.data.jpa.repository.QueryHints
어노테이션을 사용하면 된다. 참고로 이것은 SQL 힌트가 아니라 JPA 구현체에게 제공하는 힌트다.
@QueryHints(value = { @QueryHint(name = "org.hibernate.readOnly", value = "true") }, forCounting = true)
Page<Member> findByName(String name, Pageable pageable);
forCounting
속성은 반환 타입으로 Page
인터페이스를 적용하면 추가로 호출하는 페이징을 위한 count
쿼리에도 쿼리 힌트를 적용할지를 설정하는 옵션이다.
쿼리 시 락을 걸려면 org.springframework.data.jpa.repository.Lock
어노테이션을 사용하면 된다.
@Lock(LockModeType.PESSIMISTIC_WRITE)
List<Member> findByName(String name);
도메인 주도 설계는 명세라는 개념을 소개하는데, 스프링 데이터 JPA는 JPA Criteria로 이 개념을 사용할 수 있도록 지원한다.
명세를 이해하기 위한 핵심 단어는 술어인데 이것은 단순하게 참이나 거짓으로 평가된다. 이것은 AND, OR 같은 연산자로 조합할 수 있다. 예를 들어 데이터를 검색하기 위한 제약 조건 하나하나를 술어라고 할 수 있다. 이 술어를 스프링 데이터 JPA는 org.springframework.data.jpa.domain.Specification
클래스로 정의했다.
Specification
은 컴포지트 패턴으로 구성되어 있어서 여러 Specification
을 조합할 수 있다. 따라서 다양한 검색조건을 조립해서 새로운 검색조건을 쉽게 만들 수 있다.
명세 기능을 사용하려면 아래와 같이 리포지토리에서 org.springframework.data.jpa.repository.JpaSpecificationExecutor
인터페이스를 상속 받으면 된다.
public interface OrderRepository extends JpaRepository<Order, Long>, JpaSpecifiactionExecutor<Order> { }
public interface JpaSpecificationExecutor<T> {
T findOne(Specification<T> spec);
List<T> findAll(Specification<T> spec);
Page<T> findAll(Specification<T> spec, Pageable pageable);
List<T> findAll(Specification<T> spec, Sort sort);
long count(Specification<T> spec);
}
위의 JpaSpecificationExecutor
의 메소드들은 Specification
을 파라미터로 받아서 검색 조건으로 사용한다.
이제 명세를 사용하는 아래 예제를 보자. 우선 명세를 사용하는 코드를 보고나서 명세를 정의하는 코드를 보겠다.
import static org.springframework.data.jpa.domain.Specifications.*; // where()
import static jpabook.jpashop.domain.spec.OrderSpec.*;
public List<Order> findOrders(String name) {
List<Order> result = orderRepository.findAll(
where(memberName(name)).and(isOrderStatus())
);
return result;
}
Specifications
는 명세들을 조립할 수 있도록 도와주는 클래스인데 where(), and(), or(), not()
메소드를 제공한다.
findAll
을 보면 회원 이름 명세(memberName)와 주문 상태 명세(isOrderStatus)를 and
로 조합해서 검색 조건으로 사용한다.
참고로 명세 기능을 사용할 때 예제처럼 자바의 import static
을 적용하면 더 읽기 쉬운 코드가 된다.
이제 아래의 OrderSpec
명세를 정의하는 코드를 보자.
import org.springframework.data.jpa.domain.Specification;
import org.springframework.util.StringUtils;
import javax.persistence.criteria.*;
public class OrderSpec {
public static Specification<Order> memberName(final String memberName) {
return new Specification<Order>() {
@Override
public Predicate toPredicate(Root<Order> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
if(StringUtils.isEmpty(memberName)) return null;
Join<Order, Member> m = root.join("member", JoinType.INNER); // 회원과 조인
return builder.equal(m.get("name"), memberName);
}
};
}
public static Specification<Order> isOrderStatus() {
return new Specification<Order>() {
@Override
public Predicate toPredicate(Root<Order> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
return builder.equal(root.get("status"), OrderStatus.ORDER);
}
};
}
}
명세를 정의하려면 Specification
인터페이스를 구현하면 된다. 예제에서는 편의상 내부 무명 클래스를 사용했다. 명세를 정의할 때는 toPredicate(...)
메소드만 구현하면 되는데 JPA Crieria의 Root, CriteriaQuery, CriteriaBuilder 클래스가 모두 파라미터로 주어진다. 이 파라미터들을 활용해서 적절한 검색 조건을 반환하면 된다.
스프링 데이터 JPA로 리포지토리로 개발하면 인터페이스만 정의하고 구현체는 만들지 않는다. 하지만 다양한 이유로 메소드를 직접 구현해야 할 때도 있다. 그렇다고 리포지토리를 직접 구현하면 공통 인터페이스가 제공하는 기능까지 모두 구현해야 한다. 스프링 데이터 JPA는 이런 문제를 우회해서 필요한 메소드만 구현할 수 있는 방법을 제공한다.
먼저 직접 구현할 메소드를 위한 아래 예제와 같은 사용자 정의 인터페이스를 작성해야 한다. 이때 인터페이스 이름은 자유롭게 지으면 된다.
public interface MemberRepositoryCustom {
public List<Member> findMemberCustom();
}
다음으로 아래와 같은 사용자 정의 인터페이스를 구현한 클래스를 작성해야 한다. 이때 클래스 이름을 짓는 규칙이 있는데 리포지토리 인터페이스 이름 + Impl
로 지어야 한다. 이렇게 하면 스프링 데이터 JPA가 사용자 정의 구현 클래스로 인식한다.
public class MemberRepositoryImpl implements MemberRepositoryCustom {
@Override
public List<Member> findMemberCustom() {
... // 사용자 정의 구현
}
}
마지막으로 아래와 같이 리포지토리 인터페이스에서 사용자 정의 인터페이스를 상속받으면 된다.
public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom {}
만약 사용자 정의 구현 클래스 이름 끝에 Impl
대신 다른 이름을 붙이고 싶으면 repository-ipml-postfix
속성을 변경하면 된다.
스프링 데이터 프로젝트는 스프링 MVC에서 사용할 수 있는 편리한 기능을 제공한다.
식별자로 도메인 클래스를 바로 바인딩해주는 도메인 클래스 컨버터 기능과 페이징과 정렬 기능을 알아보자.
스프링 데이터가 제공하는 Web 확장 기능을 활성화하려면 org.springframework.data.web.config.SpringDataWebConfiguration
을 스프링 빈으로 등록하면 된다.
JavaConfig를 사용하면 다음과 같이 org.springframework.data.web.config.EnableSpringDataWebSupport
어노테이션을 사용하면 된다.
@Configuration
@EnableWebMvc
@EnableSpringDataWebSupport
public class WebAppConfig {
...
}
설정을 완료하면 도메인 클래스 컨버터와 페이징과 정렬을 위한 HandlerMethodArgumentResolver
가 스프링 빈으로 등록된다.
도메인 클래스 컨버터는 HTTP 파라미터로 넘어온 엔티티의 아이디로 엔티티 객체를 찾아서 바인딩해준다. 예를 들어 특정 회원을 수정하는 화면을 보여주려면 컨트롤러는 HTTP 요청으로 넘어온 회원의 아이디를 사용해서 리포지토리를 통해 회원 엔티티를 조회해야 한다. 다음과 같은 URL을 호출했다고 가정하자.
수정화면 요청 URL: /member/memberUpdateForm?id=1
@Controller
public class MemberController {
@Autowired MemberRepository memberRepository;
@RequestMapping("member/memberUpdateForm")
public String memberUpdateForm(@RequestParam("id") Long id, Model model) {
Member member = memberRepository.findOne(id); // 회원을 찾는다.
model.addAttribute("member", member);
return "member/memberSaveForm";
}
}
위 코드를 보면 컨트롤러에서 파라미터로 넘어온 회원 아이디로 회원 엔티티를 찾는다. 그리고 찾아온 회원 엔티티를 model
을 사용해서 뷰에 넘겨준다.
이번에는 도메인 클래스 컨버터를 적용한 예제를 보자.
@Controller
public class MemberController {
@RequestMapping("member/memberUpdateForm")
public String memberUpdateForm(@RequestParam("id") Member member, Model model) {
model.addAttribute("member", member);
return "member/memberSaveForm";
}
}
@RequestParam("id") Member member
부분을 보면 HTTP 요청으로 회원 아이디를 받지만 도메인 클래스 컨버터가 중간에 동작해서 아이디를 회원 엔티티 객체로 변환해서 넘겨준다. 따라서 컨트롤러를 단순하게 사용할 수 있다.
참고로 도메인 클래스 컨버터는 해당 엔티티와 관련된 리포지토리를 사용해서 엔티티를 찾는다. 여기서는 회원 리포지토리를 통해서 회원 아이디로 회원 엔티티를 찾는다.
스프링 데이터가 제공하는 페이징과 정렬 기능을 스프링 MVC에서 편리하게 사용할 수 있도록 HandlerMethodArgumentResolver
를 제공한다.
PageableHandlerMethodArgumentResolver
SortHandlerMethodArgumentResolver
바로 예제를 보자.
@RequestMapping(value = "/members", method = RequestMethod.GET)
public String list(Pageable pageable, Model model) {
Page<Member> page = memberService.findMembers(pageable);
model.addAttribute("members", page.getContent());
return "members/memberList";
}
파라미터로 Pageable
을 받은 것을 확인할 수 있다. Pageable
은 다음 요청 파라미터 정보로 만들어진다. 요청 파라미터는 다음과 같다.
사용해야 할 페이징 정보가 둘 이상이면 접두사를 사용해서 구분할 수 있다. 접두사는 스프링 프레임워크가 제공하는 @Qualifier
어노테이션을 사용한다. 그리고 "{접두사명}_"
로 구분한다.
public String list(@Qualifier("member") Pageable memberPageable, @Qualifier("order") Pageable orderPageable, ...
예 /members?member_page=0&order_page=1
Pageable
의 기본값은 page=0, size=20
이다. 만약 기본값을 변경하고 싶으면 @PageableDefault
어노테이션을 사용하면 된다.
@RequestMapping(value = "/members", method = RequestMethod.GET)
public String list(@PageableDefault(size = 12, sort = "name", direction = Sort.Direction.DESC) Pageable pageable) { ... }
스프링 데이터 JPA가 제공하는 공통 인터페이스는 org.springframework.data.jpa.repository.support.SimpleJpaRepository
클래스가 구현한다. 아래 예제를 통해 코드 일부를 분석해보자.
@Repository
@Transactional(readOnly = true)
public class SimpleJpaRepository<T, ID extends Serializable> implements JpaRepository<T, ID>, JpaSpecificationExecutor<T> {
@Transactional
public <S extends T> S save(S entity) {
if(entityInformation.isNew(entity)) {
em.persist(entity);
return entity;
} else {
return em.merge(entity);
}
}
...
}
readOnly = true
옵션이 적용되어 있다. 데이터를 변경하지 않는 트랜잭션에셔 이 옵션을 사용하면 플러시를 생략해서 약간의 성능 향상을 얻을 수 있다.첫 번째 방법은 다음처럼 리포지토리에서 QueryDslPredicateExecutor를 상속받으면 된다.
public interface ItemRepository extends JpaRepository<Item, Long>, QueryDslPredicateExecutor<Item> {}
이제 상품 리포지토리에서 QueryDSL을 사용할 수 있다.
아래 예제는 QueryDSL이 생성한 쿼리 타입으로 장난감이라는 이름을 포함하고 있으면서 가격이 10000~20000원인 상품을 검색한다.
QItem item = QItem.item;
Iterable<Item> result = itemRepository.findAll(
item.name.contains("장난감").and(item.price.between(10000, 20000))
);
예제의 QueryDslPredicateExecutor 인터페이스를 보면 QueryDSL을 검색조건으로 사용하면서 스프링 데이터 JPA가 제공하는 페이징과 정렬 기능도 함께 사용할 수 있다.
public interface QueryDslPredicateExecutor<T> {
T findOne(Predicate predicate);
Iterable<T> findAll(Predicate predicate);
Iterable<T> findAll(Predicate predicate, OrderSpecifier<?>... orders);
Page<T> findAll(Predicate predicate, Pageable pageable);
long count(Predicate predicate);
}
지금까지 살펴본 QueryDslPredicateExecutor는 스프링 데이터 JPA에서 편리하게 QueryDSL을 사용할 수 있지만 기능에 한계가 있다. 예를 들어 join, fetch를 사용할 수 없다. 따라서 QueryDSL이 제공하는 다양한 기능을 사용하려면 JPAQuery를 직접 사용하거나 스프링 데이터 JPA가 제공하는 QueryDslRepositorySupport를 사용해야 한다.
QueryDSL의 모든 기능을 사용하려면 JPAQuery 객체를 직접 생성해서 사용하면 된다. 이때 스프링 데이터 JPA가 제공하는 QueryDslRepositorySupport를 상속받아 사용하면 조금 더 편리하게 QueryDSL을 사용할 수 있다.
public interface CustomOrderRepository {
public List<Order> search(OrderSearch orderSearch);
}
스프링 데이터 JPA가 제공하는 공통 인터페이스는 직접 구현할 수 없기 때문에 예제에 CustomOrderRepository라는 사용자 정의 리포지토리를 만들었다. 이제 예제를 통해 QueryDslRepositorySupport를 사용하는 코드를 보자.
public class OrderRepositoryImpl extends QueryDslRepositorySupport implements CustomOrderRepository {
public OrderRepositoryImpl() {
super(Order.class);
}
@Override
public List<Order> search(OrderSearch orderSearch) {
QOrder order = QOrder.order;
QMember member = QMember.member;
JPQLQuery query = from(order);
if(StringUtils.hasText(orderSearch.getMemberName())) {
query.leftJoin(order.member, member).where(member.name.contains(orderSearch.getMemberName()));
}
if(orderSearch.getOrderStatus() != null) {
query.where(order.status.eq(orderSerach.getOrderStatus()));
}
return query.list(order);
}
}
저자는 스프링 프레임워크와 JPA를 함께 사용한다면 스프링 데이터 JPA는 선택이 아닌 필수라 생각한다고 했다. 필자도 이번 장을 학습하며 필수라는 생각이 들었다.