| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 | 31 |
- Java
- JWT
- 서버
- queue
- 컴퓨터 공학
- 연관관계
- 핵심가이드
- ORM
- error
- QueryDSL
- spring boot
- JPA
- 백준
- 개발자
- 자료구조
- 스프링부트
- 백엔드공부
- 백엔드
- 백엔드스쿨
- LinkedList
- SpringSecurity
- TestCode
- 컴퓨터 구조
- array
- 프로그래머스
- SpringBoot
- restTemplate
- spring
- 제로베이스
- actuator
- Today
- Total
be_better
[Springboot] Spring Data JPA 활용 - @Query 어노테이션 본문
목차
@Query 어노테이션 사용하기
@Query 어노테이션을 사용해 직접 JPQL을 작성할 수도 있다.
JPQL을 사용하면 JPA 구현체에서 자동으로 쿼리 문장을 해석하고 실행하게 된다. 만약 데이터베이스를 다른 데이터베이스로 변경할 일이 없다면 직접 해당 데이터베이스에 특화된 SQL을 작성할 수 있으며, 주로 튜닝된 쿼리를 사용하고나 할 때 직접 SQL을 작성한다.
@Query("(1)SELECT p FROM Product (2)AS p WHERE p.name = (3)?1"
List<Product> findByName(String name);
@Query 어노테이션을 사용해 JPQL 형식의 쿼리문을 작성한다.
(1) 예약어에 해당하는 단어는 소문자로 작성해도 된다.
(2) FROM 뒤에서 엔티티 타입을 지정하고 별칭을 생성하는데 AS는 생략 가능하다.
(3) 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.price, p.stock FROM Product p WHERE p.name = :name")
List<Object[]> findByNameParam2(@Param("name") String name);
이처럼 SELECT에 가져오고자 하는 칼럼을 지정하면 된다. 이 때 메서드에서는 Object 배열의 리스트 형태로 리턴 타입을 지정해야 한다.
QueryDSL 적용하기
메서드의 이름을 기반으로 생성하는 JPQL의 한계는 @Query 어노테이션을 통해 대부분 해소할 수 있지만 직접 문자열을 입력하기 때문에 컴파일 시점에 에러를 잡지 못하고 런타임 에러가 발생할 수 있다. 쿼리의 문자열이 잘못된 경우에는 애플리케이션이 실행된 후 로직이 실행되고 나서야 오류를 발견할 수 있다.
이 같은 문제를 해결하기 위해 사용되는 것이 QueryDSL이다. QueryDSL은 문자열이 아니라 코드로 쿼리를 작성할 수 있도록 도와준다.
QueryDSL이란?
QueryDSL은 정적 타입을 이용해 SQL과 같은 쿼리를 생성할 수 있도록 지원하는 프레임워크이다. 문자열이나 XML 파일을 통해 쿼리를 작성하는 대신 QueryDSL이 제공하는 플루언트(Fluent) API를 활용해 쿼리를 생성할 수 있다.
QueryDSL의 장점
- IDE가 제공하는 코드 자동 완성 기능을 사용할 수 있다.
- 문법적으로 잘못된 쿼리를 허용하지 않는다. 따라서 정상적으로 활용된 QueryDSL은 문법 오류를 발생시키지 않는다.
- 고정된 SQL 쿼리를 작성하지 않기 때문에 동적으로 쿼리를 생성할 수 있다.
- 코드로 작성하므로 가독성 및 생산성이 향상된다.
- 도메인 타입과 프로퍼티를 안전하게 참조할 수 있다.
QueryDSL을 사용하기 위한 프로젝트 생성
[Springboot] QueryDSL 설정 gradle
build.gradle 수정 코드 맨 위 queryDslVersion 추가 buildscript { ext { queryDslVersion = "5.0.0" } } plugin 추가 plugins { id 'java' id 'org.springframework.boot' version '2.7.17' id 'io.spring.dependency-management' version '1.0.15.RELEASE' // qu
jnjeaaaat.tistory.com
QueryDSL은 지금까지 작성했던 엔티티 클래스와 Q도메인(Qdomain)이라는 쿼리 타입의 클래스를 자체적으로 생성해서 메타데이터로 사용하는데, 이를 통해 SQL과 같은 쿼리를 생성해서 제공한다.
기본적인 QueryDSL 사용하기
테스트 코드를 작성해서 QueryDSL의 동작을 확인할 수 있다. ProductRepositoryTest.java에 메서드를 생성한다.
@PersistenceContext
EntityManager entityManager;
@Test
void queryDslTest() {
// (1)
JPAQuery<Product> query = new JPAQuery<>(entityManager);
QProduct qProduct = QProduct.product;
// (2)
List<Product> productList = query
.from(qProduct)
.where(qProduct.name.eq("펜"))
.orderBy(qProduct.price.asc())
.fetch(); // (3)
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("--------------------");
}
}
QueryDSL을 사용하기 위해서는 (1)JPAQuery 객체를 사용한다. JPAQuery는 엔티티 매니저(EntityManager)를 활용해 생성한다. 이렇게 생성된 JPAQuery는 (2)와 같이 빌더 형식으로 쿼리를 작성한다.
List 타입으로 값을 리턴받기 위해서는 (3)과 같이 fetch() 메서드를 사용해야 하는데 만약 4.0.1이전 버전의 QueryDSL을 설정한다면 list() 메서드를 사용해야 한다.
반환 메서드들은 다음과 같다.
- List<T> fetch() : 조회 결과를 리스트로 반환
- T fetchOne : 단 건의 조회 결과를 반환
- T fetchFirst() : 여러 건의 조회 결과 중 1건을 반환. 내부 로직을 살펴보면 '.limit(1).fetchOne()'으로 구현되어 있다.
- Long fetchCount() : 조회 결과의 개수를 반환
- QueryResult<T> fetchResults() : 조회 결과 리스트와 개수를 포함한 QueryResult를 반환
다음은 JPAQueryFactory를 활용해서 작성한 코드이다.
@Test
void queryDslTest2() {
JPAQueryFactory jpaQueryFactory = new JPAQueryFactory(entityManager);
QProduct qProduct = QProduct.product;
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("--------------------");
}
}
JPAQuery와 달리 JPAQueryFactory에서는 select 절 부터 작성 가능하다.
만약 전체 칼럼을 조회하지 않고 일부만 조회하고 싶다면 selectFrom()이 아닌 select()와 from() 메서드를 구분해서 사용하면 된다.
@Test
void queryDslTest3() {
JPAQueryFactory jpaQueryFactory = new JPAQueryFactory(entityManager);
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("--------------------");
}
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 Price : " + product.get(qProduct.price));
System.out.println("--------------------");
}
}
select 대상이 하나인 경우는 List<String> 타입으로 받으면 되고, 여러 개일 경우에는 쉼표(,)로 구분해서 작성하되, List<String> 타입이 아닌 List<Tuple> 타입으로 지정한다.
QueryDSL을 실제 비즈니스 로직에서 활용할 수 있게 설정하려면 먼저 컨피그 클래스를 생성한다.
config 패키지를 생성하고 QueryDSLConfiguration 클래스를 생성한다.
@Configuration
public class QueryDSLConfiguration {
@PersistenceContext
EntityManager entityManager;
@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(entityManager);
}
}
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("--------------------");
}
}
QuerydslPredicateExecutor, QuerydslRepositorySupport 활용
스프링 데이터 JPA에서는 QueryDSL을 더욱 편하게 사용할 수 있게 QuerydslPredicateExecutor 인터페이스와 QuerydslRepositorySupport 클래스를 제공한다.
QuerydslPredicateExecutor 인터페이스
QuerydslPredicateExecutor는 JpaRepository와 함께 리피지토리에서 QueryDSL을 사용할 수 있게 인터페이스를 제공한다.
public interface QProductRepository extends JpaRepository<Product, Long>,
QuerydslPredicateExecutor<Product>{
}
QuerydslPredicateExecutor를 상속받도록 설정한 Product 엔티티에 대한 리포지토리이다.
QuerydslPredicateExecutor 인터페이스는 다음과 같은 다양한 메서드를 제공한다.
Optional<T> findOne(Predicate predicate);
Iterable<T> findAll(Predicate predicate);
Iterable<T> findAll(Predicate predicate, Sort sort);
Iterable<T> findAll(Predicate predicate, OrderSpecifier<?>... orders);
Iterable<T> findAll(OrderSpecifier<?>... orders);
Page<T> findAll(Predicate predicate, Pageable pageable);
long count(Predicate predicate);
boolean exists(Predicate predicate);
<S extends T, R> R findBy(Predicate predicate, Function<FluentQuery.FetchableFluentQuery<S>, R> queryFunction);
보다시피 QuerydslPredicateExecutor 인터페이스의 메서드는 대부분 Predicate타입을 매개변수로 받는다. Predicate는 표현식을 작성할 수 있게 QueryDSL에서 제공하는 인터페이스이다.
Test 클래스 생성 및 findOne() 메서드 호출 방법
@SpringBootTest
public class QProductRepositoryTest {
@Autowired
QProductRepository qProductRepository;
@Test
void queryDSLTest1() {
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());
}
}
}
Predicate는 간단하게 표현식으로 정의하는 쿼리로 생각하면 된다. 위의 코드는 Predicate를 명시적으로 정의하고 사용했지만 아래 코드처럼 서술부만 가져다 사용할 수도 있다.
@Test
void queryDSLTest2() {
QProduct qProduct = QProduct.product;
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());
}
}
QuerydslPredicateExecutor를 활용하면 더욱 편하게 QueryDSL를 사용할 수 있지만 join이나 fetch 기능은 사용할 수 없다는 단점이 있다.
QuerydslRepositorySupport 추상 클래스 사용하기
QuerydslRepositorySupport 클래스 역시 QuerySDL 라이브러리를 사용하는데 유용한 기능을 제공한다.
가장 보편적인 사용방식은 CustomRepository를 활용해 리포지토리를 구현하는 방식이다. 지금까지 예로 든 Product 엔티티를 활용하기 위한 객체들의 상속 구조를 살펴보면 다음과 같다.

