Spring Data JPA
Spring Data JPA는 Spring Data 프로젝트 하위 프로젝트 중 하나다.
Spring Data 프로젝트는 JPA, 몽고 DB, NEO4J, REDIS, HADOOP, GEMFIRE 같은 다양한 데이터 저장소에 대한 접근을 추상화해서 개발자 평의를 제공하고 지루하게 반복하는 데이터 접근 코드를 줄여둔다.
Spring Data JPA는 스프링 프레임워크에서 JPA를 편리하게 사용할 수 있도록 지원하는 프로젝트다.
Spring Data JPA는 애플리케이션을 실행할 때 basePackage에 있는 리포지토리 인터페이스들을 찾아서 해당 인터페이스를 구현한 클래스를 동적으로 생성한 다음 스프링 빈으로 등록한다.
그래서 JpaRepository만 상속받고 구현체를 구현하지 않아도 CRUD 기능을 사용할 수 있다.
Spring Boot 설정은 Dependency만 추가해주면 된다.
dependencies {
...
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
}
공통 인터페이스 기능
Spring Data JPA는 간단한 CRUD 기능을 공통으로 처리하는 JpaRepository 인터페이스를 제공한다.
public interface MemberRepository extends JpaRepository<Member, Long> {
Member findByUsername(String username);
}
JpaRepository 제네릭에 회원 엔티티와 회원 엔티티의 식별자 타입을 지정하면 MemberRepository는 JpaRepository를 사용할 수 있다. JpaRepository 인터페이스의 계층 구조는 아래와 같다.
- save() : 새로운 엔티티는 저장하고 이미 있는 엔티티는 수정한다.
- delete() : 엔티티 하나를 삭제한다. 내부에서 EntityManager.remove()를 호출한다.
- getOne() : 엔티티를 프록시로 조회한다. 내부에서 EntityManager.getReference()를 호출한다.
- findAll() : 모든 엔티티를 조회한다. 정렬(Sort)이나 페이징(Pageable) 조건을 파라미터로 제공할 수 있다.
쿼리 메소드 기능
쿼리 메소드 기능은 메소드 이름만으로 쿼리를 생성하는 기능이다.
인터페이스에 메소드 이름만 선언하면 적절한 JPQL 쿼리를 생성해서 실행한다.
나는 Spring Data JPA를 처음 경험했을때 이 기능을 보고 깜짝놀랐다... 아주 편리한 기능이다.
스프링 데이터 JPA가 제공하는 쿼리 메소드 기능은 크게 3가지가 있다.
- 메소드 이름으로 쿼리 생성
- JPA NamedQuery
- @Query 어노테이션을 사용해서 리포지토리 인터페이스에 쿼리 직접 정의
메소드 이름으로 쿼리 생성
public interface MemberRepository extends JpaRepository<Member, Long> {
List<Member> findByEmailAndName(String email, String name);
}
인터페이스에 정의한 findByEmailAndName메서드의 이름을 분석해서 JPQL을 생성하고 실행한다.
findByEmailAndName의 실행하면 아래와 같은 JPQL이 실행될 것 이다.
SELECT m FROM Member m WHERE m.email = ?1 AND m.name = ?2
물론 정해진 규칙에 따라 메소그 이름을 지어야 한다. 만약 잘못된 규칙으로 이름을 선언했을 경우 컴파일 단계에서 오류가 발생한다. 정해진 규칙은 Spring Data JPA 공식 문서에서 확인할 수 있다.
참고로 엔티티의 필드명이 변경되면 인터페이스에 언선된 메서드의 이름도 변경해야 한다.
JPA NamedQuery
JPA Named 쿼리는 이름 그대로 쿼리에 이름을 부여해서 사용하는 방법이다.
@Entity
@NamedQuery(name = "Member.findByUsername", query = "SELECT m FROM Member m WHERE m.username = :username")
public class Member {
....
}
public interface MemberRepository extends JpaRepository<Member, Long> {
List<Member> findByUsername(@Param("username") String username);
}
엔티티에서 선언한 "도메인.메서드명"의 Named를 찾은 후 쿼리를 실행한다.
만약 실행할 Named 쿼리가 없으면 메소드 이름으로 쿼리 생성 전략을 사용한다.
@Param 어노테이션은 이름 기반 파라미터를 바인딩할 때 사용한다.
@Query 어노테이션을 사용해서 리포지토리 인터페이스에 쿼리 직접 정의
이 방법은 실행할 메소드에 정적 쿼리를 직접 작성하므로 이름 없는 Named 쿼리라 할 수 있다.
JPQL 쿼리로 작성할 경우 애플리케이션 실행 시점에 문법 오류를 발견할 수 있다.
public interface MemberRepository extends JpaRepository<Member, Long> {
@Query("SELECT m FROM Member m WHERE m.username = ?1") // JPQL
Member findByUsername(String username);
@Query("SELECT m FROM member m WHERE m.username = ?0", nativeQuery = true) // SQL
Member findByUsername(String username);
}
@Query 어노테이션의 nativeQuery 속성을 true로 설정하면 JPQL이 아닌 SQL로 메서드를 사용할 수 있다.
Native Query 같은 경우 바인딩하는 파라미터가 0부터 시작하는 것을 주의하자.
벌크성 수정 쿼리
앞서 블로그 글에서 벌크 연산이 무엇인지 확인해 보았다.
JPA에서는 벌크설 쿼리는 @Modifying 어노테이션을 사용해야 한다.
@Modifying
@Query("UPDATE Product p SET p.price = p.price * 1.1 WHERE p.stockAmount < :stockAmount")
int bulkPriceUp(@Param("stockAmount") String stockAmount);
벌크성 쿼리를 실행하고 나서 영속성 컨텍스트를 초기화하고 싶으면 clearAutometically 옵션을 true로 설정하면 된다.
반환 타입
Spring Data JPA는 유연한 반환 타입을 지원하는데 결과가 한 건 이상이면 컬렉션 인터페이스를 사용하고 단건이면 반환 타입을 지정한다.
List<Member> findByName(String name); // 컬렉션
Member findByEmail(String email); // 단건
만약 조회 결과가 없으면 컬렉션은 빈 컬렉션을 반환하고 단건은 null을 반환한다.
그리고 단건을 기대하고 반환타입을 지정했는데 결과가 2건 이상 조회되면 NonUniqueResultException 예외가 발생하고 조회 결과가 없으면 Null을 반환한다.
나는 Optional을 사용하거나 Exception을 설정해 간편한게 사용한다.
Optional<Member> findByEmail(String email);
Optional<Member> member = MemberRepository.findByEmail("devbeekei@gmail.com");
if (member.isPresent()) {
...
}
페이징과 정렬
Spring Data JPA는 쿼리 메소드에 페이징과 절렬 기능을 사용할 수 있도록 2가지 특별한 파라미터를 제공한다.
- org.springframework.data.domain.Sort : 정렬 기능
- org.springframework.data.domain.Pageable : 페이징 기능(내부에 Sort 포함)
파라미터에 Pageable을 사용하면 반환 타입으로 List나 org.springframework.data.domain.Page를 사용할 수 있다.
Page를 사용하면 페이징 기능을 제공하기 위해 검색된 전체 데이터 건수를 조회하는 count 쿼리를 추가로 호출한다.
public interface Page<T> extends Iterable<T> {
int getNumber(); // 현재 페이지
int getSize(); // 페이지 크기
int getTotalPages(); // 전체 페이지 수
int getNumberOfElements(); // 현재 페이지에 나올 데이터 수
long getTotalElements(); // 전체 데이터 수
boolean hasPreviousPage(); // 이전 페이지 여부
boolean hasNextPage(); // 다음 페이지 여부
boolean isFirstPage(); // 현재 페이지가 첫 페이지 인지
boolean isLastPage(); // 현재 페이지가 마지막 페이지 인지
Pageable nextPageable(); // 다음 페이지 객체, 다음 페이지가 없으면 null
Pageable previousPageable(); // 이전 페이지 객체, 이전 페이지가 없으면 null
List<T> getContent(); // 조회된 데이터
boolean hasContent(); // 조회된 데이터 존재 여부
Sort getSort(); // 정렬 정보
}
public interface MemberRepository extends Repository<Member, Long> {
Page<Mewmber> findByNameStartingWith(String name, Pageable pageble);
}
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(); // 다음 페이지 존재 여부
힌트
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 쿼리에도 쿼리 힌트를 적용할지를 설정하느 옵션이다.(기본값 true)
Lock
쿼리 시 락을 걸려면 org.springframework.data.jpa.repository.Lock 어노테이션을 사용하면 된다.
@Lock(LockModeType.PERSSIMISTIC_WRITE)
List<Member> findByName(String name);
'JPA' 카테고리의 다른 글
트랜잭션 범위의 영속성 컨텍스트 (0) | 2022.01.20 |
---|---|
JPA Secification을 이용한 검색 (0) | 2022.01.20 |
벌크 연산, JPQL과 영속성 콘텍스트, Flush Mode (1) | 2022.01.17 |
JPA 스토어드 프로시저(Stored Procedure) (1) | 2022.01.17 |
JPA N+1 문제 해결하기 (0) | 2022.01.17 |