JPQL이란 JPA Query Language의 줄임말, JPA에서 사용할 수 있는 쿼리.
문법은 SQL과 매우 비슷하다. SQL과의 차이점은 SQL에서는 테이블이나 컬럼의 이름을 사용하는 것과 달리 JPQL은 다음과 같이 엔티티 객체를 대상으로 수행하는 쿼리이기에 매핑된 엔티티의 이름과 필드의 이름을 사용한다는 것이다.
SELECT p FROM Product(엔티티 타입) p WHERE p.number(엔티티 속성) = ?1;
리포지토리는 JpaRepository를 상속받는 것만으로도 다양한 CRUD 메서드 제공.
하지만 이러한 기본 메서드들은 식별자 기반으로 생성되기에 별도의 메서드를 정의하는 경우가 많음. 이때 간단한 쿼리문 작성을 위해 사용되는 것이 쿼리 메서드.
'find...By', 'exist...By'와 같은 키워드로 쿼리의 주제를 정함.
'By' - 서술어의 시작을 나타내는 구분자 역할.
서술어 부분은 검색 및 정렬 조건을 지정하는 영역. 엔티티 속성 정의, AND나 OR 사용해 조건 확장 가능
예) (리턴타입) + {주제+서술어(속성)} 구조의 메서드
List<Person> findByLastnameAndEmail(String lastName, String email);
서술어에 들어가는 엔티티 속성 식(Expression)은 위와 같이 엔티티에서 관리하고 있는 속성(필드)만 참조 가능
조회하는 기능을 수행하는 키워드. '...'으로 표시한 영역에는 도메인(엔티티) 표현. 그러나 리포지토리에서 이미 도메인을 설정한 후에 메서드를 사용하기 때문에 중복으로 판단해 생략하기도 함. 리턴 타입으로 Collection이나 Stream에 속한 하위 타입 설정 가능.
// 리포지토리에 쿼리 메서드 작성
Optional<Product> findByNumber(Long number);
List<Product> findAllByName(String name);
Product queryByNumber(Long number);
특정 데이터가 존재하는지 확인하는 키워드. 리턴 타입으로는 boolean 사용.
boolean existsByNumber(Long number);
조회 쿼리를 수행한 후 쿼리 결과로 나온 레코드 개수 리턴.
long countByName(String name);
삭제 쿼리 수행. 리턴 타입 없거나 삭제한 횟수 리턴.
void deleteByNumber(Long number);
long removeByName(String name);
쿼리를 통해 조회된 결과값의 개수를 제한하는 키워드. 두 키워드는 동일한 동작 수행. 주제와 By 사이에 위치함. 일반적으로 한 번의 동작으로 여러 건을 조회할 때 사용, 단 건 조회는 <number.> 생략.
List<Product> findFirst5ByName(String name);
List<Product> findTop100ByName(String name);
JPQL의 서술어 부분에서 사용
값의 일치를 조건으로 사용하는 조건자 키워드. 생략되는 경우가 많으며 Equals와 동일한 기능 수행.
// findByNumber 메서드와 동일하게 동작
Product findByNumberIs(Long number);
Product findByNumberEquals(Long number);
값의 불일치를 조건으로 사용하는 조건자 키워드. Is는 생략하고 Not 키워드만 사용 가능.
Product findByNumberIsNot(Long number);
Product findByNumberNot(Long number);
값이 null인지 검사하는 조건자 키워드.
List<Product> findByUpdatedAtNull();
List<Product> findByUpdatedAtIsNull();
List<Product> findByUpdatedAtNotNull();
List<Product> findByUpdatedAtIsNotNull();
boolean 타입으로 지정된 칼럼값을 확인하는 키워드.
Product findByisActiveTrue();
Product findByisActiveIsTrue();
Product findByisActiveFalse();
Product findByisActiveIsFalse();
여러 조건을 묶을 때 사용.
Product findByNumberAndName(Long number, String name);
Product findByNumberOrName(Long number, String name);
숫자나 datetime 컬럼을 대상으로 한 비교 연산에 사용할 수 있는 조건자 키워드.
GreaterThan
, LessThan
키워드는 비교 대상에 대한 초과/미만의 개념으로 비교 연산 수행, 경계값을 포함하려면 Equal
키워드 추가.
List<Product> findByPriceIsGreaterThan(Long price);
List<Product> findByPriceGreaterThan(Long price);
List<Product> findByPriceGreaterThanEqual(Long price);
List<Product> findByPriceIsLessThan(Long price);
List<Product> findByPriceLessThan(Long price);
List<Product> findByPriceLessThanEqual(Long price);
List<Product> findByPriceIsBetween(Long lowPrice, Long highPrice);
List<Product> findByPriceBetween(Long lowPrice, Long highPrice);
컬럼값에서 일부 일치 여부를 확인하는 조건자 키워드.
SQL 쿼리문에서 값의 일부를 포함하는 값을 추출할 때 사용하는 '%' 키워드와 동일한 역할을 하는 키워드.
자동으로 생성되는 SQL문을 보면 Containing
키워드는 문자열의 양 끝, StartingWith
키워드는 문자열의 앞, EndingWith
키워드는 문자열의 끝에 '%'가 배치됨. 여기서 별도로 고려해야 하는 키워드는 Like
키워드인데, 이 키워드는 코드 수준에서 메소드를 호출하면서 전달하는 값에 %를 명시적으로 입력해야 함.
List<Product> findByNameLike(String name);
List<Product> findByNameIsLike(String name);
List<Product> findByNameContains(String name);
List<Product> findByNameContaining(String name);
List<Product> findByNameIsContaining(String name);
List<Product> findByNameStartsWith(String name);
List<Product> findByNameStartingWith(String name);
List<Product> findByNameIsStartingWith(String name);
List<Product> findByNameEndsWith(String name);
List<Product> findByNameEndingWith(String name);
List<Product> findByNameIsEndingWith(String name);
기본 쿼리 메서드 작성 후 OrderBy 키워드 삽입, 정렬하고자 하는 칼럼과 오름차순/내림차순 설정
List<Product> findByNameOrderByNumberAsc(String name); // Asc : 오름차순 // 상품정보를 이름으로 검색한 후 상품 번호로 오름차순 정렬
List<Product> findByNameOrderByNumberDesc(String name); // Desc : 내림차순
조건 여러 개 사용시 And나 Or 키워드 사용X
List<Product> findByNameOrderByPriceAscStockDesc(String name); // 먼저 Price를 기준으로 오름차순 정렬 후 후순위로 재고수량을 기준으로 내림차순 정렬 수행
매개변수를 활용한 쿼리 정렬
List<Product> findByName(String name, Sort sort);
이름에 키워드를 넣지 않고 Sort 객체를 활용해 매개변수로 받아들인 정렬 기준을 가지고 쿼리문 작성. 쿼리 메서드를 정의하는 단계에서 코드가 줄어드는 장점이 있음.
@SpringBootTest
public class ProductRepositoryTest0 {
@Autowired
ProductRepository productRepository;
@Test
void sortingAndPagingTest() {
Product product1 = new Product();
product1.setName("펜");
product1.setPrice(1000);
product1.setStock(100);
product1.setCreatedAt(LocalDateTime.now());
product1.setUpdatedAt(LocalDateTime.now());
Product product2 = new Product();
product2.setName("펜");
product2.setPrice(5000);
product2.setStock(300);
product2.setCreatedAt(LocalDateTime.now());
product2.setUpdatedAt(LocalDateTime.now());
Product product3 = new Product();
product3.setName("펜");
product3.setPrice(500);
product3.setStock(50);
product3.setCreatedAt(LocalDateTime.now());
product3.setUpdatedAt(LocalDateTime.now());
Product savedProduct1 = productRepository.save(product1);
Product savedProduct2 = productRepository.save(product2);
Product savedProduct3 = productRepository.save(product3);
productRepository.findByName("펜", Sort.by(Sort.Order.asc("price")));
productRepository.findByName("펜", Sort.by(Sort.Order.asc("price"), Sort.Order.desc("stock")));
}
}
Sort 부분을 하나의 메서드로 분리해 쿼리 메서드를 호출하는 방법
...상단 코드 동일, 생략
productRepository.findByName("펜", getSort());
}
private Sort getSort() {
return Sort.by(
Sort.Order.asc("price"),
Sort.Order.desc("stock")
);
}
데이터베이스의 레코드를 개수로 나눠 페이지를 구분하는 것.
ex) 25개 레코드 -> 레코드를 7개씩, 총 4개의 페이지로 구분 / 특정 페이지를 가져옴.
JPA에서는 페이징 처리를 위해 Page
와 Pageable
사용.
// 페이징 처리를 위한 쿼리 메서드
Page<Product> findByName(String name, Pageable pageable);
리턴 타입으로 Page
를 설정하고 매개변수에는 Pageable
타입의 객체 정의.
// 페이징 쿼리 메서드 호출
Page<Product> productPage = productRepository.findByName("펜", PageRequest.of(0, 2));
리턴 타입으로 Page
객체를 받아야 하기에 Page<Product>
로 타입을 선언해 객체를 리턴 받음, Pageable
파라미터를 전달하기 위해 PageRequest
클래스 사용. PageRequest
는 Pageable
의 구현체.
일반적으로 PageRequest
는 of
메서드를 통해 PageRequest
객체 생성. of
메서드는 매개변수에 따라 다양한 형태로 오버로딩.
Page 객체를 그대로 출력하면 해당 객체의 값을 보여주지 않고 몇 번째 페이지에 해당하는지만 확인 가능.
Page<Product> productPage = productRepository.findByName("펜", PageRequest.of(0, 2));
System.out.println(productPage.getContent()); // 배열 형태로 값 출력
@Query
어노테이션을 사용해 직접 JPQL을 작성할 수 있음.
@Query("SELECT p FROM Product AS p WHERE p.name = ?1")
List<Product> findByName(String name);
FROM 뒤에서 엔티티 타입 지정, 별칭 생성(AS 생략 가능).
WHERE문에서는 SQL과 마찬가지로 조건 지정.
조건문에서 사용한 '?1'은 파라미터를 전달받기 위한 인자에 해당, 1은 첫 번째 파라미터 의미. 파라미터의 순서가 바뀌면 오류 발생 가능성이 있어 다음과 같이 @Param
어노테이션을 사용하는 것이 좋음.
@Query("SELECT p FROM Product AS p WHERE p.name = :name")
List<Product> findByNameParam(@Param("name") String name);
파라미터를 바인딩하는 방식으로 메서드를 구현하면 코드의 가독성이 높아지고 유지보수가 수월해짐.
@Query
를 사용하면 엔티티 타입이 아니라 원하는 컬럼의 값만 추출 가능.
@Query("SELECT p.name, p.price, p.stock FROM Product p WHERE p.name = :name")
List<Object[]> findByNameParam2(@Param("name") String name);
SELECT에 가져오고자 하는 컬럼 지정. 이때 메서드에서는 Object 배열의 리스트 형태로 리턴 타입을 지정해야 함.
출처 - (책) 스프링 부트 핵심 가이드 / 장정우, 위키북스