여기서 JpaRepository와 QuerydslRepositorySupport는 Spring Data JPA에서 제공하는 인터페이스와 클래스이다. 나머지 ProductRepository와 ProductRepositoryCustom, ProductRepositoryCustomImpl은 직접 구현해야 한다.
간단한 구조로는 다음과 같다.
- 먼저 앞에서 사용했던 방식처럼 JpaRepository를 상속받는 ProductRepository를 생성한다.
- 이때 직접 구현한 쿼리를 사용하기 위해서는 JpaRepository를 상속받지 않는 리포지토리 인터페이스인 ProductRepositoryCustom을 생성한다. 이 인터페이스에 정의하고자 하는 기능들을 메서드로 정의한다.
- ProductRepositoryCustom에서 정의한 메서드를 사용하기 위해 ProductRepository에서 ProductRepositoryCustom을 상속받는다.
- ProductRepositoryCustom에서 정의된 메서드를 기반으로 실제 쿼리 작성을 하기 위해 구현체인 ProductRepositoryCustomImpl 클래스를 생성한다.
- ProductRepositoryCustomImpl 클래스에서는 다양한 방법으로 쿼리를 구현할 수 있지만 QueryDSL을 사용하기 위해 QueryDslRepositorySupport를 상속받는다.
위와 같이 구성하면 DAO나 서비스에서 리포지토리를 접근하기 위해 ProductRepository를 사용하는데 이를 활용함으로써 QueryDSL의 기능도 사용할 수 있게 된다.
프로젝트의 repository 패키지 안에 support 패키지 않에 ProductRepository, ProductRepositoryCustom, ProductRepositoryCustomImpl 총 3개의 인터페이스와 클래스를 생성한다.
ProductRepositoryCustom
public interface ProductRepositoryCustom {
List<Product> findByName(String name);
}
위와 같이 인터페이스를 생성하고 쿼리로 구현하고자 하는 메서드를 정의하는 작업을 수행한다.
ProductRepositoryCustomImpl 클래스
@Component
public class ProductRepositoryCustomImpl extends QuerydslRepositorySupport
implements ProductRepositoryCustom {
public ProductRepositoryCustomImpl() {
super(Product.class);
}
@Override
public List<Product> findByName(String name) {
QProduct product = QProduct.product;
List<Product> productList = from(product)
.where(product.name.eq(name))
.select(product)
.fetch();
return productList;
}
}
ProductRepositoryCustomImpl 클래스에서는 QueryDSL을 사용하기 위해 QueryDslRepositorySupport를 상속받고 ProductRepositoryCustom 인터페이스를 구현한다.
QueryDslRepositorySupport를 상속받으면 생성자를 통해 도메인 클래스를 부모 클래스에 전달해야 한다.
기존의 ProductRepository와 구분하기 위해 새로운 ProductRepository를 별도로 생성한다. 이 때 빈 생성 시 이름 충돌을 방지하기 위해 @Repository("productRepositorySupport")와 같이 별도의 이름을 지정해줘야 한다.
@Repository("productRepositorySupport")
public interface ProductRepository extends JpaRepository<Product, Long>,
ProductRepositoryCustom {
}
ProductRepository의 findByName() 메서드 테스트 코드
@SpringBootTest
class ProductRepositoryTest {
@Autowired
ProductRepository productRepository;
@Test
void findByNameTest() {
List<Product> productList = productRepository.findByName("펜");
for (Product product : productList) {
System.out.println(product.getName());
System.out.println(product.getPrice());
System.out.println(product.getStock());
}
}
}'Springboot > 이론' 카테고리의 다른 글
| [Springboot] Spring Data JPA 활용 - JPA Auditing (0) | 2023.11.12 |
|---|---|
| [Springboot] QueryDSL 설정 gradle (0) | 2023.11.12 |
| [Springboot] Spring Data JPA 활용 - 정렬과 페이징 (0) | 2023.11.10 |
| [Springboot] Spring Data JPA 활용 - JPQL (0) | 2023.11.10 |
| [Springboot] 데이터베이스 연동 - 프로젝트 적용 (0) | 2023.11.05 |