Using @Query

2024. 10. 20. 18:52Spring Boot/Spring Data JPA

@Query 어노테이션은 Named Query를 사용하는 전통적인 방식과는 다른 유연한 쿼리 정의 방법을 제공합니다. 이를 통해 쿼리를 리포지토리 인터페이스에 직접 정의할 수 있습니다.

1. @Query 어노테이션의 기본 개념

  • Named Query와의 차이: Named Query는 도메인 클래스에 선언하여 사용할 수 있는 쿼리입니다. 반면, @Query 어노테이션을 사용하면 쿼리를 직접 리포지토리 인터페이스의 메서드에 정의할 수 있습니다. 이렇게 하면 도메인 클래스가 영속성 관련 정보로부터 자유로워지고, 쿼리와 관련된 코드를 리포지토리와 함께 묶어둘 수 있습니다.
  • 우선 순위: @Query 어노테이션이 선언된 메서드는 Named Query나 orm.xml에 정의된 Named Query보다 우선 순위가 높습니다. 즉, 동일한 메서드 이름으로 Named Query가 존재하더라도, @Query 어노테이션이 붙은 메서드가 호출될 때는 @Query에서 정의한 쿼리가 사용됩니다.

2. @Query 사용 예시

다음은 @Query 어노테이션을 사용하여 쿼리를 정의하는 예시입니다:

public interface UserRepository extends JpaRepository<User, Long> {

    @Query("select u from User u where u.emailAddress = ?1")
    User findByEmailAddress(String emailAddress);
}

위 코드에서 findByEmailAddress 메서드는 User 엔티티의 emailAddress 속성과 일치하는 사용자를 조회하는 JPQL 쿼리를 실행합니다. 여기서 ?1은 메서드의 첫 번째 파라미터를 나타냅니다.

Applying a QueryRewriter

쿼리 재작성 기능은 EntityManager에 쿼리가 전송되기 직전에 쿼리를 수정할 수 있는 기능입니다. 이 기능을 사용하면 마지막 순간에 쿼리를 변경할 수 있습니다.

@Query와 QueryRewriter 사용 예시

다음 예시는 QueryRewriter를 사용하여 쿼리를 재작성하는 방법을 보여줍니다:

public interface MyRepository extends JpaRepository<User, Long> {

    @Query(value = "select original_user_alias.* from SD_USER original_user_alias",
           nativeQuery = true,
           queryRewriter = MyQueryRewriter.class)
    List<User> findByNativeQuery(String param);

    @Query(value = "select original_user_alias from User original_user_alias",
           queryRewriter = MyQueryRewriter.class)
    List<User> findByNonNativeQuery(String param);
}

위 코드에서 findByNativeQuery 메서드는 네이티브 SQL 쿼리를 사용하여 데이터를 조회합니다. 이때 MyQueryRewriter 클래스를 통해 쿼리가 전송되기 전에 쿼리가 재작성됩니다.

QueryRewriter 구현 예시

쿼리 재작성기를 구현하는 예시는 다음과 같습니다:

public class MyQueryRewriter implements QueryRewriter {

    @Override
    public String rewrite(String query, Sort sort) {
        return query.replaceAll("original_user_alias", "rewritten_user_alias");
    }
}

위 코드에서 MyQueryRewriter 클래스는 QueryRewriter 인터페이스를 구현하여 rewrite 메서드를 정의합니다. 이 메서드는 original_user_aliasrewritten_user_alias로 교체하는 역할을 합니다.

QueryRewriter 등록

QueryRewriter를 사용할 때는 해당 클래스가 Spring 애플리케이션 컨텍스트에 등록되어 있어야 합니다. 이를 위해 @Component 어노테이션을 사용하거나 @Configuration 클래스의 @Bean 메서드로 정의할 수 있습니다.

리포지토리에서 QueryRewriter 제공

리포지토리 자체가 QueryRewriter 인터페이스를 구현할 수도 있습니다:

public interface MyRepository extends JpaRepository<User, Long>, QueryRewriter {

    @Query(value = "select original_user_alias.* from SD_USER original_user_alias",
           nativeQuery = true,
           queryRewriter = MyRepository.class)
    List<User> findByNativeQuery(String param);

