정적 타입을 이용해 SQL과 같은 쿼리를 생성할 수 있도록 지원하는 프레임워크로, 문자열이나 XML 파일을 통해 쿼리를 작성하는 대신 QueryDSL이 제공하는 플루언트(Fluent) API를 활용해 쿼리를 생성할 수 있음
pom.xml
(classifier를 jakarta로 명시해주어야 함)
<!--QueryDSL을 사용하기 위한 디펜던시 추가-->
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-jpa</artifactId>
<version>5.0.0</version>
<classifier>jakarta</classifier>
</dependency>
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
<version>5.0.0</version>
<classifier>jakarta</classifier>
</dependency>
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-core</artifactId>
<version>5.0.0</version>
</dependency>
...
...
...
<!-- QueryDSL을 사용하기 위한 플러그인 추가 -->
<plugin>
<groupId>com.mysema.maven</groupId>
<artifactId>apt-maven-plugin</artifactId>
<version>1.1.3</version>
<executions>
<execution>
<goals>
<goal>process</goal>
</goals>
<configuration>
<outputDirectory>target/generated-sources/java</outputDirectory>
<processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
</configuration>
</execution>
</executions>
</plugin>
APT(Annotation Processing Tool)는 어노테이션으로 정의된 코드를 기반으로 새로운 코드를 생성하는 기능임
위 코드에서 JPAAnnotationProcessor는 @Entity 어노테이션으로 정의된 엔티티 클래스를 찾아서 쿼리 타입을 생성함
이후 'Generate Sources ...' 실행
아래 경로에 Q도메인 클래스가 생성됨
우선 테스트 코드로 기본적인 QueryDSL 사용법을 알아보자
// QueryDSL에 의해 생성된 Q도메인 클래스를 활용하는 코드
@PersistenceContext
EntityManager entityManager;
@Test
void queryDslTest() {
JPAQuery<Product> query = new JPAQuery(entityManager);
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 Number : " + product.getNumber());
System.out.println("Product Name : " + product.getName());
System.out.println("Product Price : " + product.getPrice());
System.out.println("Product Stock : " + product.getStock());
System.out.println();
System.out.println("----------------");
}
}
반환 메서드 종류
- List<T> fetch(): 조회 결과를 리스트로 반환
- T fetchOne(): 단 건의 조회 결과 반환
- T fetchFirst(): 여러 건의 조회 결과 중 1건을 반환 -> .limit(1).fetchOne()
- Long fetchCount(): 조회 결과의 개수를 반환
- QueryResult<T> fetchResults: 조회 결과 리스트와 개수를 포함한 QueryResults 반환
@Test
void queryDslTest2() {
JPAQueryFactory jpaQueryFactory = new JPAQueryFactory(entityManager);
QProduct qProduct = QProduct.product;
// JPAQueryFactory에서는 select 절부터 작성 가능
List<Product> productList = jpaQueryFactory.selectFrom(qProduct)
.where(qProduct.name.eq("펜"))
.orderBy(qProduct.price.asc())
.fetch();
for (Product product : productList) {
System.out.println("----------------");
System.out.println();
System.out.println("Product Number : " + product.getNumber());
System.out.println("Product Name : " + product.getName());
System.out.println("Product Price : " + product.getPrice());
System.out.println("Product Stock : " + product.getStock());
System.out.println();
System.out.println("----------------");
}
}
@Test
void queryDslTest3() {
JPAQueryFactory jpaQueryFactory = new JPAQueryFactory(entityManager);
QProduct qProduct = QProduct.product;
// 일부 컬럼 조회를 위해 select와 from 메서드 구분해서 사용
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("----------------");
}
// 조회 대상이 여러 개일 경우 쉼표로 구분, 또한 리턴 타입을 List<Tuple>로 지정 (not List<String>)
List<Tuple> tupleList = jpaQueryFactory
.select(qProduct.name, qProduct.price)
.from(qProduct)
.where(qProduct.name.eq("펜"))
.orderBy(qProduct.price.asc())
.fetch();
for (Tuple product : tupleList) {
System.out.println("----------------");
System.out.println("Product Name : " + product.get(qProduct.name));
System.out.println("Product Name : " + product.get(qProduct.price));
System.out.println("----------------");
}
}
config/QueryDSLConfiguration.java
@Configuration
public class QueryDSLConfiguration {
@PersistenceContext
EntityManager entityManager;
@Bean
public JPAQueryFactory jpaQueryFactory(){
return new JPAQueryFactory(entityManager);
}
}
위와 같이 JPAQueryFactory 객체를 @Bean 객체로 등록해두면 앞에서 작성한 예제처럼 매번 JPAQueryFactory를 초기화하지 않고 스프링 컨테이너에서 가져다 쓸 수 있음 (아래 코드 참고)
// 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("----------------");
}
}
스프링 데이터 JPA에서 QueryDSL을 더 편하게 사용할수 있도록 QuerydslPredicateExecutor 인터페이스와 QuerydslRepositorySupport 클래스를 제공함
JPARepository와 함께 리포지토리에서 QueryDSL을 사용할 수 있게 인터페이스를 제공함
QuerydslPredicateExecutor를 사용하는 리포지토리(QProductRepository) 생성
public interface QProductRepository extends JpaRepository<Product, Long>,
QuerydslPredicateExecutor<Product> {
}
- QuerydslPredicateExecutor에서 제공하는 메서드
Predicate는 표현식을 작성할 수 있게 QueryDSL에서 제공하는 인터페이스임
QProductRepositoryTest 클래스 생성 및 테스트 코드 작성(활용)
@SpringBootTest
public class QProductRepositoryTest {
@Autowired
QProductRepository qProductRepository;
@Test
public void queryDSLTest1() { // findOne()
// Predicate 명시적으로 정의함
Predicate predicate = QProduct.product.name.containsIgnoreCase("펜")
.and(QProduct.product.price.between(1000, 2500));
Optional<Product> foundProduct = qProductRepository.findOne(predicate);
if(foundProduct.isPresent()){
Product product = foundProduct.get();
System.out.println(product.getNumber());
System.out.println(product.getName());
System.out.println(product.getPrice());
System.out.println(product.getStock());
}
}
@Test
public void queryDSLTest2() { // findAll()
QProduct qProduct = QProduct.product;
// Predicate 서술부만 가져다 사용함
Iterable<Product> productList = qProductRepository.findAll(
qProduct.name.contains("펜")
.and(qProduct.price.between(550, 1500))
);
for (Product product : productList) {
System.out.println(product.getNumber());
System.out.println(product.getName());
System.out.println(product.getPrice());
System.out.println(product.getStock());
}
}
}
QueryDSL을 더욱 편하게 사용할 수 있지만, join이나 fetch 기능은 사용할 수 없다는 단점이 있음
가장 보편적으로 사용하는 방식인 CustomRepository를 활용해 리포지토리를 구현해보자
위에서 예로 든 Product 엔티티를 활용하기 위한 객체들의 상속구조는 아래와 같음
JpaRepository와 QuerydslRepositorySupport는 Spring Data JPA에서 제공하는 인터페이스와 클래스이며, 나머지는 직접 구현해야 함
위와 같이 구성하면 DAO나 서비스에서 리포지토리에 접근하기 위해 ProductRepository를 사용하며 따라서 QueryDSL의 기능도 사용할 수 있게 됨
repository/support/ProductRepositoryCustom.java
// 필요한 쿼리를 작성할 메소드를 정의하는 인터페이스
public interface ProductRepositoryCustom {
List<Product> findByName(String name);
}
repository/support/ProductRepositoryCustomImpl.java
@Component
public class ProductRepositoryCustomImpl extends QuerydslRepositorySupport implements
ProductRepositoryCustom {
// 생성자를 통해 도메인 클래스를 부모 클래스에 전달애햐 함
public ProductRepositoryCustomImpl() {
super(Product.class);
}
// 인터페이스에 정의한 메서드를 구현
@Override
public List<Product> findByName(String name) {
QProduct product = QProduct.product;
// Q도메인 클래스를 사용해 QuerydslRepositorySupport가 제공하는 기능 사용
List<Product> productList = from(product)
.where(product.name.eq(name))
.select(product)
.fetch();
return productList;
}
}
repository/support/ProductRepository.java
(기존 Product 엔티티 클래스와 매핑해서 사용하던 ProductRepository 사용 가능하나 예시를 위해 별도로 생성함)
import com.springboot.advanced_jpa.data.entity.Product;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
// repository.ProductRepository와 이름이 동일하여 Bean 생성 충돌이 발생하므로 Bean 이름을 지정함
@Repository("productRepositorySupport")
public interface ProductRepository extends JpaRepository<Product, Long>, ProductRepositoryCustom {
}
위 코드를 사용할 때는 ProductRepository만 이용하면 됨
ProductRepositoryCustom에서 정의한 findByName()을 테스트해보자
@SpringBootTest
public class ProductRepositoryTest {
@Autowired
ProductRepository productRepository;
@Test
void findByNameTest(){
List<Product> productList = productRepository.findByName("펜");
for(Product product : productList){
System.out.println(product.getNumber());
System.out.println(product.getName());
System.out.println(product.getPrice());
System.out.println(product.getStock());
}
}
}
리포지토리를 구성하면서 모든 로직을 구현했기 때문에 findByName() 메서드를 사용할 때는 위와 같이 간단하게 구현해 사용할 수 있음