Book/스프링부트 핵심가이드

@Query 어노테이션 관련 내용 정리

블로그 주인장 2023. 11. 9.

@Query 어노테이션 사용하기

메서드의 이름만으로 쿼리 메서드를 생성할 수 있다.

@Query 어노테이션을 사용해 직접 JPQL을 작성할 수 있는데 알아보겠습니다.


@Query 어노테이션


  • JPQL을 사용하면 JPA 구현체에서 자동으로 쿼리 문장을 해석하고 실행하게 된다.
  • 만약 데이터베이스를 다른 데이터베이스로 변경할 일이 없다면 직접 해당 데이터베이스에 특화된 SQL을 작성할 수 있다.
  • 주로 튜닝된 쿼리를 사용하고자 할 때에 직접 SQL을 작성한다.

 

@Query 어노테이션을 사용하는 메서드

@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
    @Query(" select p from Product as p where p.name = ?1")
    List<Product> findByName(String name);
}
  • @Query 어노테이션을 사용해 JPQL 형식의 쿼리문을 작성한다.
  • From 뒤에 엔티티 타입을 지정하고, 별칭을 생성한다. (As 는 생략 가능하다)
  • Where 문에서는 SQL과 마찬가지로 조건을 지정한다.
  • 조건문에서 사용한 '?1' 은 파라미터를 전달받기 위한 인자에 해당한다.
  • 1 은 첫 번째 파라미터를 의미한다.

 

@Query 어노테이션과 @Param 어노테이션을 사용한 메서드

@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
    @Query(" select p from Product as p where p.name = ?1")
    List<Product> findByNameParam(@Param("name") String name);
}
  • 파라미터의 순서가 바뀌면 오류가 발생할 가능성이 있으므로 @Param 어노테이션을 사용하는 것이 좋다.
  • 파라미터를 바인딩하는 방식으로 메서드를 구현하면 코드의 가독성이 높아지고 유지보수가 수월해진다.

 

특정 컬럼만 추출하는 쿼리

@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
    @Query(" select p.name, p.price, p.stock from Product p where p.name = :name ")
    List<Product> findByNameParam2(@Param("name") String name);
}
  • Select 에 가져오고자 하는 컬럼을 지정하면 된다.
  • Object 배열의 리스트 형태로 리턴 타입을 지정해야한다.

 

QueryDSL 적용하기


  • 정적 타입을 이용해 SQL과 같은 쿼리를 생성할 수 있도록 지원하는 프레임워크이다.
  • 문자열이나 XML 파일을 통해 쿼리를 작성하는 대신 QueryDSL이 제공하는 플루언트(Fluent) API를 활용하여 쿼리를 생성할 수 있다.

 

QueryDSL의 장점

  1. IDE가 제공하는 코드 자동 완성 기능을 사용할 수 있다.
  2. 문법적으로 잘못된 쿼리를 허용하지 않는다. 따라서 정상적인 QueryDSL은 문법 오류를 발생시키지 않는다.
  3. 고정된 SQL 쿼리를 작성하지 않기 때문에 동적으로 쿼리를 생성할 수 있다.
  4. 코드를 작성하므로 가독성 및 생산성이 향상된다.
  5. 도메인 타입과 프로퍼티를 안전하게 참조할 수 있다.

 

QueryDSL  의존성 추가(pom.xml)

<!-- QueryDSL 추가-->
<dependency>
    <groupId>com.querydsl</groupId>
    <artifactId>querydsl-apt</artifactId>
    <scope>provided</scope>
</dependency>
<dependency>
    <groupId>com.querydsl</groupId>
    <artifactId>querydsl-jpa</artifactId>
</dependency>

 

QueryDSL  플러그인 추가(pom.xml)

<!-- 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>
                <options>
                    <querydsl.entityAccessors>true</querydsl.entityAccessors>
                </options>
            </configuration>
        </execution>
    </executions>
</plugin>

 

QueryDSL  확인

  1. Maven -> Lifecycle(수명 주기)  -> [complie] 클릭하여 빌드 작업 진행
  2. plugin에 지정했던 generated-source 경로에 Q도메인 클래스 생성

 

QueryDSL  설정

  • QueryDSL은 지금까지 작성했던 엔티티 클래스와 Q도메인(Qdomain)이라는 쿼리 타입의 클래스를 자체적으로 생성하여 메타데이터로 사용하는데, 이를 SQL과 같은 쿼리를 생성해서 제공한다.
  • Q도메인이 클래스가 제대로 생성되지 않으면 [프로젝트 폴더 오른쪽 마우스 클릭] -> [Maven] -> [Generate Sources and Update Folders]를 선택한다.

QueryDSL  IDE 설정

  • [File] -> [Project Structure] -> [Module] 탭을 클릭한다.
  • generated-sources 폴더를 눌러 소스파일을 인식할 수 있게 [Sources] 을 클릭한다.

 

 

QueryDSL  코드 작성


JPAQuery를 활용한 QueryDSL 테스트 코드

@SpringBootTest
class ProductRepositoryTest {

    @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("pen"))
                .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("====================");
        }
    }
}
  • QueryDSL에 의해 생성된 Q도메인 클래스를 활용하는 코드이다.
  • QueryDSL을 사용하기 위해서는 JPAQuery 객체를 사용한다.
  • JPAQuery는 엔티티 매니저(EntityManager)를 활용하여 생성한다.
  • 빌더 메서드에서 확인할 수 있듯이, SQL 쿼리에서 사용되는 키워드로 메서드가 구성되어있다.
  • List 타입으로 값을 리턴받기 위해서는 fetch() 메서드를 사용한다.
  • ver 4.0.1 이전인 경우 list() 메서드를 사용해야한다.

 