    @Query(value = "select original_user_alias from User original_user_alias",
           queryRewriter = MyRepository.class)
    List<User> findByNonNativeQuery(String param);

    @Override
    default String rewrite(String query, Sort sort) {
        return query.replaceAll("original_user_alias", "rewritten_user_alias");
    }
}

위 코드에서 MyRepository 인터페이스는 QueryRewriter를 구현하여 rewrite 메서드를 오버라이드합니다. 이 방법을 사용하면 QueryRewriter를 별도로 작성하지 않고도 쿼리를 재작성할 수 있습니다.

Using Advanced LIKE Expressions

Spring Data JPA에서 @Query를 사용하여 쿼리를 수동으로 정의할 때, 고급 LIKE 표현식을 정의할 수 있는 기능이 있습니다. 이 기능은 LIKE 쿼리에서 패턴을 사용하여 문자열 검색을 더 유연하게 만들어줍니다.

1. 고급 LIKE 표현식 정의

예를 들어, 사용자의 이름이 특정 패턴으로 끝나는지를 확인하는 쿼리를 정의할 수 있습니다. 다음은 @Query를 사용하여 LIKE 표현식을 활용한 예시입니다.

public interface UserRepository extends JpaRepository<User, Long> {

    @Query("select u from User u where u.firstname like %?1")
    List<User> findByFirstnameEndsWith(String firstname);
}

2. 쿼리 설명

  • 쿼리 구조: 위의 예에서 @Query 어노테이션으로 정의된 쿼리는 사용자의 이름(firstname)이 특정 문자열로 끝나는지를 확인하는 쿼리입니다.
  • LIKE 패턴: % 기호는 SQL에서 와일드카드(wildcard)로 사용되어, 어떤 문자열이든 올 수 있음을 나타냅니다. 여기서 ?1은 메서드의 첫 번째 파라미터를 의미합니다.
  • 변경된 쿼리: 이 쿼리는 JPQL 쿼리로 변환되며, % 문자가 자동으로 인식되고 쿼리 실행 시 파라미터에 추가됩니다. 즉, 사용자가 메서드를 호출할 때 제공한 문자열이 이 % 기호와 결합되어 실제 쿼리에서 사용됩니다.

3. 예시 사용

만약 findByFirstnameEndsWith 메서드를 호출하여 "son"이라는 문자열을 전달한다고 가정해 보겠습니다. 이 경우 최종적으로 실행되는 쿼리는 다음과 같이 변환됩니다:

SELECT u FROM User u WHERE u.firstname LIKE '%son'

이 쿼리는 사용자의 이름이 "son"으로 끝나는 모든 User 엔티티를 검색합니다.

4. 결과

  • 유연한 검색: 이렇게 고급 LIKE 표현식을 사용하면 사용자가 원하는 패턴에 맞는 결과를 더 유연하게 검색할 수 있습니다. 단순한 문자열 일치를 넘어서서, 부분 문자열 검색이 가능해집니다.
  • 편리한 메서드 호출: 사용자는 간단하게 메서드를 호출하여 복잡한 쿼리를 수동으로 작성할 필요 없이 원하는 결과를 얻을 수 있습니다.

 

Spring Data JPA의 @Query 어노테이션을 사용하면 LIKE 표현식을 쉽게 정의하고 활용할 수 있습니다. 이 기능은 특히 동적 검색이 필요한 경우에 매우 유용하며, 코드의 가독성과 유지보수성을 높여주는 효과가 있습니다. 이를 통해 데이터베이스와의 상호작용을 더욱 간결하고 직관적으로 만들 수 있습니다.

Native Queries

Spring Data JPA에서 @Query 어노테이션을 사용하면 네이티브 쿼리를 실행할 수 있습니다. 네이티브 쿼리는 데이터베이스에 직접적인 SQL 쿼리를 작성하여 실행하는 방법입니다. 아래에서 이와 관련된 다양한 예제와 설명을 자세히 살펴보겠습니다.

1. 네이티브 쿼리 정의

