JPAL
JPQL은 JPA에서 사용할 수 있는 쿼리이다. JPQL문법은 SQL과 매우 비슷하며 DB쿼리에 익숙하다면 어렵지 않게 사용 가능하다. SLQ과의 차이점은 테이블이나 칼럼의 이름대신 엔티티 객체를 대상으로 수행하는 쿼리익 때문에 매핑된 엔티티 이름과 필드 이름을 사용한다.
쿼리 메서드
쿼리 메서드는 크게 동작을 결정하는 주제(Subject)와 서술어(Predicate)로 구분된다.
'find...By', 'exist...By'와 같은 키워드로 쿼리의 주제를 정하고 'By'는 서술어의 시작을 나타내는 구분자 역할이다.
서술어 부분은 검색 및 정렬 조건을 지정하는 영역이다.
ex) (리턴타입) + {주제 + 서술어(속성)} 구조의 메서드
List<Person> findByLastnameAndEmail(String lastName, String email);
서술어에 들어가는 속성 식은 엔티티에서 관리하는 속성만 참조할 수 있다.
...으로 표시한 영역에는 도메인(엔티티)을 표현할 수 있지만 리포지토리에서 이미 도메인을 설정한 후에 메서드를 사용하기 때문에 중복으로 판단되어 생략하기도 한다.
exists...By
특정 데이터가 존재하는지 확인
boolean existsByNumber(Long number);
count...By
조회 쿼리 수행 후 결과로 나온 레코드 개수 리턴
long countByName(String name);
delete...By
remove...By
삭제 쿼리 수행 리턴 타입이 없거나 삭제한 횟수를 리턴
void deleteByNumber(Long number);
long removeByName(String name);
First(n)...By
Top(n)...By
조회된 결과값의 개수를 제한하는 키워드
List<Product> findFirst5ByName(String name);
List<Product> findTop10ByName(String name);
Is
값의 일치를 조건으로 사용하는 조건자 키워드 입니다. 생략되는 경우가 많고 Equals와 동일한 기능을 수행한다
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
여러 조건을 묶을 때 사용
Product findByNumberAndName(Long number, String name);
Product findByNumberOrName(Long number, String name);
(Is)GreateThan, (Is)LessThan, (Is)Between
숫자나 datatime 컬럼을 대상으로 한 비교 연산에 사용할 수 있는 조건자 키워드.
GreateThan, 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);
(Is)StartingWith(==StartsWith), (Is)EndingWith(==EndsWith),(Is)Containing(==Contains), (Is)Like
컬럼값에서 일부 일치 여부를 확인하는 조건자 키워드이다. SQL쿼리문에서 값의 일부를 포함하는 값을 추출할 때 사용하는 %키워드와 동일한 역할을 한다.
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);
페이징 처리
페이징이란 DB의 레코드를 개수로 나눠 페이지를 구분하는 것을 의미한다. 예를들어 25개의 레코드가 있다면 7개씩, 총 4개의 페이지로 구분하고 그중 특정 페이지를 가져오는 것이다.
JPA에선 페이징 처리를 위해 Page와 Pageable을 사용한다.
Page<Product> findByName(String name, Pageable pageable);
// 호출 방법
Page<Product> productPage = productRepository.findByName("펜", PgeRequest.of(0, 2));
리턴타입으로 Page 객체를 받아야 하기 때문에 Page<Product>로 타입을 선언해서 객체를 리턴받고 Pageable 파라미터를 전달하기 위해 PageRequest 클래스를 사용하면 된다. PageRequest는 Pageable의 구현체이다.
of 메서드는 매개변수에 따라 다양한 형태로 오버로딩 되어있다.
- of(int page, int size) 페이지 번호(0부터), 페이지당 데이터 개수 데이터를 정렬하지 않음
- of(int page, int size, Sort) 페이지번호, 페이지당 데이터 개수 정렬 sort에 의해 정렬
- of(int page, int size, Direction, String...properties) 페이지번호, 페이지당 데이터 개수, 정렬 방향, 속성 Sort.by(direction, properties)에 의해 정렬
@Query 어노테이션 사용하기
DB에서 값을 가져올 때는 메서드 이름만으로 쿼리 메서드를 생성할 수도 있지만 @Query 어노테이션을 사용해 직접 JPQL을 작성할 수도 있다.
JPQL을 사용하면 JPA 구현체에서 자동으로 해석하고 실행하며 만약 DB를 다른 DB로 변경할 일이 없다면 직접 해당 DB에 특화된 SQL을 작성할 수 있고, 주로 튜닝 쿼리를 사용하고자 직접 SQL을 작성한다.
ex)
@Query("SELECT p FRM Product p WHERE p.name = ?1")
List<Product> findByName(String name);
@Query 어노테이션을 사용해 JPQL 형식의 쿼리문을 작성하고 FROM 뒤에서 엔티티 타입을 지정하고 별칭을 생성합니다. WHERE문에서는 SQL과 마찬가지로 조건을 지정한다. 조건문의 '?1'은 파라미터를 전달받기 위한 인자이고 1은 첫 번째 파라미터를 의미하지만 이렇게 사용할 경우 순서가 바뀌면 오류가 발생할 수도 있기 때문에 @Param 어노테이션을 사용하는 것이 좋다.
ex)
@Query("SELECT p FRM Product p WHERE p.name = :name")
List<Product> findByNameParam(@Param("name") String name);
QueryDSL 사용하기
QueryDSL이란 정적 타입을 이용해 SQL과 같은 쿼리를 생성할 수 있도록 지원하는 프레임워크 입니다.
문자열이나 XML 파일을 통해 쿼리를 작성하는 대신 QueryDSL이 제공하는 플루언트 API를 활용해 쿼리를 생성할 수 있다.
장점
- IDE가 제공하는 코드 자동 완성 기능 사용 가능
- 문법적으로 잘못된 쿼리를 허용하지 않으므로 문법 오류를 발생시키지 않음
- 고정된 SQL 쿼리를 작성하지 않기 때문에 동적으로 쿼리를 생성할 수 있음
- 코드로 작성하므로 가독성 및 생산성 향상
- 도메인 타입과 프로퍼티를 안전하게 참조 가능
@SpringBootTest
class ProductRepositoryTest {
@PersistenceContext
EntityManager em;
@Autowired
ProductRepository productRepository;
@Test
void queryDslTest() {
JPAQuery<Product> query = new JPAQuery<>(em);
QProduct qProduct = QProduct.product;
List<Product> productList = query
.from(qProduct)
.where(qProduct.name.eq("펜"))
.orderBy(qProduct.price.asc())
.fetch();
for (Product product : productList) {
System.out.println("============================");
System.out.println();
System.out.println("product.getNumber() = " + product.getNumber());
System.out.println("product.getName() = " + product.getName());
System.out.println("product.getPrice() = " + product.getPrice());
System.out.println("product.getStock() = " + product.getStock());
System.out.println();
System.out.println("============================");
}
}
}
QueryDSL을 사용하려면 JPAQuery 객체를 사용하고 JPAQuery는 엔티티 매니저를 활용해 생성합니다.
예제를 보면 SQL쿼리에서 사용되는 키워드로 메서드가 구성되어 있고 그렇기 때문에 메서드를 활용해서 좀 더 쉽게 코드를 작성할 수 있다.
List타입으로 값을 리턴받기 위해선 fetch() 메서드를 사용하면 된다.
- List<T> fetch() : 조회 결과를 리스트로 반환
- T fetchOne : 단 건의 조회 결과를 반환
- T fetchFirst() : 여러 건의 조회 결과 중 1건을 반환
- Long fetchCount() : 조회 결과의 개수 반환
- QueryResult<T> fetchResults() : 조회 결과 리스트와 개수를 포함한 QueryResults를 반환
JPAQuery 객체를 사용하는 방법 말고 JPAQueryFactory를 활용해서 쿼리를 작성할 수 있는데 JPAQuery와 달리 select 절부터 작성이 가능하다.
@Configuration
public class QueryDSLConfiguration {
@PersistenceContext
EntityManager em;
@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(em);
}
}
위와 같이 JPAQueryFactory 객체를 @Bean 객체로 등록해두면 매번 JPAQueryFactory를 초기화하지 않고 사용가능하다.
@Autowired
JPAQueryFactory jpaQueryFactory;
@Test
void queryDslTest4() {
QProduct qProduct = QProduct.product;
List<String> productList = jpaQueryFactory
.select(qProduct.name)
.from(qProduct)
.where(qProduct.name.eq("펜"))
.orderBy(qProduct.price.asc())
.fetch();
for (String product : productList) {
System.out.println("----------------");
System.out.println("Product Name : " + product);
System.out.println("----------------");
}
}