JPAQuery 메서드 종류

  1. List<T> fetch() : 조회 결과를 리스트로 반환한다.
  2. T fetchOne : 단 건의 조회 결과를 반환한다.
  3. T fetchFirst() : 여러 건의 조회 결과 중 1건을 반환한다. 내부 로직 상 ' .limit(1).fetchOne()' 으로 구성되어있다.
  4. Long fetchCount() : 조회 결과의 개수를 반환한다.
  5. QueryResult<T> fetchResults() : 조회 결과 리스트와 개수를 포함한 QueryResults 를 반환한다.

 

JPAQueryFactory를 활용한 QueryDSL 테스트 코드

  • JPAQuery를 사용했을 떄와 달리 JPAQueryFactory에서는 select 절부터 작성이 가능하다.
  • 일부 컬럼 조회를 할 경우 selectFrom( ) 이 아닌 select( ) 와 from( ) 메서드를 구분해서 사용하면 된다.
@SpringBootTest
class ProductRepositoryTest {

    @PersistenceContext
    EntityManager entityManager;

    @Test
    void queryDslTest2() {
        JPAQueryFactory jpaQueryFactory = new JPAQueryFactory(entityManager);
        QProduct qProduct = QProduct.product;

        List<Product> productList = jpaQueryFactory.selectFrom(qProduct)
                .where(qProduct.name.eq("pen"))
                .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("====================");
        }
    }
}

 

JPAQueryFactory의 select() 메서드 테스트

  • 조회 대상이 여러 개일 경우에는 쉼표(,)로 구분하여 작성하면 된다.
  • List<Tuple> 타입으로 지정한다.
@SpringBootTest
class ProductRepositoryTest {

    @PersistenceContext
    EntityManager entityManager;

    @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("pen"))
                .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("pen"))
                .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("====================");
        }
    }
}

 

QueryDSL Config 파일 생성

  • JPAQueryFactory 객체를 @Bean 객체로 등록하면 초기화하지 않고 스프링 컨테이너에서 가져다 쓸 수 있다.
@Configuration
public class QueryDSLConfiguration {

    @PersistenceContext
    EntityManager entityManager;

    @Bean
    public JPAQueryFactory jpaQueryFactory() {
        return new JPAQueryFactory(entityManager);
    }
}

 

JPAQueryFactory @Bean 파일 활용한 테스트

  • JPAQueryFactory를 의존성 주입하여 쿼리를 작성한다.
@SpringBootTest
class ProductRepositoryTest {

    @Autowired
    JPAQueryFactory jpaQueryFactory;

    @Test
    void queryDslTest4() {
        QProduct qProduct = QProduct.product;

        List<String> productList = jpaQueryFactory
                .select(qProduct.name)
                .from(qProduct)
                .where(qProduct.name.eq("pen"))
                .orderBy(qProduct.price.asc())
                .fetch();

        for (String product : productList) {
            System.out.println("====================");
            System.out.println("Product Name : " + product);
            System.out.println("====================");
        }
    }
}

 

QuerydslPredicateExecutor


QueryDSL을 더욱 편하게 사용할 수 있는 클래스를 활용해보겠습니다.

 

QuerydslPredicateExecutor

  • JpaRepository와 함께 리포지토리에서 QueryDSL을 사용할 수 있게 인터페이스를 제공한다.
@Repository
public interface QProductRepository extends JpaRepository<Product, Long>,
    QuerydslPredicateExecutor<Product>{
    
}
  • 대부분 Predicate 타입으로 매개변수를 받는다.
  • Predicate : 표현식을 작성할 수 있게 QueryDSL에서 제공하는 인터페이스이다.

QuerydslPredicateExecutor 테스트 코드

@SpringBootTest
class QProductRepositoryTest {

    @Autowired
    QProductRepository qProductRepository;

    @Test
    void queryDSLTest1() {
        Predicate predicate = QProduct.product.name.containsIgnoreCase("pen")
                .and(QProduct.product.price.between(1000, 2500));

        Optional<Product> foundProduct = qProductRepository.findOne(predicate);

        if (foundProduct.isPresent()) {
            System.out.println(foundProduct.get().getNumber());
            System.out.println(foundProduct.get().getName());
            System.out.println(foundProduct.get().getPrice());
            System.out.println(foundProduct.get().getStock());
        }
    }
}

 

 

 QuerydslRepositorySupport 활용


QuerydslRepositorySupport 추상 클래스

  • CustomRepository를 활용하여 리포지토리를 구현하는 방식이 보편적이다.
  • QuerydslRepositorySupport 를 사용하기 위해서는 CustomRepository와 CustromImpl을 직접 구현해야한다.

 

QuerydslRepositoryCustomImpl 클래스

  • QueryDSL을 사용하기 위해 QuerydslRepositorySupport 를 상속받고 인터페이스를 구현한다.
  • QuerydslRepositorySupport 를 상속받으면 생성자를 통해 도메인 클래스를 부모 클래스에 전달해야한다.( super( ) )
  • from( )  : 어떤 도메인에 접근할 것인지 지정하고, JPAQuery로 리턴한다.
@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;
    }
}

 

ProductRepository 설정

@Repository("productRepositorySupport")
public interface ProductRepositorySupport extends JpaRepository<Product, Long>
        , ProductRepositoryCustom {
}

 

ProductRepository 메서드 테스트

@SpringBootTest
class ProductRepositorySupportTest {

    @Autowired
    ProductRepositorySupport productRepositorySupport;

    @Test
    void findByNameTest() {
        List<Product> productList = productRepositorySupport.findByName("pen");

        for (Product product : productList) {
            System.out.println(product.getNumber());
            System.out.println(product.getName());
            System.out.println(product.getPrice());
            System.out.println(product.getStock());
        }
    }
}
반응형

댓글