네이티브 쿼리를 정의하려면 @Query 어노테이션의 nativeQuery 플래그를 true로 설정해야 합니다. 다음은 이메일 주소로 사용자를 찾는 네이티브 쿼리의 예입니다.

public interface UserRepository extends JpaRepository<User, Long> {

    @Query(value = "SELECT * FROM USERS WHERE EMAIL_ADDRESS = ?1", nativeQuery = true)
    User findByEmailAddress(String emailAddress);
}
  • 쿼리 설명: 위의 쿼리는 데이터베이스의 USERS 테이블에서 EMAIL_ADDRESS가 주어진 인자와 일치하는 사용자를 검색합니다.

2. 네이티브 쿼리에서 동적 정렬 지원

현재 Spring Data JPA는 네이티브 쿼리에서 동적 정렬을 지원하지 않습니다. 이는 실제 SQL 쿼리를 조작해야 하는데, 이는 네이티브 SQL에서는 신뢰할 수 없기 때문입니다. 하지만, 페이지네이션은 가능합니다. 다음은 페이지네이션을 위해 카운트 쿼리를 직접 지정한 예입니다.

public interface UserRepository extends JpaRepository<User, Long> {

    @Query(value = "SELECT * FROM USERS WHERE LASTNAME = ?1",
           countQuery = "SELECT count(*) FROM USERS WHERE LASTNAME = ?1",
           nativeQuery = true)
    Page<User> findByLastname(String lastname, Pageable pageable);
}
  • 쿼리 설명: 이 쿼리는 주어진 LASTNAME으로 사용자를 검색하는 네이티브 쿼리와 해당 사용자의 수를 세기 위한 카운트 쿼리를 동시에 정의합니다. 이처럼 네이티브 쿼리와 카운트 쿼리를 함께 사용하여 페이지네이션 기능을 구현할 수 있습니다.

3. 네이티브 쿼리 파서 설정

Spring Data JPA에서는 네이티브 쿼리 파서로 JSqlParser를 사용할 수 있지만, 이를 비활성화할 수도 있습니다. spring.data.jpa.query.native.parserregex로 설정하면 regex 기반 쿼리 향상 도구를 사용할 수 있습니다. 가능 값은 다음과 같습니다:

  • auto: 기본값으로, 자동 선택.
  • regex: 내장된 regex 기반 쿼리 향상기 사용.
  • jsqlparser: JSqlParser 사용.

4. 네이티브 결과 읽기

네이티브 쿼리를 사용하면 데이터베이스의 실제 컬럼 이름과 값을 키/값 쌍으로 나타내는 네이티브 튜플을 읽을 수 있습니다. 아래는 네이비트 데이터를 리턴하는 예제입니다.

interface UserRepository extends JpaRepository<User, Long> {

    @NativeQuery("SELECT * FROM USERS WHERE EMAIL_ADDRESS = ?1")
    Map<String, Object> findRawMapByEmail(String emailAddress);     // (1)

    @NativeQuery("SELECT * FROM USERS WHERE LASTNAME = ?1")
    List<Map<String, Object>> findRawMapByLastname(String lastname); // (2)
}
  • (1): 단일 Map 결과를 반환하며, 각 키는 데이터베이스의 컬럼 이름, 값은 해당 컬럼의 값을 나타냅니다.
  • (2): 여러 개의 Map 결과를 반환하며, 각 Map은 각 사용자에 대한 정보를 포함합니다.

5. 주요 사항

  • Tuple Queries: 문자열 기반의 튜플 쿼리는 Hibernate에서만 지원하며, EclipseLink는 Criteria 기반의 튜플 쿼리만 지원합니다.

 

Spring Data JPA의 네이티브 쿼리는 데이터베이스와의 직접적인 상호작용을 가능하게 하며, 복잡한 SQL 쿼리를 자유롭게 작성할 수 있는 장점을 제공합니다. 그러나 네이티브 쿼리를 사용할 때는 SQL 문법에 대한 이해가 필요하며, Spring Data JPA의 다른 기능들에 비해 동적 쿼리 생성 기능이 제한적이라는 점을 고려해야 합니다.