1. 개요

SpringData는 우리가 실행할 수있는 쿼리를 정의하는 다양한 방법을 제공합니다. 이 중 하나는  @Query 어노테이션입니다.

이 예제에서는 SpringData JPA에서 @Query 어노테이션 을 사용하여 JPQL 및 네이티브 SQL 쿼리를 모두 실행 하는 방법을 보여 줍니다 .

또한 @Query 어노테이션이 충분하지 않을 때 동적 쿼리를 작성하는 방법을 보여줍니다 .

2. 쿼리 선택

스프링 데이터 저장소 메소드에 대해 실행할 SQL을 정의하기 위해 , @Query 어노테이션으로 메소드에 어노테이션을 달  수 있습니다. 속성에는 실행할 JPQL 또는 SQL이 포함됩니다.

@query의 어노테이션은 어노테이션 명명 된 쿼리보다 우선합니다 @NamedQuery 또는 정의 orm.xml의 파일.

명명 된 쿼리로 도메인 모델 내부가 아닌 저장소 내부의 메서드 바로 위에 쿼리 정의를 배치하는 것이 좋습니다. 저장소는 지속성을 담당하므로 이러한 정의를 저장하는 것이 더 좋습니다.

2.1. JPQL

기본적으로 쿼리 정의는 JPQL을 사용합니다.

데이터베이스에서 활성 사용자 엔터티 를 반환하는 간단한 리포지토리 메서드를 살펴 보겠습니다 .

@Query("SELECT u FROM User u WHERE u.status = 1")
Collection<User> findAllActiveUsers();

2.2. 원주민

네이티브 SQL을 사용하여 쿼리를 정의 할 수도 있습니다. 우리가해야 할 일은 nativeQuery 속성 의 값 true 로 설정하고 어노테이션 value 속성에 네이티브 SQL 쿼리를 정의하는 것입니다.

@Query(
  value = "SELECT * FROM USERS u WHERE u.status = 1", 
  nativeQuery = true)
Collection<User> findAllActiveUsersNative();

3. 질의에서 순서 정의

@Query 어노테이션 이있는 SpringData 메소드 선언에  Sort  유형의 추가 매개 변수를 전달할 수 있습니다 .  데이터베이스에 전달 되는 ORDER BY 절로 변환됩니다 .

3.1. JPA 제공 및 파생 메서드 정렬

findAll (Sort) 또는 메서드 시그니처를 구문 분석하여 생성 된 메서드 와 같이 즉시 사용할 수있는 메서드 의 경우 정렬을 정의하기 위해 객체 속성 만 사용할 수 있습니다 .

userRepository.findAll(Sort.by(Sort.Direction.ASC, "name"));

이제 이름 속성의 길이를 기준으로 정렬하고 싶다고 가정 해 보겠습니다.

userRepository.findAll(Sort.by("LENGTH(name)"));

위 코드를 실행하면 예외가 발생합니다.

org.springframework.data.mapping.PropertyReferenceException : 유형 User에 대한 LENGTH (name) 속성이 없습니다!

3.2. JPQL

쿼리 정의에 JPQL을 사용할 때 SpringData는 아무런 문제없이 정렬을 처리 할 수 ​​있습니다. 우리가해야 할 일은 Sort 유형의 메소드 매개 변수를 추가하는 것 뿐입니다 .

@Query(value = "SELECT u FROM User u")
List<User> findAllUsers(Sort sort);

이 메서드를 호출하고 Sort 매개 변수를 전달 하면 User 개체 이름 속성에 따라 결과가 정렬 됩니다 .

userRepository.findAllUsers(Sort.by("name"));

그리고 @Query 어노테이션 을 사용 했으므로 동일한 방법을 사용하여 이름 길이 별로 정렬 된 사용자 List을 가져올 수 있습니다.

userRepository.findAllUsers(JpaSort.unsafe("LENGTH(name)"));

JpaSort.unsafe () 를 사용하여 Sort 객체 인스턴스 를 만드는 것이 중요 합니다.

우리가 사용할 때 :

Sort.by("LENGTH(name)");

그러면 위에서 findAll () 메서드에 대해 본 것과 똑같은 예외가 발생  합니다.

SpringData가 @Query 어노테이션 을 사용하는 메소드 의 안전하지 않은 정렬 순서를  발견하면  정렬 절을 쿼리에 추가하기 만하면됩니다. 정렬 기준이되는 속성이 도메인 모델에 속하는지 여부를 확인하지 않습니다.

