1. JPQL
JPQL은 JPA Query Language의 줄임말로, JPA에서 사용할 수 있는 쿼리를 의미한다. SQL과의 차이점은 SQL에서는 테이블이나 칼럼의 이름을 사용하지만 JPQL은 아래 그림과 같이 엔티티 객체를 대상으로 수행하는 쿼리이기 때문에 매핑된 엔티티의 이름과 필드의 이름을 사용한다.
2. 쿼리 메서드
Repository 는 JpaRepository 를 상속받는 것만으로도 다양한 CRUD 메서드를 제공한다. 하지만 이러한 기본 메서드들은 식별자 기반으로 생성되기 때문에 별도의 메서드를 정의해서 사용하는 경우가 많다. 이때 간단한 쿼리문을 작성하기 위해 사용되는 것이 쿼리 메서드 이다.
(1) 쿼리 메서드의 생성
쿼리 메서드는 동작을 결정하는 주제(Subject)와 서술어(Predicate)로 구분한다. 'find ... By' 와 같은 키워드로 쿼리의 주제를 정하며 "By"는 서술어의 시작을 나타내는 구분자 역할을 한다. 서술어 부분은 검색 및 정렬 조건을 지정하는 영역이다. 기본적으로 엔티티의 속성이나 AND 또는 OR을 사용해 조건을 확장하는 것도 가능하다.
(2) 쿼리 메서드의 주제 키워드
쿼리 메서드의 주제 부분에 사용할 수 있는 주요 키워드 종류이다. ' ... ' 로 표시된 영역에는 도메인(엔티티)을 표현할 수 있다.
- 조회하는 기능을 수행하는 키워드
- find ... By
find ... By 키워드를 활용한 쿼리 메서드 예시 - read ... By
- get ... By
- query ... By
- search ... By
- stream ... By
- find ... By
- 특정 데이터가 존재하는지 확인하는 키워드, 리턴 타입으로 boolean 타입 사용
- exists ... By
- 조회 쿼리를 수행한 후 쿼리 결과로 나온 레코드의 개수를 리턴
- count ... By
- 삭제 쿼리를 수행, 리턴 타입이 없거나 삭제한 횟수를 리턴한다.
- delete ... By
- remove ... By
- 쿼리를 통해 조회된 결괏값의 개수를 제한하는 키워드
- ...First<number>...
- ...Top<number>...
(3) 쿼리 메서드의 조건자 키워드
JPQL의 서술어 부분에 사용할 수 있는 조건자 키워드들이다.
- Is
- 값의 일치를 조건으로 사용하는 조건자 키워드
- 생략되는 경우가 많고 Equals와 동일한 기능을 수행한다.
// findByNumber 메서드와 동일하게 동작
Product findByNumberIs(Long number);
Product findByNumberEquals(Long number);
- (Is)Not
- 값의 불일치를 조건으로 사용하는 조건자 키워드
- Is는 생략하고 Not 키워드만 사용할 수도 있다.
Product findByNumberIsNot(Long number);
Product findByNumberNot(Long number);
- (Is)Null, (Is)NotNull
- 값이 null인지 검사하는 조건자 키워드
List<Product> findByUpdatedAtNull();
List<Product> findByUpdatedAtIsNull();
List<Product> findByUpdatedAtNotNull();
List<Product> findByUpdatedAtIsNotNull();
- (Is)True, (Is)False
- boolean 타입으로 지정된 칼럼값을 확인하는 키워드
Product findByisActiveTrue();
Product findByisActiveIsTrue();
Product findByisActiveFalse();
Product findByisActiveIsFalse();
- And, Or
- 여러 조건을 묶을 때 사용하는 키워드
// And, Or 키워드를 사용한 쿼리 메서드
Product findByNumberAndName(Long number, String name);
Product findByNumberOrName(Long number, String name);
- (Is)GreaterThan, (Is)LessThan, (Is)Between
- 숫자나 datetime 칼럼을 대상으로 한 비교 연산에 사용할 수 있는 조건자 키워드
- GreaterThan, LessThan 키워드는 비교 대상에 대한 초과 / 미만의 개념으로 비교 연산을 수행하고, 경곗값을 포함하려면 Equal 키워드를 추가한다
// GreaterThan, LessThan, Between 키워드를 사용한 쿼리 메서드
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);
- (Is)StartingWith(==StartsWith), (Is)EndingWith(==EndsWith), (Is)Containing(==Contains), (Is)Like
- 칼럼값에서 일부 일치 여부를 확인하는 조건자 키워드
- 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);
3. 정렬과 페이징 처리
(1) 정렬 처리하기
일반적인 쿼리문에서 정렬을 사용할 때 ORDER BY 구문을 사용하듯 쿼리 메서드도 정렬 기능에 동일한 키워드를 사용한다.
다른 쿼리 메서드들은 조건 구문에서 조건을 여러 개 사용하기 위해 And 와 Or 키워드를 사용했으나 정렬 구문은 And나 Or 키워드를 사용하지 않고 차례대로 작성한다.
// Asc : 오름차순, Desc : 내림차순
List<Product> findByNameOrderByNumberAsc(String name); // 오름차순
List<Product> findByNameOrderByNumberDesc(String name); // 내림차순
// 쿼리 메서드에서 여러 정렬 기준 사용
// And 나 Or 키워드를 사용하지 않는다.
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")));
(2) 페이징 처리
페이징이란 데이터베이스의 레코드를 개수로 나눠 페이지를 구분하는 것을 의미한다. JPA 에서는 페이징 처리를 위헤 Page와 Pageable을 사용한다. 리턴 타입으로 Page를 설정하고 매개변수에는 Pageable 타입의 객체를 정의한다.
// 페이징 처리를 위한 쿼리 메서드 예시
Page<Product> findByName(String name, Pageable pageable);
// 페이징 쿼리 메서드를 호출하는 방법
Page<Product> productPage = productRepository.findByName("펜", PageRequest.of(0,2));
일반적으로 PageRequest는 of 메서드를 통해 PageRequest 객체를 생성한다.
4. @Query 어노테이션 사용하기
@Query 어노테이션을 사용해 직접 JPQL을 작성할 수 있다. JPQL을 사용하면 JPA 구현체에서 자동으로 쿼리 문장을 해석하고 실행하게 된다. 하지만 튜닝된 쿼리를 사용하고자 할 때 직접 SQL을 작성한다.
// Query 어노테이션을 사용하는 메서드
@Query("SELECT p FROM Product AS p WHERE p.name = ?1")
List<Product> findByName(String name);
WHERE문의 조건문에서 사용한 '?1' 은 파라미터를 전달받기 위한 인자에 해당한다. 1은 첫 번째 파라미터를 의미한다. 하지만 이 방식을 사용할 경우 파라미터의 순서가 바뀌면 오류가 발생할 수 있어 @Param 어노테이션을 사용하는 것이 좋다.
// @Query 어노테이션과 @Param 어노테이션을 사용한 메서드
@Query("SELECT p FROM Product 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<Product> findByNameParam(@Param("name") String name);
5. QueryDSL 적용하기
(1) QueryDSL 이란
QueryDSL 은 정적 타입을 이용해 SQL과 같은 쿼리를 생성할 수 있도록 지원하는 프레임워크이다. 문자열이나 XML 파일을 통해 쿼리를 작성하는 대신 QueryDSL이 제공하는 플루언트(Fluent) API를 활용해 쿼리를 생성할 수 있다.
(2) QueryDSL 의 장점
- IDE가 제공하는 코드 자동 완성 기능을 사용할 수 있다.
- 문접적으로 잘못된 쿼리를 허용하지 않는다. 따라서 정상적으로 활용된 QueryDSL 은 문법 오류를 발생시키지 않는다.
- 고정된 SQL쿼리를 작성하지 않기 때문에 동적으로 쿼리 생성이 가능하다.
- 코드로 작성하므로 가독성 및 생산성이 향상된다.
- 도메인 타입과 Property를 안전하게 참조할 수 있다.
(3) 기본적인 QueryDSL 사용하기
- QueryDSL 을 사용하기 위해서 JPAQuery 객체를 사용한다.
- JPAQuery는 엔티티 매니저(Entity Manager)을 활용해 생성한다.
// JPAQuery를 활용한 QueryDSL 코드
JPAQuery<Product> query = new JPAQuery(entityManager); // JPAQuery 객체 생성
QProduct qProduct = QProduct.product; // JPAQuery객체를 이용하여 QueryDSL 사용
List<Product> productList = query
.from(qProduct)
.where(qProrduct.name.eq("펜"))
.orderBy(qProduct.price.asc())
.fetch();
- 반환 메서드로 사용할 수 있는 메서드 종류
- List<T> fetch() : 조회 결과를 리스트로 반환
- T fetchOne : 단 건의 조회 결과를 반환
- T fetchFirrst() : 여러 건의 조회 결과 중 1건을 반환한다.
- Long fetchCount() : 조회 결과의 개수 반환
- QueryResult<T> fetchResults() : 조회 결과 리스트와 개수를 포함한 QueryResults 를 반환
JPAQueryFactory를 활용하면 select 절부터 작성 가능하다.
// 방법 1
JPAQueryFactory jpaQueryFactory = new JPAQueryFactory(entityManager);
QProduct qProduct = QProduct.product;
List<Product> productList = jpaQueryFactory.selectFrom(qProduct)
.where(qProduct.name.eq("펜"))
.orderBy(qProduct.price.asc())
.fetch();
// 방법 2
JPAQueryFactory jpaQueryFactory = new JPAQueryFactory(entityManager);
QProduct qProduct = QProduct.product;
List<Product> productList = jpaQueryFactory
.select(qProduct.name)
.from(qProduct)
.where(qProduct.name.eq("펜"))
.orderBy(qProduct.price.asc())
.fetch();
'SpringBoot' 카테고리의 다른 글
연관관계 매핑 (0) | 2024.02.22 |
---|---|
JPA Auditing (0) | 2024.02.14 |
ORM (Object Relational Mapping) (1) | 2024.02.08 |
REST API 명세를 문서화하기 (0) | 2024.02.02 |
API 작성 방법 (Delete API) (0) | 2024.02.02 |