오늘은 Spring Data JPA 활용에 관한 부분이다. 이전에는 JPA가 무엇인지 알아보았고 오늘은 어떻게 활용하는지 알아보자.
JPQL은 JPA Query Language의 줄임말로 JPA에서 사용할 수 있는 쿼리를 의미한다. JPQL의 문법은 SQL과 비슷하지만 테이블이나 컬럼의 이름을 사용하지 않고 엔티티의 이름과 필드 이름을 사용한다.
Repository는 JpaRepository를 상속받는 것만으로도 다양한 CRUD 메서드를 제공한다. 하지만 이러한 메서드들 이외에도 별도의 메서드를 정의해서 사용하는 경우가 훨씬 많다. 이때 사용되는 것이 쿼리 메서드이다.
쿼리 메서드는 Subject와 Predicate로 구성된다. 예를 들자면 'find...By', 'exists...By'와 같은 형식이다. find, exists가 Subject이고 By의 뒷부분이 Predicate이다. 또한 AND, OR을 사용해 조건을 확장한다.
예제 코드
//리턴타입 + Subject + Predicate
List<Person> finddByLastnameAndEmail(String lastName, String email);
쿼리 메서드 Subject 부분에 사용할 수 있는 주요 키워드는 다음과 같다.
exists...By
특정 데이터가 존재하는지 확인하는 키워드
리턴 타입으로 boolean 값 사용
boolean existsByNumber(Long number);
count...By
조회 쿼리 수행 후, 쿼리 결과로 나온 레코드의 개수를 리턴한다
long countByName(String name);
delete...By, remove...By
void deleteByNumber(Long number);
long removeByName(String name);
...First<number>..., ...Top<number>...
number
>를 생략하면 된다.List<Product> findFirst5ByName(String name);
List<Product> findTop10ByName(String name);
서술어 부분에 사용할 수 있는 키워드는 다음과 같다.
Is
Product findByNumberIs(Long number);
Product findByNumberEquals(Long number);
//모두 findByNumber와 동일
(Is)Not
Product findByNumberIsNot(Long number);
Product findByNumberNot(Long number);
(Is)Null, (Is)NotNull
List<Product> findByUpdatedAtNull();
List<Product> findByUpdatedAtIsNull();
List<Product> findByUpdatedAtNotNull();
List<Product> findByUpdatedAtIsNotNull();
(Is)True, (Is)False
Product findByisActiveTrue();
Product findByisActiveIsTrue();
Product findByisActiveFalse();
Product findByisActiveIsFalse();
And, Or
Product findByNumberAndName(Long number, String name);
Product findByNumberOrName(Long number, String name);
(Is)GreaterThan, (Is)LessThan, (Is)Between
List<Product> findByPriceGreaterThan(Long price);
List<Product> findByPriceGreaterThanEqual(Long price);
List<Product> findByPriceLessThan(Long price);
List<Product> findByPriceLessThanEqual(Long price);
List<Product> findByPriceBetween(Long lowPrice, Long highPrice);
(Is)StartingWith(==StartsWith), (Is)EndingWith(==EndsWith), (Is)Containing(==Contains), (Is)Like
List<Product> findByNameLike(String name);
List<Product> findByNameContains(String name);
List<Product> findByNameStartsWith(String name);
List<Product> findByNameEndsWith(String name);
기본 쿼리 메서드의 이름을 통해 페이징 처리도 가능하지만 다른 방법들도 많이 사용된다. 한번 살펴보자.
일반적으로 쿼리문에서 정렬을 사용할 때는 ORDER BY를 많이 사용한다.
// Asc: 오름차순, Desc: 내림차순
List<Product> findByNameOrderByNumberAsc(String name);
List<Product> findByNameOrderByNumberDesc(String name);
위의 코드를 해석하자면, '상품정보를 이름으로 검색 후 상품 번호로 오름차순(내림차순) 정렬을 수행' 한다는 뜻이다.
또한 여러가지 기준을 적용하고 싶으면 AND를 생략하고 조건을 붙이면 된다.
List<Product> findByNameOrderByPriceAscStockDesc(String name);
위 코드는 '상품 가격을 기준으로 오름차순 정렬 후, 재고수량을 기준으로 내림차순 정렬' 한다는 의미이다.
쿼리 메서드에 정렬 키워드를 삽입해서 정렬을 수행할 수도 있지만, 매개변수를 활용해 정렬할 수도 있다.
List<Product> findByName(String name, Sort sort);
productRepository.findByName("펜", Sort.by(Order.asc("price")));
productRepository.findByName("펜", Sort.by(Order.asc("price"), Order.desc("stock")));
Sort 객체를 전달하여 메서드를 정렬하는 코드이다. Sort 클래스는 내부 클래스로 정의돼 있는 Order 객체를 활용해 정렬 기준을 생성한다. 여러 정렬 기준을 사용할 경우 콤마를 사용하여 구분한다.
페이징이란 데이터베이스의 레코드를 개수로 나눠 페이지를 구분하는 것을 의미한다. 보통 게시판 프로젝트나 CRUD 구현에서 자주 볼수 있다.
JPA에서는 페이징 처리를 위해 Page와 Pageable을 사용한다.
Page<Product> findByName(String name, Pageable pageable);
Page<Product> productPage = findByName("펜", PageRequest.of(0, 2));
위와 같이 메서드에서 매개변수로 Pageable 객체를 받는다. 호출에는 PageRequest를 넘겨 주었는데, PageRequest는 Pageable의 구현체이다.
PageRequest는 of 메서드를 통해 PageRequest 객체를 생성한다. of 메서드는 매개변수에 따라 다양한 형태로 오버로딩 되어 있다.
of(int page, int size)
페이지 번호(0부터 시작), 페이지당 데이터 개수
데이터를 정렬하지 않음
of(int page, int size, Sort)
of(int page, int size, Direction, String... properties)
페이지 번호, 페이지당 데이터 개수, 정렬 방향, 속성(컬럼)
Sort.by(direction, properties)
에 의해 정렬
또한 Page 객체를 그대로 출력하면 값을 보여주지 않고 몇 번째 페이지에 해당하는지만 확인할 수 있는데, 세부적인 값을 보려면 아래와 같이 사용한다.
Page<Product> productPage = findByName("펜", PageRequest.of(0, 2))); System.out.println(productPage.getContent());
위와 같이
getContent()
를 사용하여 배열로 출력 가능하다.
위의 예시들과 같이 메서드 이름만으로 쿼리 메서드를 생성 할수도 있지만, @Query 어노테이션을 사용해 직접 JPQL을 작성할 수도 있다.
@Query("SELECT p FROM Product AS p WHERE p.name = ?1")
List<Product> findByName(String name);
//소문자로 작성 가능하며 AS는 생략 가능
조건문에서 사용한 '?1'은 파라미터를 전달 받기 위한 인자
1은 첫 번째 파라미터를 의미한다.
하지만 이는 파라미터의 순서가 바뀌면 오류가 발생할 수도 있으니 @Param 어노테이션을 사용한다.
@Query("SELECT p FROM Product p WHERE p.name = :name")
List<Product> findByNameParam(@Param("name") String name);
또한 @Query 사용 시 특정 원하는 컬럼만 값 추출이 가능하다.
@Query("SELECT p.name, p.pricec, p.stock FROM Product p WHERE p.name = :name")
List<Object[]> findByNameParam2(@Param("name") String name);
메서드의 이름과 @Query 어노테이션을 사용하면 직접 문자열을 입력하기 때문에 컴파일 시점에 에러를 잡지 못하고 런타임 에러가 발생할 수 있다. 이 같은 문제를 해결하기 위해 QueryDSL이 사용된다.
QueryDSL은 문자열이 아니라 코드로 쿼리를 작성할 수 있도록 도와준다.
..QueryDSL은 추후에 알아보도록 하자..