3.3. 원주민

@query의 어노테이션이 네이티브 SQL을 사용하여, 다음은 정의 할 수는 없습니다 정렬 .

그렇게하면 예외가 발생합니다.

org.springframework.data.jpa.repository.query.InvalidJpaQueryMethodException : 동적 정렬 및 / 또는 페이지 매김과 함께 네이티브 쿼리를 사용할 수 없습니다.

예외에서 말했듯이 기본 쿼리에는 정렬이 지원되지 않습니다. 오류 메시지는 페이지 매김으로 인해 예외가 발생한다는 힌트를 제공합니다.

그러나 페이지 매김을 활성화하는 해결 방법이 있으며 다음 섹션에서 다룰 것입니다.

4. 페이지 매김

페이지 매김을 사용하면 Page 에서 전체 결과의 하위 집합 만 반환 할 수 있습니다  . 예를 들어 웹 페이지에서 여러 데이터 페이지를 탐색 할 때 유용합니다.

페이지 매김의 또 다른 장점은 서버에서 클라이언트로 전송되는 데이터의 양이 최소화된다는 것입니다. 더 작은 데이터 조각을 보내면 일반적으로 성능이 향상되는 것을 볼 수 있습니다.

4.1. JPQL

JPQL 쿼리 정의에서 페이지 매김을 사용하는 것은 간단합니다.

@Query(value = "SELECT u FROM User u ORDER BY id")
Page<User> findAllUsersWithPagination(Pageable pageable);

PageRequest  매개 변수를 전달하여 데이터 페이지를 가져올 수 있습니다.

페이지 매김은 네이티브 쿼리에서도 지원되지만 약간의 추가 작업이 필요합니다.

4.2. 원주민

추가 속성 countQuery를 선언하여 네이티브 쿼리에 페이지 매김을 활성화  할 수 있습니다 .

이것은 전체 결과의 행 수를 계산하기 위해 실행할 SQL을 정의합니다.

@Query(
  value = "SELECT * FROM Users ORDER BY id", 
  countQuery = "SELECT count(*) FROM Users", 
  nativeQuery = true)
Page<User> findAllUsersWithPagination(Pageable pageable);

4.3. 2.0.4 이전의 SpringData JPA 버전

네이티브 쿼리에 대한 위의 솔루션은 SpringData JPA 버전 2.0.4 이상에서 잘 작동합니다.

해당 버전 이전에는 이러한 쿼리를 실행하려고하면 정렬에 대한 이전 섹션에서 설명한 것과 동일한 예외가 발생합니다.

쿼리 내에 페이지 매김을위한 추가 매개 변수를 추가하여이를 극복 할 수 있습니다.

@Query(
  value = "SELECT * FROM Users ORDER BY id \n-- #pageable\n",
  countQuery = "SELECT count(*) FROM Users",
  nativeQuery = true)
Page<User> findAllUsersWithPagination(Pageable pageable);

위의 예에서는 pagination 매개 변수의 자리 표시 자로 "\ n– #pageable \ n"을 추가합니다. 이것은 SpringData JPA에게 쿼리를 구문 분석하고 페이징 가능한 매개 변수를 주입하는 방법을 알려줍니다. 이 솔루션은 H2 데이터베이스에서 작동 합니다.

JPQL 및 네이티브 SQL을 통해 간단한 선택 쿼리를 만드는 방법을 살펴 보았습니다. 다음으로 추가 매개 변수를 정의하는 방법을 보여줍니다.

5. 인덱싱 된 쿼리 매개 변수

쿼리에 메소드 매개 변수를 전달할 수있는 두 가지 가능한 방법이 있습니다. 색인 된 매개 변수와 명명 된 매개 변수입니다.

이 섹션에서는 인덱싱 된 매개 변수를 다룹니다.

5.1. JPQL

JPQL의 인덱싱 된 매개 변수의 경우 SpringData 는 메소드 선언에 나타나는 것과 동일한 순서로 메소드 매개 변수를 쿼리에 전달합니다 .

@Query("SELECT u FROM User u WHERE u.status = ?1")
User findUserByStatus(Integer status);

@Query("SELECT u FROM User u WHERE u.status = ?1 and u.name = ?2")
User findUserByStatusAndName(Integer status, String name);

상기 쿼리의 경우, 상태 에있어서 파라미터 인덱스 쿼리 매개 변수에 할당 될 1 및  이름 메소드 파라미터 인덱스 쿼리 매개 변수에 할당한다 (2) .

