12장. 스프링 데이터 JPA
12.1 스프링 데이터 JPA 소개
- 스프링 데이터 JPA는 스프링 프레임워크에서 JPA를 편리하게 사용할 수 있도록 지원하는 프로젝트다.
- 이 프로젝트는 데이터 접근 계층을 개발할 때 지루하게 반복되는 CRUD 문제를 세련된 방법으로 해결한다.
- 우선 CRUD를 처리하기 위한 공통 인터페이스를 제공한다.
- 리포지토리를 개발할 때 인터페이스만 작성하면 실행 시점에 스프링 데이터 JPA가 구현 객체를 동적으로 생성해서 주입해준다.
- CRUD를 처리하기 위한 공통 메소드는 스프링 데이터 JPA가 제공하는 `org.springframework.data.jpa.repository.JpaRepository인터페이스에 있다.
예제 12.2 스프링 데이터 JPA 적용
public interface MemberRepository extends JPARepository<Member, Long>{
Member findByUsername(String username);
}
public interface ItemRepository extends JpaRepository<Item, Long> {}
12.1.1 스프링 데이터 프로젝트
- JPA는 스프링 데이터 프로젝트의 하위 프로젝트 중 하나다.
- 스프링 데이터(Spring Data) 프로젝트는 JPA, 몸고DB, ... 다양한 데이터 저장소에 대한 접근을 추상화해서 개발자의 편의를 제공
그림 12.2 스프링 데이터 연관 프로젝트
12.2 스프링 데이터 JPA 설정
- JPA 환경 설정
예제 12.5 javaConfig 설정
@Configuration
@EnableJpaRepositories(basePackages = "org.example.repository")
public class AppConfig {
}
- 스프링 데이터 JPA는 애플리케이션을 실행할 때 basePackage에 있는 리포지토리
- 인터페이스들을 찾아서 해당 인터페이스를 구현한 클래스를 동적으로 생성한 다음 스프링 빈으로 등록한다.
- 개발자가 직접 구현 클래스를 만들지 않아도 된다.
그림 12.3 구현 클래스 생성
12.3 공통 인터페이스 기능
- 스프링 데이터 JPA는 간단한 CRUD 기능을 공통으로 처리하는 예제 12.6의 JpaRepository 인터페이스를 제공한다.
- 스프링 데이터 JPA를 사용하는 가장 단순한 방법은
- 예제 12.7과 같이 이 인터페이스를 상속받는 것이다.
- 그리고 제네릭에 엔티티 클래스와 엔티티 클래스가 사용하는 식별자 타입을 지정하면 된다.
예제 12.6 JpaRepository 공통 기능 인터페이스
public interface JpaRepository<T, ID extends Serializable> extends PagingAndSortingRepository<T, ID>{}
예제 12.7 JpaRepository를 사용하는 인터페이스
public interface MemberRepository extends JpaRepository<Member, Long> {}
- 예제 12.7의 상속받은 JpaRepository<Member, Long> 부분을 보면 제네릭에 회원 엔티티와 회원 엔티티의 식별자 타입을 지정했다.
- 회원 리포지토리는 JpaRepository 인터페이스가 제공하는 다양한 기능을 사용할 수 있다.
- 그림 12.4를 보면 윗부분에 스프링 데이터 모듈이 있고 그 안에 Repository, CrudRepository, PagingAndSortingRepositry가 있는데
- 이것은 스프링 데이터 프로젝트가 공통으로 상요하는 인터페이스다.
- 스프링 데이터 JPA가 제공하는 JpaRepository 인터페이스는 여기에 추가로 JPA에 특화된 기능을 제공한다.
- JpaRepository 인터페이스를 상속받으면 사용할 수 있는 주요 메소드 몇 가지를 간단히 소개 하겠다.
- 참고로 T는 엔티티, ID는 엔티티의 식별자 타입, S는 엔티티와 그 자식 타입을 뜻한다.
메서드 | 설명 |
---|---|
Save(S) | 새로운 엔티티는 저장하고 이미 있는 엔티티는 수정한다. |
Delete (T) | 엔티티 하나를 삭제한다. 내부에서 EntityManager.remove()를 호출한다. |
findOne (ID) | 엔티티 하나를 조회한다. 내부에서 EntityManager.find()를 호출한다. |
getOne (ID) | 엔티티를 프록시로 조회한다. 내부에서 EntityManager.getReference()를 호출한다. |
findAll (...) | 모든 엔티티를 조회한다. 정렬(sort)이나 페이징(Pageable) 조건을 파라미터로 제공할 수 있다. |
- save (S) 메소드는 엔티티에 식별자 값이 없으면 (null 이면) 새로운 엔티티로 판단해서 EntityManager.persist()를 호출하고
- 식별자 값이 있으면 이미 있는 엔티티로 판단해서 EntityManager.merge()를 호출한다.
- 필요하다면 스프링 데이터JPA의 기능을 확장해서 신규 엔티티 판단 전략을 변경할 수 있다.
12.4 쿼리 메소드 기능
- 쿼리 메소드 기능은 스프링 데잍 JPA가 제공하는 마법 같은 기능이다.
- 메소드 이름으로 쿼리 생성
- 메소드 이름으로 JPA NamedQuery 호출
@Query
어노테이션을 사용해서 리포지토리 인터페이스에 쿼리 직접 정의
12.4.1 메소드 이름으로 쿼리 생성
- 이메일과 이름으로 회원을 조회하려면 다음과 같은 메소드 정의
public interface MemberRepository extends Repository<Member, Long> {
List<Member> findByEmailAndName(String email, String name);
}
JPQL 키워드와 Spring Data JPA 메서드
JPQL 키워드 | Spring Data JPA 메서드 | 설명 |
---|---|---|
And |
findByLastnameAndFirstname |
lastname = ?1 와 firstname = ?2 를 사용하여 검색 |
Or |
findByLastnameOrFirstname |
lastname = ?1 또는 firstname = ?2 를 사용하여 검색 |
Is , Equals |
findByFirstname |
firstname = ?1 을 사용하여 검색 |
Between |
findByStartDateBetween |
startDate 가 ?1 과 ?2 사이에 있는 항목 검색 |
LessThan |
findByAgeLessThan |
age < ?1 인 항목 검색 |
LessThanEqual |
findByAgeLessThanEqual |
age <= ?1 인 항목 검색 |
GreaterThan |
findByAgeGreaterThan |
age > ?1 인 항목 검색 |
GreaterThanEqual |
findByAgeGreaterThanEqual |
age >= ?1 인 항목 검색 |
After |
findByStartDateAfter |
startDate > ?1 인 항목 검색 |
Before |
findByStartDateBefore |
startDate < ?1 인 항목 검색 |
IsNull |
findByAgeIsNull |
age 가 null 인 항목 검색 |
IsNotNull , NotNull |
findByAgeIsNotNull |
age 가 null 이 아닌 항목 검색 |
Like |
findByFirstnameLike |
firstname 이 ?1 을 포함하는 항목 검색 |
NotLike |
findByFirstnameNotLike |
firstname 이 ?1 을 포함하지 않는 항목 검색 |
StartingWith |
findByFirstnameStartingWith |
firstname 이 ?1 로 시작하는 항목 검색 |
EndingWith |
findByFirstnameEndingWith |
firstname 이 ?1 로 끝나는 항목 검색 |
Containing |
findByFirstnameContaining |
firstname 이 ?1 을 포함하는 항목 검색 |
OrderBy |
findByAgeOrderByLastnameDesc |
age = ?1 로 필터링한 결과를 lastname 내림차순으로 정렬 |
Not |
findByLastnameNot |
lastname 이 ?1 이 아닌 항목 검색 |
In |
findByAgeIn(Collection ages) |
age 가 주어진 컬렉션 ages 내에 있는 항목 검색 |
NotIn |
findByAgeNotIn(Collection age) |
age 가 주어진 컬렉션 age 내에 없는 항목 검색 |
JPQL 메서드 | JPQL 쿼리 | 설명 |
---|---|---|
TRUE findByActiveTrue() |
WHERE x.active = true |
active 가 true 인 엔터티를 검색하는 메서드 |
FALSE findByActiveFalse() |
WHERE x.active = false |
active 가 false 인 엔터티를 검색하는 메서드 |
IgnoreCase findByFirstnameIgnoreCase |
WHERE UPPER(x.firstname) = UPPER(?1) |
대소문자를 무시하고 firstname 과 주어진 값과 일치하는 엔터티를 검색하는 메서드 |
12.4.2 JPA NamedQuery
- 스프링 데이터 JPA는 메소드 이름으로 JPA Named 퀄리를 호출하는 기능을 한다.
- JPA Named 쿼리는 이름 그대로 쿼리에 이름을 부여해서 상요하는 방법인데
- 예지 12.8과 같이 어노테이션이나 예제 12.9와 같이 XML에 쿼리를 정의 할수 있다.
- 상세내용은 10.2.15절 참고
예제 12.8 @NamedQuery 어노테이션으로 Named 쿼리 정의
@Entity
@NamedQuery(
name="Member.findByUsername",
query="select m from Member m where m.username = "username")
public class Member{...}
예제 12.9 orm.xml의 XML 사용
<named-query name="Member.findByUsername">
<query><CDATA[
select m
from Member m
where m.username = :username
]></query>
</named-query>
예제 12.10 JPA를 직겆ㅂ 사용해서 Named 쿼리 호출
public class MemberRepository {
public List<Member> findByUsername (String username){
...
List<Member> resultList =
em.createNamedQuery("Member.findByUsername", Member.class)
.setParameter("username", "회원1")
.getResultList();
}
}
예제 12.11 스프링 데이터 JPA로 Named 쿼리 호출
public interface MemberRepository extends JpaRepository<Member, Long> {
List<Member> findByUsername(@Param("username") String username)
}
- 스프링 데이터 JPA는 선언한 "도메인 클래스 + .(점) + 메소드 이름"으로 Named쿼리를 찾아서 실행한다.
- Member.findByUsername이라는 Named쿼리를 실행한다.
- 실행할 Named 쿼리가 없으면 메소드 이름으로 쿼리 생성 전략을 사용한다.
- 예제 12.11에서 findByUsername() 메소드의 파라미터에
@Param
을 사용했는데 이것은 이름기반 파라미터를 바인딩할 때 사용하는 어노테이션이다.
12.4.3 @Query, 리포지토리 메소드에 쿼리 정의
- 리포지토리 메소드에 직접 쿼리를 정의 하려면
- 예제 12.12와 같이
@org.springframework.data.jpa.repository.query
어노테이션 사용 - JPA Named 쿼리처럼 애플리케이션 실행 시점에 문법 오류를 발견할 수 있는 장점이 있다.
예제 12.12 메소드에 JPQL 쿼리 작성
public interface MemberRepository extends JpaRepository<Member, Long> {
@Query("select m from Member m where m.username = ?1")
Member findByUsername(String username);
}
예제 12.13와 같이 @Query 어노테이션에 nativeQuery = true
를 설정한다.
참고로 스프링 데이터 JPA가 지원하는 파라미터 바인딩을 사용하면 JPQL은 위치 기반 파라미터를 1부터 시작하지만
네이티브 SQL은 예제 12.13과 같이 0부터 시작한다.
public interface MemberRepository extends JpaRepository<Member, Long> {
@Query(value = "SELECT * FROM MEMBER WHERE USERNAME = ?0",
nativeQuery = true)
Member findByUsername(String username);
}
12.4.4 파라미터 바인딩
- 스프링 데이터 JPA는 위치 기반 파라미터 바인딩과 이름기반 파라미터 바인딩을 모두 지원한다.
select m from Member m where m.username = ?1 // 위치 기반
select m from Member m where m.username = :name // 이름 기반
- 기본값은 위치 기반인데 파라미터 순서로 바인딩한다.
- 이름 기반 파라미터 바인딩을 사용하려면
- org.springframework.data.repository.query.Param(파라미터 이름) 어노테이션을 사용하면 된다.
- 코드 가독성과 유지보수를 위해 이름 기반 파라미터 바인딩을 사용하자.
예제 12.14 파라미터 바인딩
import org.springframework.data.repository.query.Param
public interface MemberRepository extends JpaRepository<Member, Long> {
@Query("select m from Member m where m.username = :name")
// 이름 기반 파라미터 바인딩은 @Param을 사용한다.
Member findByUsername(@Param("name") String username);
}
12.4.5 벌크성 수정 쿼리
예제 12.15를 통해 JPA로 작성한 벌크성 수정 쿼리부터 보자.
예제 12.16 스프링 데이터 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 옵션을 ture로 설정하면 된다. 참고로 이 옵션의 기본값은 false다.
12.4.6 반환 타입
- 스프링 데이터 JPA는 유연한 반환 타입을 지원하는데 결과가 한 건 이상이면 컬렉션 인터페이스를 사용하고
- 단건이면 반환 타입을 지정한다.
List<Member> findByName(String name); // 컬렉션
Member findByEmail(String email); // 단건
- 만약 조회 결과가 없으면 컬렉션은 빈 컬렉션을 반환하고 단건은 null을 반환한다.
- 그리고 단건을 기대하고 반환 타입을 저정했는데 결과가 2건 이상 조회되면
javax.persistence.NonUniqueResultExceptin
예외가 발생한다. - 참고로 단건으로 지정한 메소드를 호출하면 스프링 데이터 JPA는 내부에서 JPQL의 Query.getSingleResult() 메소드를 호출한다.
- 조회 결과가 없으면
javax.persistence.NoResultException
예외가 발생하는데 개발자 입장에서 다루기가 상당히 불편하다. - 스프링 데이터 JPA는 단건을 조회할 때 이 예외가 발생하면 예외를 무시하고 대신에
null
을 반환한다.
- 조회 결과가 없으면
12.4.7 페이징과 정렬
- 스프링 데이터 JPA는 쿼리 메소드에 페이징과 정렬 기능을 사용할 수 있도록 2가지 특별한 파라미터를 제공한다.
org.springframework.data.domain.Sort
: 정렬 기능org.springframework.data.domain.Pageable
: 페이징 기능 (내부에 Sort 포함)
예제 12.17과 같이 파라미터에 Pageable을 사용하면 반환 타입으로 List나 org.springframework.data.domain.Page를 사용할 수 있다.
반환 타입으로 Page를 사용하면 스프링 데이터 JPA는 페이징 기능을 제공하기 위해 검색된 전체 데이터 건수를 조회하는 count 쿼리를 추가로 호출한다.
예제 12.17 페이징과 정렬 사용 예제
// count 쿼리 사용
Page<Member> findByName(String name, Pageable pageable);
// count 쿼리 사용 안 함
List<Member> findByName(String name, Pageable pageable);
List<Member> findByName(String name, Sort sort);
- 다음 조건으로 페이징과 정렬을 사용하는 예제 코드를 보자.
- 검색 조건: 이름이 김으로 시작하는 회원
- 정렬 조건: 이름으로 내림차순
- 페이징 조건: 첫 번째 페이지, 페이지당 보여줄 데이터는 10건
예제 12.18 Page 사용 예제 정의 코드
public interface MemberRepository extends Repository<Member, Long>{
Page<Member> findByNameStartingWith(String name, Pageable Pageable);
}
예제 12.19 Page tㅏ용 예제 실행 코드
// 페이징 조건과 정렬 조건 설정
PageRequest pageRequest =
new PageRequest ( 0, 10, new Sort(Direction.DESC, "name"));
Page<Member> result =
memberrepository.findByNameStartingWith("김", pageRequest);
List<Member> members = result.getContent(); // 조회된 데이터
int totalPages = result.getTotalPages(); // 전체 페이지수
boolean hasNextPage = result.hasNextPage(); // 다음 페이지 존재 여부
두 번째 파라미터로 받은 Pagable은 인터페이스다.
따라서 실제 사용할 때는 예제 12.19와 같이 해당 인터페이스를 구현함 org.springframework.data.domain.PageRequest
객체를 사용한다.
PageRequest 생성자의 첫 번째 파라미터에는 현재 페이지를,
두 번째 파라미터에는 조회할 데이터 수를 입력한다. 여기에 추가로 정렬 정보도 파라미터로 사용할 수 있다. 참고로 페이지는 0부터 시작한다.
예제 12.20을 통해 반환 타입인 Page 인터페이스가 제공하는 다양한 메소드를 보자.
예제 12.20 Page 인터페이스
- 페이지 정보 관련 메서드:
getNumber()
: int (현재 페이지 번호)getSize()
: int (페이지 크기)getTotalPages()
: int (전체 페이지 수)getNumberOfElements()
: int (현재 페이지에 포함된 요소 수)getTotalElements()
: long (전체 요소 수)
- 페이지 위치 관련 메서드:
hasPreviousPage()
: boolean (이전 페이지 존재 여부)isFirstPage()
: boolean (현재 페이지가 첫 페이지인지 여부)hasNextPage()
: boolean (다음 페이지 존재 여부)isLastPage()
: boolean (현재 페이지가 마지막 페이지인지 여부)
- 페이지 이동 관련 메서드:
nextPageable()
: Pageable 또는 null (다음 페이지의 페이지 설정 정보)previousPageable()
: Pageable 또는 null (이전 페이지의 페이지 설정 정보)
- 조회된 데이터 관련 메서드:
getContent()
: List (현재 페이지에 포함된 요소 리스트)hasContent()
: boolean (현재 페이지에 내용이 있는지 여부)
- 정렬 정보 관련 메서드:
getSort()
: Sort (페이지 요소의 정렬 정보)
Pageable과 Page를 사용하면 지루하고 반복적인 페이징 처리를 손쉽게 개발할 수 있다.
12.4.8 힌트
- JPA 쿼리 힌트를 사용하려면
org.springframework.data.jpa.repository.QueryHints
어노테이션을 사용하면 된다. - 참고로 이것은 SQL 힌트가 아니라 JPA 구현체에게 제공하는 힌트이다.
@QueryHints(value =
@QueryHint (name = "org.hibernate.readOnly",
value = "true",
forCounting = true )
)
- forCounting 속성은 반환 타입으로 Page 인터페이스를 적용하면 추가로 호출하는 페이징을 위한 count 쿼리에도 쿼리 힌트를 적용할지를 설정하는 옵션이다. (기본값 true)
12.4.9 LOCK
- 쿼리 시 락을 걸려면
org.springframework.data.jpa.repository.Lock
어노테이션을 사용하면 된다. - JPA가 제공하는 락에 대해서는 16.1절을 참고하자
@Lock(LockModeType.PESSIMISTIC_WRITE)
List<Member> findByName(String name);
12.5 명세
- 책 도메인 주도 설계(Domain Driven Design)는 명세(SPECIFICATION)라는 개념을 소개하는데
- 스프링 데이터 JPA는 JPA Criteria로 이개념을 사용할 수 있도록 지원한다.
- 명세(Specification)를 이해하기 위한 핵심 단어는 술어(predicate)인데 이것은 단순히 참이나 거짓으로 평가된다.
- 그리고 이것은 AND, OR같은 연산자로 조합할 수 있다.
- 예를 들어 데이터를 검색하기 위한 제약 조건 하나하나를 술어라 할 수 있다.
- 이 술어를 스프링 데이터 JPA는 org.springframework.data.jpa.domain.Specification 클래스로 정의 해싿.
Specification은 컴포지트 패턴(en.wikipedia.org/wiki/Composite_pattern)으로 구성되어 있어서 여러 Specification을 조합할 수 있다.
따라서 다양한 검색조건을 조립해서 새로운 검색 조건을 쉽게 만들 수 있다.
따라서 다양한 검색조건을 조립해서 새로운 검색조건을 쉽게 만들 수 있다.
명세 기능을 사용하려면 예제 12.21과 같이 리포지토리에서 org.springframework.data.jpa.repository.JpaSpecificationExecutor 인터페이스를 상속받으면 된다.
예제 12.21 JpaSpecificationExecutor 상속
public interface OrderRepository extends JpaRepository<Order, Long>,
JpaSpecificationExecutor<Order>{...}
예제 12.22 JpaSpecificationExecutor 인터페이스
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);
}
- 예제 12.22의 JpaSpecificationExecutor의 메소드들은 Specification을 파라미터로 받아서 검색 조건으로 사용한다.
- 이제 명세를 사용하는 예제 12.23을 보자. 우선 명세를 사용하는 코드를 보고나서 명세를 정의하는 코드를 보겠다.
예제 12.23 명세 사용코드
import static org.springframework.data.jpa.domain.Specification.*;
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;
}
- Specification는 명세들을 조립할 수 있도록 도와주는 클래스인데
where(), and(), or(), not()
메소드를 제공한다. - findAll을 보면 회원 이름 명세(memberName)와 주문 상태 명세(isOrderStatus)를 and로 조합해서 검색 조건으로 사용한다.
- 이제 예제 12.24의 OrderSpec 명세를 정의하는 코드를 보자.
예제 12.24 OrderSpec 명세 정의 코드
package jpabook.jpashop.domain;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.util.StringUtils;import javax.persistence.criteria.*;
public class OrderSpec {
public static Specification<Order> memberNameLike(final String memberName) {
return new Specification<Order>() {
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.like(m.<String>get("name"), "%" + memberName + "%");
}
};
}
public static Specification<Order> isOrderStatus(final OrderStatus orderStatus) {
return new Specification<Order>() {
public Predicate toPredicate(Root<Order> root,
CriteriaQuery<?> query, CriteriaBuilder builder) {
if (orderStatus == null) return null;
return builder.equal(root.get("status"), orderStatus);
}
};
}
}
- 명세를 정의하려면
Specification
인터페이스를 구현하면 된다. - 예제에서는 편의상 내부 무명 클래스를 사용했다.
- 명세를 정의 할 때는 toPredicate(...) 메소드만 구현하면 되는데
JPA Critera
의 Root, CriteriaQuery, CriteriaBuilder클래스가 모두 파라미터로 주어진다.- 이 파라미터들을 활용해서 적절한 검색 조건을 반환하면 된다.
- JPA Criteria에 대한 이해가 부족하다면 10.3절을 참고하자.
12.6 사용자 정의 리포지토리 구현 // 리포지토리 커스텀
- 스프링 데이터 JPA로 리포지토리를 계발하면 인터페이스만 정의하고 구현체는 만들지 않는다.
- 하지만 다양한 이유로 메소드를 직접 구현해야 할 때도 있다.
- 그렇다고 리포지토리를 직접 구현하면 공통 인터페이스가 제공하는 기능까지 모두 구현해야 한다.
- 스프링 데이터 JPA는 이런 문제를 우회해서 필요한 메소드만 구현할 수 있는 방법을 제공한다.
- 먼저 직접 구현할 메소드를 위한 예제 12.25와 같은 사용자 정의 인터페이스를 작성해야 한다.
- 이때 인터페이스 이름은 자유롭게 지으면 된다.
예제 12.25 사용자 정의 인터페이스
public interface MemberRepositoryCustom {
public List<Member> findMemberCustom();
}
- 다음으로 예제 12.26과 같은 사용자 정의 인터페이스를 구현한 클래스를 작성해야 한다.
- 이때 클래스 이름을 짓는 규칙이 있는데 리포지토리 인터페이스 이름 + Impl로 지어야 한다.
- 이렇게 하면 스프링 데이터 JPA가 사용자 정의 구현 클래스로 인식한다.
예제 12.26 사용자 정의 구현 클래스
public class MemberRepositoryImpl implements MemberRepositoryCustom {
@Override
public List<Member> findMemberCustom(){
... // 사용자 정의 구현
}
}
- 마지막으로 예제 12.27과 같이 리포지토리 인터페이스에서 사용자 정의 인터페이스를 상속받으면 된다.
예제 12.27 사용자 정의 인터페이스 상속
public interface MemberRepository
extends JpaRepository<Member, Long>, MemberRepositoryCustom{...}
- 만약 사용자 정의 구현 클래스 이름 끝에 Impl 대신 다른 이름을 붙이고 싶으면
repository-impl-postfix
속성을 변경하면 된다. 참고로 Impl이 기본값이다.
Xml 설정
<repositories base-package="jpabook.jpashop.repository" repository-impl-postfix="impl" />
JavaConfig 설정
@EnableJpaRepositories(basePackages = "jpabook.jpashop.repository", repositoryImplementationPostfix = "impl")
12.8 스프링 데이터 JPA가 사용하는 구현체
예제 12.31 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 (entityInformaion.isNew(entity)) {
em.persist(entity);
return entity;
} else {
return em.merge(entity);
}
}
// 나머지 코드 생략
}
- @Repository 적용: JPA 예외를 스프링이 추상화한 예외로 변환한다.
- @Transactional 트랜잭션 적용: JPA의 모든 변경은 트랜잭션 안에서 이루어져야 한다. 스프링 데이터 JPA가 제공하는 공통 인터페이스를 사용하면 데이터를 변경(등록, 수정, 삭제)하는 메소드에 @Transactional로 트랜잭션 처리가 되어 있 다. 따라서 서비스 계층에서 트랜잭션을 시작하지 않으면 리포지토리에서 트랜잭션을 시작한다. 물론 서비스 계층에서 트랜잭션을 시작했으면 리포지토리도 해당 트랜잭션을 전파받아서 그대로 사용한다.
- @Transactional (readOnly = true): 데이터를 조회하는 메소드에는 readOnly = true 옵션이 적용되어 있다. 데이터를 변경하지 않는 트랜잭션에서 readOnly = true 옵션을 사용하면 플러시를 생략해서 약간의 성능 향상을 얻을 수 있는 데, 자세한 내용은 15.4.2절에서 설명하겠다.
- save() 메소드: 이 메소드는 저장할 엔티티가 새로운 엔티티면 저장(persist)하고 이미 있는 엔티티면 병합(merge)한다. 새로운 엔티티를 판단하는 기본 전략은 엔티티의 식별자로 판단하는데 식별자가 객체일 때 null, 자바 기본 타입일 때 숫자 0 값이면 새로운 엔티티로 판단한다.
필요하면 엔티티에 예제 12.32의 Persistable 인터페이스를 구현해서 판단 로직을 변경할 수 있다.
예제 12.32 Persistable
public interface Persistable<ID extends Serializable> extends Serializable {
ID getId();
boolean isNew();
}
12.9 JPA 샵에 적용
12.9.3 명세 적용
- 명세(Specification)로 검색하는 기능을 사용하려면 리포지토리에
org.springframework.data.jpa.repository.JpaSpecificationExecutor
을 추가로 상속
예제 12.41 OrderSpec 추가
package jpabook.jpashop.domain;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.util.StringUtils;
import javax.persistence.criteria.*;
public class OrderSpec {
public static Specification<Order> memberNameLike(final String memberName) {
return new Specification<Order>() {
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.like(m.<String>get("name"), "%" + memberName + "%");
}
};
}
public static Specification<Order> orderStatusEq(final OrderStatus orderStatus) {
return new Specification<Order>() {
public Predicate toPredicate(Root<Order> root,
CriteriaQuery<?> query, CriteriaBuilder builder) {
if (orderStatus == null) return null;
return builder.equal(root.get("status"), orderStatus);
}
};
}
}
- 검색조건을 가지고 있는 예제 12.42의 OrderSearch 객체에 자신이 가진 검색 조건으로
Specification
을 생성하도록 코드를 추가하자.
예제 12.42 검색 객체가 Specification 생성하도록 추가
package jpabook.jpashop.domain;
import org.springframework.data.jpa.domain.Specifications;
import static jpabook.jpashop.domain.OrderSpec.memberNameLike;
import static jpabook.jpashop.domain.OrderSpec.orderStatusEq;
import static org.springframework.data.jpa.domain.Specifications.where;
public class OrderSearch {
private String memberName; // 회원명 sibet HeC OneL
private OrderStatus orderStatus; // 주문 상태
// Getter, Setter
// 추가
public Specifications<Order> toSpecification() {
return where(memberNameLike(memberName))
.and(orderStatusEq(orderStatus));
}
}
예제 12.43, 예제 12.44 리팩토링 전, 후
// 예제 12.43 OrderService, findOrders 리팩토링 전
public List<Order> findOrders(OrderSearch orderSearch) {
return orderRepository.findAll(orderSearch);
}
// 예제 12.44 OrderService, findOrders 리팩토링 후
public List<Order> findOrders(OrderSearch orderSearch) {
return orderRepository.findAll(orderSearch.toSpecification());
}
12.10 스프링 데이터 JPA와 QueryDSL 통합
- 스프링 데이터 JPA에 추가로 QueryDSL을 사용해보자.
■ org.springframework.data.querydsl.QueryDslPredicateExecutor
■ org.springframework.data.querydsl.QueryDslRepositorySupport
12.10.1 QueryDslPredicateExecutor 사용
- 첫 번째 방법은 다음처럼 리포지토리에서 QueryDslPredicateExecutor를 상속받 으면 된다.
public interface ItemRepository
extends JpaRepository<Item, Long>, QueryDslPredicateExecutor<Item> {
}
- 이제 상품 리포지토리에서 QueryDSI 울 사용할 수 있다.
- 예제 12.45는 QueryDSL이 생성한 쿼리 타입으로 장난감이라는 이름을 포함하 고 있으면서 가격이 10000~20000원인 상품을 검색한다.
예제 12.45 QueryDSL 사용 예제
QItem item = QItem. item;
Iterable<Item> result = itemRepository .findAll (
item.name.contains ("장난감") . and (item.price.between (10000, 20000) )
);
예제 12.46의 QueryDslPredicateExecutor 인터페이스를 보면 QueryDSL을 검색조건으로 사용하면서 스프링 데이터 JPA가 제공하는 페이징과 정렬 기능도 함 께 사용할 수 있다.
예제 12.46 QueryDslPredicateExecutor 인터페이스
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);
}
예제 12.47 CustomOrderRepository 사용자 정의 리포지토리
// 예제 12.47 CustomOrderRepository 사용자 정의 리포지토리
package jpabook.jpashop.repository.custom;
import jpabook.jpashop.domain.Order;
import jpabook.jpashop.domain.OrderSearch;
import java.util.List;
public interface CustomOrderRepository {
List<Order> search(OrderSearch orderSearch);
}
12.10.2 QueryDsIRepositorySupport 사용
QueryDSL의 모든 기능을 사용하려면 JPAQuery 객체를 직접 생성해서 사용하면 된다. 이때 스프링 데이터 JPA가 제공하는 QueryDsIRepositorySupport를 상속 받아 사용하면 조금 더 편리하게 QueryDSL을 사용할 수 있다.
예제 12.47을 통해 사용 예제를 보자.
예제 12.47 CustomOrderRepository 사용자 정의 리포지토리
package jpabook. jpashop. repository. custom;
import jpabook. jpashop. domain.Order;
import jpabook. jpashop.domain. OrderSearch;
import java. util.List;
public interface CustomOrderRepository {
public List<Order> search(OrderSearch orderSearch);
}
- 스프링 데이터 JPA가 제공하는 공통 인터페이스는 직접 구현할 수 없기 때문에
- 예제 12.47에서 customOrderRepository라는 사용자 정의 리포지토리를 만들었다.
- 이제 예제 12.16을 통해 QueryDslRepositorySupport를 사용하는 코드를 보자.
예제 12.48 QueryDslRepositorySupport 사용 코드
// 예제 12.48 QueryDslRepositorySupport 사용 코드
package jpabook.jpashop.repository.custom;
import com.mysema.query.jpa.JPQLQuery;
import jpabook.jpashop.domain.Order;
import jpabook.jpashop.domain.OrderSearch;
import jpabook.jpashop.domain.QOrder;
import jpabook.jpashop.domain.QMember;
import org.springframework.data.jpa.repository.support.QueryDslRepositorySupport;
import org.springframework.util.StringUtils;
import java.util.List;
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(orderSearch.getOrderStatus()));
}
return query.list(order);
}
}
- 예제 12.48은 웹 애플리케이션 만들기에서 사용했던 주문 내역 검색 기 능을 QueryDsIRepositorysupport를 사용해서
- QueryDSL로 구현한 예제다.
- 검색 조건에 따라 동적으로 퀴리를 생성한다.
- 참고로 생성자에서 QueryDsIRepositorysupport에 엔티티 클래스 정보를 넘겨주어야 한다.
- 예제 12.49는 QueryDsIRepositorysupport의 핵심 기능만 간추러보았다.
package org.springframework.data.jpa.repository.support;
import org.springframework.stereotype.Repository;
import javax.persistence.EntityManager;
@Repository
public abstract class QueryDslRepositorySupport {
protected EntityManager entityManager;
protected Querydsl querydsl;
// 엔티티 매니저 반환
protected QueryDslRepositorySupport(EntityManager entityManager) {
this.entityManager = entityManager;
this.querydsl = new Querydsl(entityManager, new PathBuilderFactory().create(getClass()));
}
// from 절 반환
protected JPQLQuery from(EntityPath<?>... paths) {
return querydsl.createQuery(paths);
}
// QueryDSL delete 절 반환
protected DeleteClause<JPADeleteClause> delete(EntityPath<?> path) {
return new JPADeleteClause(entityManager, path);
}
// QueryDSL update 절 반환
protected UpdateClause<JPAUpdateClause> update(EntityPath<?> path) {
return new JPAUpdateClause(entityManager, path);
}
// 스프링 데이터 JPA가 제공하는 Querydsl을 편하게 사용하도록 돕느느
// 핼퍼 객체 반환
protected Querydsl getQuerydsl() {
return this.querydsl;
}
}
'개인공부 > JPA' 카테고리의 다른 글
10장 객체지향 쿼리 언어 (1) | 2024.02.05 |
---|---|
9장 값 타입 (2) | 2024.02.05 |
8장 프록시와 연관관계 관리 (0) | 2024.02.05 |
7장 고급 매핑 (2) | 2024.02.05 |
6장 다양한 연관관계 매핑 (1) | 2024.02.05 |