5.2. 원주민

네이티브 쿼리에 대한 인덱싱 된 매개 변수는 JPQL과 정확히 동일한 방식으로 작동합니다.

@Query(
  value = "SELECT * FROM Users u WHERE u.status = ?1", 
  nativeQuery = true)
User findUserByStatusNative(Integer status);

다음 섹션에서는 이름을 통해 매개 변수를 전달하는 다른 접근 방식을 보여줄 것입니다.

6. 명명 된 매개 변수

명명 된 매개 변수를 사용하여 메서드 매개 변수를 쿼리에 전달할 수도 있습니다 . 리포지토리 메서드 선언 내 에서 @Param 어노테이션을 사용하여 정의합니다 .

@Param으로 어노테이션이 달린 각 매개 변수 에는 해당 JPQL 또는 SQL 쿼리 매개 변수 이름과 일치하는 값 문자열이 있어야합니다. 명명 된 매개 변수가있는 쿼리는 읽기가 더 쉽고 쿼리를 리팩토링해야하는 경우 오류가 발생하기 쉽습니다.

6.1. JPQL

위에서 언급했듯이 메서드 선언에서 @Param 어노테이션을 사용하여  JPQL에서 이름으로 정의 된 매개 변수를 메서드 선언의 매개 변수와 일치시킵니다.

@Query("SELECT u FROM User u WHERE u.status = :status and u.name = :name")
User findUserByStatusAndNameNamedParams(
  @Param("status") Integer status, 
  @Param("name") String name);

위의 예에서는 SQL 쿼리와 메서드 매개 변수가 동일한 이름을 갖도록 정의했지만 값 문자열이 동일한 경우에는 필요하지 않습니다.

@Query("SELECT u FROM User u WHERE u.status = :status and u.name = :name")
User findUserByUserStatusAndUserName(@Param("status") Integer userStatus, 
  @Param("name") String userName);

6.2. 원주민

네이티브 쿼리 정의의 경우 JPQL과 비교하여 이름을 통해 쿼리에 매개 변수를 전달하는 방법에는 차이가 없습니다. @Param 어노테이션을 사용합니다 .

@Query(value = "SELECT * FROM Users u WHERE u.status = :status and u.name = :name", 
  nativeQuery = true)
User findUserByStatusAndNameNamedParamsNative(
  @Param("status") Integer status, @Param("name") String name);

7. 수집 매개 변수

JPQL 또는 SQL 쿼리 where 절에 IN (또는 NOT IN ) 키워드 가 포함 된  경우를 고려해 보겠습니다  .

SELECT u FROM User u WHERE u.name IN :names

이 경우 Collection  을 매개 변수로 사용하는 쿼리 메서드를 정의 할 수 있습니다 .

@Query(value = "SELECT u FROM User u WHERE u.name IN :names")
List<User> findUserByNameList(@Param("names") Collection<String> names);

매개 변수가 Collection 이므로 List, HashSet 등과 함께 사용할 수 있습니다  .

다음으로 @ Modifying  어노테이션을 사용하여 데이터를 수정하는 방법을 보여줍니다 .

8. @Modifying으로 쿼리 업데이트

@ Query 어노테이션을 사용 하여 저장소 메서드에 @ Modifying  어노테이션 도 추가하여 데이터베이스의 상태를 수정할 수  있습니다 .

8.1. JPQL

데이터를 수정하는 저장소 메서드는 select 쿼리 와 비교하여 두 가지 차이점 이 있습니다. @Modifying 어노테이션이 있고 JPQL 쿼리는 select 대신 update사용합니다 .

@Modifying
@Query("update User u set u.status = :status where u.name = :name")
int updateUserSetStatusForName(@Param("status") Integer status, 
  @Param("name") String name);

반환 값은 쿼리 실행으로 업데이트 된 행 수를 정의합니다. 색인화 된 매개 변수와 명명 된 매개 변수는 모두 업데이트 쿼리 내에서 사용할 수 있습니다.

8.2. 원주민

네이티브 쿼리를 사용하여 데이터베이스의 상태를 수정할 수도 있습니다. @Modifying 어노테이션 만 추가하면 됩니다.

@Modifying
@Query(value = "update Users u set u.status = ? where u.name = ?", 
  nativeQuery = true)
int updateUserSetStatusForNameNative(Integer status, String name);

8.3. 삽입물

삽입 작업을 수행하려면 INSERT가 JPA 인터페이스의 일부가 아니기  때문에 @Modifying을 적용  하고 기본 쿼리를 사용해야합니다  .

@Modifying
@Query(
  value = 
    "insert into Users (name, age, email, status) values (:name, :age, :email, :status)",
  nativeQuery = true)
void insertUser(@Param("name") String name, @Param("age") Integer age, 
  @Param("status") Integer status, @Param("email") String email);

9. 동적 쿼리

종종 값이 런타임에만 알려진 조건이나 데이터 세트를 기반으로 SQL 문을 작성해야하는 경우가 있습니다. 이러한 경우 정적 쿼리를 사용할 수 없습니다.

9.1. 동적 쿼리의 예

예를 들어, 런타임에 정의 된 집합 ( email1 , email2 ,…, emailn) 에서 이메일이 LIKE 인 모든 사용자를 선택해야하는 상황을 상상해보십시오 .

SELECT u FROM User u WHERE u.email LIKE '%email1%' 
    or  u.email LIKE '%email2%'
    ... 
    or  u.email LIKE '%emailn%'

집합이 동적으로 구성 되었기 때문에 컴파일 타임 에 추가 할 LIKE 절의 수를 알 수 없습니다 .

이 경우 정적 SQL 문을 제공 할 수 없으므로 @Query 어노테이션 만 사용할 수 없습니다.

대신 사용자 정의 복합 저장소를 구현하여 기본 JpaRepository 기능을 확장하고 동적 쿼리를 작성하기위한 자체 로직을 제공 할 수 있습니다. 이를 수행하는 방법을 살펴 보겠습니다.

9.2. 사용자 정의 저장소 및 JPA 기준 API

다행스럽게도 Spring은 커스텀 프래그먼트 인터페이스를 사용하여 기본 저장소를 확장하는 방법을 제공합니다. 그런 다음 이들을 함께 연결하여 복합 저장소 를 만들 수 있습니다  .

사용자 지정 조각 인터페이스를 만드는 것으로 시작합니다.

public interface UserRepositoryCustom {
    List<User> findUserByEmails(Set<String> emails);
}

그런 다음이를 구현합니다.

public class UserRepositoryCustomImpl implements UserRepositoryCustom {

    @PersistenceContext
    private EntityManager entityManager;

    @Override
    public List<User> findUserByEmails(Set<String> emails) {
        CriteriaBuilder cb = entityManager.getCriteriaBuilder();
        CriteriaQuery<User> query = cb.createQuery(User.class);
        Root<User> user = query.from(User.class);

        Path<String> emailPath = user.get("email");

        List<Predicate> predicates = new ArrayList<>();
        for (String email : emails) {
            predicates.add(cb.like(emailPath, email));
        }
        query.select(user)
            .where(cb.or(predicates.toArray(new Predicate[predicates.size()])));

        return entityManager.createQuery(query)
            .getResultList();
    }
}

위에 표시된대로 JPA Criteria API활용하여 동적 쿼리를 작성했습니다.

또한 클래스 이름에 Impl 접미사 를 포함해야합니다 . Spring은 UserRepositoryCustom 구현을 UserRepositoryCustomImpl검색합니다 . 프래그먼트는 그 자체로 저장소가 아니기 때문에 Spring은 프래그먼트 구현을 찾기 위해이 메커니즘에 의존합니다.

9.3. 기존 저장소 확장

섹션 2에서 섹션 7까지의 모든 쿼리 메서드는 UserRepository에 있습니다.

이제 UserRepository 에서 새 인터페이스를 확장하여 조각을 통합 할 것입니다 .

public interface UserRepository extends JpaRepository<User, Integer>, UserRepositoryCustom {
    //  query methods from section 2 - section 7
}

9.4. 저장소 사용

마지막으로 동적 쿼리 메서드를 호출 할 수 있습니다.

Set<String> emails = new HashSet<>();
// filling the set with any number of items

userRepository.findUserByEmails(emails);

우리는 성공적으로 복합 저장소를 생성하고 커스텀 메소드를 호출했습니다.

10. 결론

이 기사에서는 @Query 어노테이션을 사용하여 SpringData JPA 저장소 메소드에서 쿼리를 정의하는 여러 방법을 다루었습니다  .

또한 사용자 지정 저장소를 구현하고 동적 쿼리를 만드는 방법도 배웠습니다.

항상 그렇듯이이 문서에 사용 된 전체 코드 예제 는 GitHub에서 사용할 수 있습니다 .