JPA Query Methods

Dev.Hammy·2024년 4월 20일
0

Spring Data JPA

목록 보기
7/13

이 섹션에서는 Spring Data JPA를 사용하여 쿼리를 생성하는 다양한 방법을 설명합니다

Query Lookup Strategies

JPA 모듈은 쿼리를 문자열로 수동으로 정의하거나 메서드 이름에서 파생되도록 지원합니다.

조건자 IsStartingWith, StartingWith, StartsWith, IsEndingWith, EndingWith, EndsWith, IsNotContaining, NotContaining, NotContains, IsContaining, Containing, Contains가 포함된 파생 쿼리는 이러한 쿼리에 대한 해당 인수가 삭제(sanitize)됩니다. 즉, 인수에 실제로 LIKE에서 와일드카드로 인식되는 문자가 포함되어 있으면 이러한 문자는 이스케이프 처리되어 리터럴로만 일치합니다. 사용되는 이스케이프 문자는 @EnableJpaRepositories annotation의 escapeCharacter를 설정하여 구성할 수 있습니다. SpEL 표현식 사용과 비교해 보세요.

Declared Queries

메소드 이름에서 파생된 쿼리를 얻는 것은 매우 편리하지만 메소드 이름 구문 분석기가 사용하려는 키워드를 지원하지 않거나 메소드 이름이 불필요하게 보기 흉해지는 상황에 직면할 수 있습니다. 따라서 명명 규칙을 통해 JPA 명명된 쿼리를 사용하거나(자세한 내용은 JPA 명명된 쿼리 사용 참조) 쿼리 메서드에 @Query로 annotation을 달 수 있습니다(자세한 내용은 @Query 사용 참조).

Query Creation

일반적으로 JPA의 쿼리 생성 메커니즘은 쿼리 메서드에 설명된 대로 작동합니다. 다음 예에서는 JPA 쿼리 메서드가 무엇으로 변환되는지 보여줍니다.

Example 1. Query creation from method names

public interface UserRepository extends Repository<User, Long> {
  List<User>  findByEmailAddressAndLastname(String  emailAddress, String lastname);
}

여기에서 JPA 기준 API를 사용하여 쿼리를 생성하지만 기본적으로 이는 다음 쿼리로 변환됩니다. select u from User where u.emailAddress = ?1 and u.lastname = ?2. Spring Data JPA는 속성 표현식에 설명된 대로 속성 검사를 수행하고 중첩된 속성을 탐색합니다.

다음 표에서는 JPA에 지원되는 키워드와 해당 키워드가 포함된 메서드의 변환 내용을 설명합니다.

Table 1. Supported keywords inside method names

표 1. 메서드 이름 내에서 지원되는 키워드
키워드 예시 JPQL 조각

Distinct

findDistinctByLastnameAndFirstname

select distinct …​ where x.lastname = ?1 and x.firstname = ?2

And

findByLastnameAndFirstname

… where x.lastname = ?1 and x.firstname = ?2

Or

findByLastnameOrFirstname

… where x.lastname = ?1 or x.firstname = ?2

Is, Equals

findByFirstname,findByFirstnameIs,findByFirstnameEquals

… where x.firstname = ?1

Between

findByStartDateBetween

… where x.startDate between ?1 and ?2

LessThan

findByAgeLessThan

… where x.age < ?1

LessThanEqual

findByAgeLessThanEqual

… where x.age <= ?1

GreaterThan

findByAgeGreaterThan

… where x.age > ?1

GreaterThanEqual

findByAgeGreaterThanEqual

… where x.age >= ?1

After

findByStartDateAfter

… where x.startDate > ?1

Before

findByStartDateBefore

… where x.startDate < ?1

IsNull, Null

findByAge(Is)Null

… where x.age is null

IsNotNull, NotNull

findByAge(Is)NotNull

… where x.age not null

Like

findByFirstnameLike

… where x.firstname like ?1

NotLike

findByFirstnameNotLike

… where x.firstname not like ?1

StartingWith

findByFirstnameStartingWith

… where x.firstname like ?1 (매개변수가 %로 끝남)

EndingWith

findByFirstnameEndingWith

… where x.firstname like ?1 (매개변수가 %로 시작함)

Containing

findByFirstnameContaining

… where x.firstname like ?1 (매개변수가 %로 감싸짐)

OrderBy

findByAgeOrderByLastnameDesc

… where x.age = ?1 order by x.lastname desc

Not

findByLastnameNot

… where x.lastname <> ?1

In

findByAgeIn(Collection<Age> ages)

… where x.age in ?1

NotIn

findByAgeNotIn(Collection<Age> ages)

… where x.age not in ?1

True

findByActiveTrue()

… where x.active = true

False

findByActiveFalse()

… where x.active = false

IgnoreCase

findByFirstnameIgnoreCase

… where UPPER(x.firstname) = UPPER(?1)

[Note]
InNotIn은 또한 Collection의 하위 클래스와 배열 또는 가변 인수를 매개변수로 사용합니다. 동일한 논리 연산자의 다른 구문 버전에 대해서는 리포지토리 쿼리 키워드를 확인하세요.

[Warning]
DISTINCT는 까다로울 수 있으며 항상 예상한 결과를 생성하지 못할 수도 있습니다. 예를 들어 select distinct u from User u하면 select distinct u.lastname from User u하는 것과 완전히 다른 결과가 생성됩니다. 첫 번째 경우에는 User.id를 포함하므로 아무것도 중복되지 않으므로 전체 테이블을 얻게 되며 이는 User 개체로 구성됩니다.

그러나 후자의 쿼리는 초점을 User.lastname으로 좁히고 해당 테이블의 고유한 성을 모두 찾습니다. 이는 또한 List<User> 결과 집합 대신 List<String> 결과 집합을 생성합니다.

countDistinctByLastname(String lastname)도 예상치 못한 결과를 생성할 수 있습니다. Spring Data JPA는 select count(distinct u.id) from User u where u.lastname = ?1를 파생합니다. 다시 말하지만, u.id는 중복 항목에 도달하지 않으므로 이 쿼리는 바인딩 성을 가진 모든 사용자를 계산합니다. countByLastname(String lastname)과 동일합니다!

어쨌든 이 쿼리의 요점은 무엇입니까? 특정 성을 가진 사람의 수를 찾으려면? 해당 구속력 있는 성을 가진 고유한 사람의 수를 찾으려면? 고유한 성의 수를 찾으려면? (마지막 쿼리는 완전히 다른 쿼리입니다!) distinct를 사용하려면 쿼리를 직접 작성하고 원하는 정보를 가장 잘 캡처하기 위해 @Query를 사용해야 하는 경우도 있습니다. 결과 집합을 캡처하기 위해 프로젝션이 필요할 수도 있기 때문입니다.

Annotation-based Configuration

annotation 기반 구성은 다른 구성 파일을 편집할 필요가 없어 유지 관리 노력이 줄어든다는 장점이 있습니다. 모든 새로운 쿼리 선언에 대해 도메인 클래스를 다시 컴파일해야 하므로 이러한 이점에 대한 비용을 지불합니다.

Example 2. Annotation-based named query configuration

@Entity
@NamedQuery(name = "User.findByEmailAddress",
  query = "select u from User u where u.emailAddress = ?1")
public class User {

}

Using JPA Named Queries

[Note]
예제에서는 <named-query /> 요소와 @NamedQuery annotation을 사용합니다. 이러한 구성 요소에 대한 쿼리는 JPA 쿼리 언어로 정의되어야 합니다. 물론 <named-native-query />@NamedNativeQuery도 사용할 수 있습니다. 이러한 요소를 사용하면 데이터베이스 플랫폼 독립성을 상실하여 기본 SQL에서 쿼리를 정의할 수 있습니다.

XML Named Query Definition

XML 구성을 사용하려면 클래스 경로의 META-INF 폴더에 있는 orm.xml JPA 구성 파일에 필요한 <named-query /> 요소를 추가하세요. 일부 정의된 명명 규칙을 사용하면 명명된 쿼리의 자동 호출이 활성화됩니다. 자세한 내용은 아래를 참조하세요.

예 3. XML 명명된 쿼리 구성

<named-query name="User.findByLastname">
  <query>select u from User u where u.lastname = ?1</query>
</named-query>

쿼리에는 런타임 시 쿼리를 해결하는 데 사용되는 특수 이름이 있습니다.

Declaring Interfaces

이러한 명명된 쿼리를 허용하려면 다음과 같이 UserRepositoryWithRewriter를 지정합니다.

예 4. UserRepository의 쿼리 메서드 선언

public interface UserRepository extends JpaRepository<User, Long> {

  List<User> findByLastname(String lastname);

  User findByEmailAddress(String emailAddress);
}

Spring Data는 구성된 도메인 클래스의 간단한 이름으로 시작하고 점으로 구분된 메서드 이름이 뒤따르는 명명된 쿼리에 대한 이러한 메서드에 대한 호출을 해결하려고 시도합니다. 따라서 앞의 예에서는 메서드 이름에서 쿼리를 만드는 대신 이전에 정의한 명명된 쿼리를 사용합니다.

Using @Query

명명된 쿼리를 사용하여 엔터티에 대한 쿼리를 선언하는 것은 유효한 접근 방식이며 적은 수의 쿼리에 대해 잘 작동합니다. 쿼리 자체는 이를 실행하는 Java 메서드에 연결되어 있으므로 실제로 도메인 클래스에 annotation을 추가하는 대신 Spring Data JPA @Query annotation을 사용하여 쿼리를 직접 바인딩할 수 있습니다. 이렇게 하면 지속성 관련 정보로부터 도메인 클래스가 해방되고 쿼리가 저장소 인터페이스에 같은 위치에 배치됩니다.

쿼리 메서드에 annotation이 달린 쿼리는 @NamedQuery를 사용하여 정의된 쿼리나 orm.xml에 선언된 명명된 쿼리보다 우선합니다.

다음 예에서는 @Query annotation을 사용하여 생성된 쿼리를 보여줍니다.

Example 5. Declare query at the query method using @Query

public interface UserRepository extends JpaRepository<User, Long> {

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

Applying a QueryRewriter

때로는 적용하려는 기능의 수에 관계없이 쿼리가 EntityManager로 전송되기 전에 Spring Data JPA가 쿼리에 원하는 모든 항목을 적용하도록 하는 것이 불가능해 보일 수 있습니다.

쿼리가 EntityManager로 전송되기 직전에 쿼리를 직접 확인하고 "rewrite" 수 있습니다. 즉, 마지막 순간에 어떤 변경이라도 할 수 있습니다.

예 6. @Query를 사용하여 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);
}

이 예에서는 동일한 QueryRewriter를 활용하는 기본(순수 SQL) 재작성과 JPQL 쿼리를 모두 보여줍니다. 이 시나리오에서 Spring Data JPA는 해당 유형의 애플리케이션 컨텍스트에 등록된 Bean을 찾습니다.

다음과 같이 쿼리 재작성을 작성할 수 있습니다.

Example 7. Example QueryRewriter

public class MyQueryRewriter implements QueryRewriter {

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

Spring Framework의 @Component 기반 annotation 중 하나를 적용하거나 @Configuration 클래스 내 @Bean 메서드의 일부로 포함하여 QueryRewriter가 애플리케이션 컨텍스트에 등록되었는지 확인해야 합니다.

또 다른 옵션은 저장소 자체가 인터페이스를 구현하도록 하는 것입니다.

Example 8. Repository that provides the 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");
		}
}

QueryRewriter로 수행하는 작업에 따라 각각 애플리케이션 컨텍스트에 등록된 둘 이상을 갖는 것이 좋습니다.

[Note]
CDI 기반 환경에서 Spring Data JPA는 BeanManager에서 QueryRewriter 구현 인스턴스를 검색합니다.

Using Advanced LIKE Expressions

@Query를 사용하여 생성된 수동으로 정의된 쿼리에 대한 쿼리 실행 메커니즘을 사용하면 다음 예제와 같이 쿼리 정의 내에서 고급 LIKE 표현식을 정의할 수 있습니다.

Example 9. Advanced like expressions in @Query

public interface UserRepository extends JpaRepository<User, Long> {

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

앞의 예에서는 LIKE 구분 기호 문자(%)가 인식되고 쿼리가 유효한 JPQL 쿼리로 변환됩니다(% 제거). 쿼리를 실행하면 메서드 호출에 전달된 매개 변수가 이전에 인식된 LIKE 패턴으로 보강됩니다.

Native Queries

@Query 어노테이션을 사용하면 다음 예와 같이 nativeQuery 플래그를 true로 설정하여 기본 쿼리를 실행할 수 있습니다.

예 10. @Query를 사용하여 쿼리 메서드에서 native query 선언

public interface UserRepository extends JpaRepository<User, Long> {

  @Query(value = "SELECT * FROM USERS WHERE EMAIL_ADDRESS = ?1", nativeQuery = true)
  User findByEmailAddress(String emailAddress);
}

[Note]
Spring Data JPA는 현재 네이티브 쿼리에 대한 동적 정렬을 지원하지 않습니다. 왜냐하면 선언된 실제 쿼리를 조작해야 하기 때문입니다. 이는 네이티브 SQL에 대해 안정적으로 수행할 수 없습니다. 그러나 다음 예와 같이 개수 쿼리를 직접 지정하여 페이지 매김에 기본 쿼리를 사용할 수 있습니다.

예 11. @Query를 사용하여 쿼리 메서드에서 페이지 매김을 위한 기본 개수 쿼리를 선언합니다.

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);
}

쿼리 복사본에 .count 접미사를 추가하면 명명된 기본 쿼리에서도 유사한 접근 방식이 작동합니다. 하지만 개수 쿼리에 대한 결과 집합 매핑을 등록해야 할 수도 있습니다.

Using Sort

PageRequest를 제공하거나 Sort를 직접 사용하여 정렬을 수행할 수 있습니다. SortOrder 인스턴스 내에서 실제로 사용되는 속성은 도메인 모델과 일치해야 합니다. 즉, 쿼리 내에서 사용되는 속성이나 별칭으로 확인되어야 합니다. JPQL은 이를 상태 필드 경로 표현식으로 정의합니다.

[Note]
참조할 수 없는 경로 표현식을 사용하면 Exception가 발생합니다.

그러나 @Query와 함께 Sort를 사용하면 ORDER BY 절 내에 함수가 포함된 경로 확인되지 않은 Order 인스턴스를 몰래 가져올 수 있습니다. 이는 주어진 쿼리 문자열에 Order가 추가되기 때문에 가능합니다. 기본적으로 Spring Data JPA는 함수 호출이 포함된 Order 인스턴스를 거부하지만 JpaSort.unsafe를 사용하여 잠재적으로 안전하지 않은 순서를 추가할 수 있습니다.

다음 예제에서는 JpaSort의 안전하지 않은 옵션을 포함하여 SortJpaSort를 사용합니다.

Example 12. Using Sort and JpaSort

public interface UserRepository extends JpaRepository<User, Long> {

  @Query("select u from User u where u.lastname like ?1%")
  List<User> findByAndSort(String lastname, Sort sort);

  @Query("select u.id, LENGTH(u.firstname) as fn_len from User u where u.lastname like ?1%")
  List<Object[]> findByAsArrayAndSort(String lastname, Sort sort);
}

repo.findByAndSort("lannister", Sort.by("firstname")); // (1)
repo.findByAndSort("stark", Sort.by("LENGTH(firstname)")); // (2)
repo.findByAndSort("targaryen", JpaSort.unsafe("LENGTH(firstname)")); // (3)
repo.findByAsArrayAndSort("bolton", Sort.by("fn_len")); // (4)

(1) 도메인 모델의 속성을 가리키는 유효한 낷 표현식입니다.
(2) 함수 호출을 포함하는 잘못된 Sort입니다. 예외가 발생합니다.
(3) 명시적으로 안전하지 않은 Order가 포함된 유효한 Sort입니다.
(4) 별칭이 지정된 함수를 가리키는 유효한 Sort 표현식입니다.

Scrolling Large Query Results

대규모 데이터 세트로 작업할 때 스크롤은 모든 결과를 메모리에 로드하지 않고도 해당 결과를 효율적으로 처리하는 데 도움이 될 수 있습니다.

대규모 쿼리 결과를 사용할 수 있는 여러 가지 옵션이 있습니다.

  1. 페이징. 이전 장에서 PageablePageRequest에 대해 배웠습니다.

  2. 오프셋 기반 스크롤. 이는 총 결과 개수가 필요하지 않기 때문에 페이징보다 가벼운 변형입니다.

  3. 키셋-베이스 스크롤. 이 방법은 데이터베이스 인덱스를 활용하여 오프셋 기반 결과 검색의 단점을 방지합니다.

특정 배치에 가장 적합한 method에 대해 자세히 알아보세요.

쿼리 메서드, Query-by-ExampleQuerydsl과 함께 Scroll API를 사용할 수 있습니다.

[Note]
문자열 기반 쿼리 방법을 사용한 스크롤은 아직 지원되지 않습니다. 저장된 @Procedure 쿼리 메서드를 사용한 스크롤도 지원되지 않습니다.

Using Named Parameters

기본적으로 Spring Data JPA는 이전 예제에서 설명한 대로 위치 기반 매개변수 바인딩을 사용합니다. 이로 인해 매개변수 위치와 관련하여 리팩토링할 때 쿼리 메서드에 약간의 오류가 발생하기 쉽습니다. 이 문제를 해결하려면 다음 예제와 같이 @Param annotation을 사용하여 메서드 매개 변수에 구체적인 이름을 지정하고 쿼리에 이름을 바인딩할 수 있습니다.

Example 13. Using named parameters

public interface UserRepository extends JpaRepository<User, Long> {

  @Query("select u from User u where u.firstname = :firstname or u.lastname = :lastname")
  User findByLastnameOrFirstname(@Param("lastname") String lastname,
                                 @Param("firstname") String firstname);
}

[Note]
메소드 매개변수는 정의된 쿼리의 순서에 따라 전환됩니다.

[Note]
버전 4부터 Spring은 -parameters 컴파일러 플래그를 기반으로 Java 8의 매개변수 이름 검색을 완벽하게 지원합니다. 디버그 정보 대신 빌드에서 이 플래그를 사용하면 명명된 매개변수에 대한 @Param annotation을 생략할 수 있습니다.

Using SpEL Expressions

Spring Data JPA 릴리스 1.4부터 @Query로 정의된 수동 정의 쿼리에서 제한된 SpEL 템플릿 표현식의 사용을 지원합니다. 쿼리가 실행되면 이러한 표현식은 미리 정의된 변수 집합에 대해 평가됩니다. Spring Data JPA는 entityName이라는 변수를 지원합니다. 사용법은 select x from #{#entityName} x하는 것입니다. 지정된 저장소와 연결된 도메인 유형의 entityName을 삽입합니다. entityName은 다음과 같이 확인됩니다. 도메인 type이 @Entity annotation에 이름 속성을 설정한 경우 해당 속성이 사용됩니다. 그렇지 않으면 도메인 유형의 단순 클래스 이름이 사용됩니다.

다음 예에서는 쿼리 메서드와 수동으로 정의된 쿼리를 사용하여 저장소 인터페이스를 정의하려는 쿼리 문자열의 #{#entityName} 표현식에 대한 한 가지 사용 사례를 보여줍니다.

Example 14. Using SpEL expressions in repository query methods - entityName

@Entity
public class User {

  @Id
  @GeneratedValue
  Long id;

  String lastname;
}

public interface UserRepository extends JpaRepository<User,Long> {

  @Query("select u from #{#entityName} u where u.lastname = ?1")
  List<User> findByLastname(String lastname);
}

@Query annotation의 쿼리 문자열에 실제 엔터티 이름을 명시하지 않으려면 #{#entityName} 변수를 사용할 수 있습니다.

[Name]
@Entity annotation을 사용하여 entityName을 사용자 정의할 수 있습니다. SpEL 표현식에는 orm.xml의 사용자 정의가 지원되지 않습니다.

물론 쿼리 선언에서 User를 직접 사용할 수도 있지만 그렇게 하려면 쿼리도 변경해야 합니다. #entityName에 대한 참조는 향후 User 클래스를 다른 엔터티 이름으로 다시 매핑할 가능성을 선택합니다(예: @Entity(name = "MyUser") 사용).

쿼리 문자열의 #{#entityName} 표현식에 대한 또 다른 사용 사례는 구체적인 도메인 type에 대한 generic 저장소 인터페이스를 사용하여 일반 저장소 인터페이스를 정의하려는 경우입니다. 구체적인 인터페이스에서 사용자 정의 쿼리 메서드 정의를 반복하지 않으려면 다음 예와 같이 generic 저장소 인터페이스의 @Query annotation 쿼리 문자열에 엔터티 이름 표현식을 사용할 수 있습니다.

Example 15. Using SpEL expressions in repository query methods - entityName with inheritance

@MappedSuperclass
public abstract class AbstractMappedType {String attribute;
}

@Entity
public class ConcreteType extends AbstractMappedType {}

@NoRepositoryBean
public interface MappedTypeRepository<T extends AbstractMappedType>
  extends Repository<T, Long> {

  @Query("select t from #{#entityName} t where t.attribute = ?1")
  List<T> findAllByAttribute(String attribute);
}

public interface ConcreteRepository
  extends MappedTypeRepository<ConcreteType> {}

앞의 예에서 MappedTypeRepository 인터페이스는 AbstractMappedType을 확장하는 몇 가지 도메인 유형에 대한 공통 상위 인터페이스입니다. 또한 generic 저장소 인터페이스의 인스턴스에 사용할 수 있는 일반 findAllByAttribute(…) 메서드를 정의합니다. 이제 ConcreteRepository에서 findByAllAttribute(…)를 호출하면 쿼리는 select t from ConcreteType t where t.attribute = ?1 됩니다.

인수를 조작하는 SpEL 표현식을 사용하여 메서드 인수를 조작할 수도 있습니다. 이러한 SpEL 표현식에서는 엔터티 이름을 사용할 수 없지만 인수는 사용할 수 있습니다. 다음 예에 설명된 것처럼 이름이나 색인으로 액세스할 수 있습니다.

Example 16. Using SpEL expressions in repository query methods - accessing arguments.

@Query("select u from User u where u.firstname = ?1 and u.firstname=?#{[0]} and u.emailAddress = ?#{principal.emailAddress}")
List<User> findByFirstnameAndCurrentUserWithCustomQuery(String firstname);

like- 조건의 경우 문자열 값 매개변수의 시작 또는 끝에 %를 추가하려는 경우가 많습니다. 이는 바인드 매개변수 표시자 또는 SpEL 표현식에 %를 추가하거나 접두사를 추가하여 수행할 수 있습니다. 다음 예제에서는 이를 보여줍니다.

Example 17. Using SpEL expressions in repository query methods - wildcard shortcut.

@Query("select u from User u where u.lastname like %:#{[0]}% and u.lastname like %:lastname%")
List<User> findByLastnameWithSpelExpression(@Param("lastname") String lastname);

안전하지 않은 소스에서 오는 값과 함께 like- 조건을 사용하는 경우 값은 와일드카드를 포함할 수 없도록 삭제되어야 하며 이를 통해 공격자가 가능한 것보다 더 많은 데이터를 선택할 수 있습니다. 이를 위해 SpEL 컨텍스트에서 escape(String) 메소드를 사용할 수 있습니다. 첫 번째 인수에 있는 _%의 모든 인스턴스 앞에는 두 번째 인수의 단일 문자가 붙습니다. JPQL 및 표준 SQL에서 사용할 수 있는 like 표현식의 escape 절과 결합하여 바인드 매개변수를 쉽게 정리할 수 있습니다.

Example 18. Using SpEL expressions in repository query methods - sanitizing input values.

@Query("select u from User u where u.firstname like %?#{escape([0])}% escape ?#{escapeCharacter()}")
List<User> findContainingEscaped(String namePart);

저장소 인터페이스 findContainingEscaped("Peter_")에서 이 메소드 선언이 주어지면 Peter_Parker는 찾지만 Peter Parker는 찾지 않습니다. 사용되는 이스케이프 문자는 @EnableJpaRepositories 주석의 escapeCharacter를 설정하여 구성할 수 있습니다. SpEL 컨텍스트에서 사용할 수 있는 escape(String) 메소드는 SQL 및 JPQL 표준 와일드카드인 _%만 이스케이프합니다. 기본 데이터베이스 또는 JPA 구현이 추가 와일드카드를 지원하는 경우 이러한 와일드카드는 이스케이프되지 않습니다.

Other Methods

Spring Data JPA는 쿼리를 작성하는 다양한 방법을 제공합니다. 그러나 때로는 귀하의 쿼리가 제공된 기술에 비해 너무 복잡할 수도 있습니다. 그러한 상황에서는 다음을 고려하십시오.

  • 아직 작성하지 않았다면 @Query를 사용하여 직접 쿼리를 작성하세요.

  • 이것이 귀하의 요구 사항에 맞지 않으면 맞춤 구현을 구현하는 것이 좋습니다. 이를 통해 구현을 완전히 사용자에게 맡기면서 저장소에 메소드를 등록할 수 있습니다. 이는 다음과 같은 기능을 제공합니다.

    • EntityManager와 직접 대화합니다(순수 HQL/JPQL/EQL/네이티브 SQL 작성 또는 Criteria API 사용).

    • Spring Framework의 JdbcTemplate(네이티브 SQL) 활용

    • 다른 타사 데이터베이스 도구 키트를 사용하세요.

  • 또 다른 옵션은 쿼리를 데이터베이스 내부에 넣은 다음 Spring Data JPA의 @StoredProcedure annotation을 사용하거나 데이터베이스 함수인 경우 @Query annotation을 사용하고 CALL로 호출하는 것입니다.

이러한 전술은 Spring Data JPA가 리소스 관리를 제공하도록 하면서 쿼리를 최대한 제어해야 할 때 가장 효과적일 수 있습니다.

Modifying Queries

이전 섹션에서는 모두 특정 엔터티 또는 엔터티 컬렉션에 액세스하기 위한 쿼리를 선언하는 방법을 설명했습니다. Spring 데이터 저장소에 대한 사용자 정의 구현에 설명된 사용자 정의 메소드 기능을 사용하여 사용자 정의 수정 동작을 추가할 수 있습니다. 이 접근 방식은 포괄적인 사용자 지정 기능에 적합하므로 다음 예와 같이 쿼리 메서드에 @Modifying annotation을 추가하여 매개 변수 바인딩만 필요한 쿼리를 수정할 수 있습니다.

Example 19. Declaring manipulating queries

@Modifying
@Query("update User u set u.firstname = ?1 where u.lastname = ?2")
int setFixedFirstnameFor(String firstname, String lastname);

이렇게 하면 쿼리를 선택하는 대신 업데이트 쿼리로 메서드에 주석이 달린 쿼리가 트리거됩니다. 수정 쿼리 실행 후 EntityManager에 오래된 엔터티가 포함될 수 있으므로 이를 자동으로 지우지 않습니다(자세한 내용은 EntityManager.clear()의 JavaDoc 참조). 이는 EntityManager에 아직 보류 중인 플러시되지 않은 모든 변경 사항을 효과적으로 삭제하기 때문입니다. EntityManager를 자동으로 지우려면 @Modifying annotation의clearAutomatically 속성을 true로 설정하면 됩니다.

@Modifying annotation은 @Query annotation과 결합된 경우에만 관련됩니다. 파생된 쿼리 메서드 또는 사용자 지정 메서드에는 이 annotation이 필요하지 않습니다.

Derived Delete Queries

Spring Data JPA는 다음 예제와 같이 JPQL 쿼리를 명시적으로 선언하지 않아도 되도록 파생된 삭제 쿼리도 지원합니다.

Example 20. Using a derived delete query

interface UserRepository extends Repository<User, Long> {

  void deleteByRoleId(long roleId);

  @Modifying
  @Query("delete from User u where u.role.id = ?1")
  void deleteInBulkByRoleId(long roleId);
}

deleteByRoleId(…) 메서드는 기본적으로 deleteInBulkByRoleId(…)와 동일한 결과를 생성하는 것처럼 보이지만 실행 방식 측면에서 두 메서드 선언 사이에는 중요한 차이점이 있습니다. 이름에서 알 수 있듯이 후자의 방법은 데이터베이스에 대해 단일 JPQL 쿼리(annotation에 정의된 쿼리)를 실행합니다. 이는 현재 로드된 User 인스턴스라도 호출된 수명 주기 콜백을 볼 수 없음을 의미합니다.

수명 주기 쿼리가 실제로 호출되는지 확인하기 위해 deleteByRoleId(…)를 호출하면 쿼리를 실행한 다음 반환된 인스턴스를 하나씩 삭제하므로 지속성 공급자가 실제로 해당 엔터티에 대해 @PreRemove 콜백을 호출할 수 있습니다.

실제로 파생된 삭제 쿼리는 쿼리를 실행한 다음 결과에 대해 CrudRepository.delete(Iterable<User> users)를 호출하고 동작을 CrudRepository의 다른 delete(…) 메서드 구현과 동기화하는 바로 가기입니다.

Applying Query Hints

저장소 인터페이스에 선언된 쿼리에 JPA 쿼리 힌트를 적용하려면 @QueryHints annotation을 사용할 수 있습니다. 다음 예제와 같이 페이지 매김을 적용할 때 트리거되는 추가 카운트 쿼리에 적용되는 힌트를 잠재적으로 비활성화하려면 JPA @QueryHint annotation 배열과 부울 플래그를 사용합니다.

Example 21. Using QueryHints with a repository method

public interface UserRepository extends Repository<User, Long> {

  @QueryHints(value = { @QueryHint(name = "name", value = "value")},
              forCounting = false)
  Page<User> findByLastname(String lastname, Pageable pageable);
}

이전 선언에서는 실제 쿼리에 대해 구성된 @QueryHint를 적용하지만 총 페이지 수를 계산하기 위해 트리거된 개수 쿼리에는 적용을 생략합니다.

Adding Comments to Queries

때로는 데이터베이스 성능을 기반으로 쿼리를 디버깅해야 하는 경우도 있습니다. 데이터베이스 관리자가 보여주는 쿼리는 @Query를 사용하여 작성한 쿼리와 매우 다르게 보일 수도 있고, 사용자 정의 파인더와 관련하여 Spring Data JPA가 생성했다고 가정하는 쿼리 또는 예제로 쿼리를 사용한 것과 전혀 다르게 보일 수도 있습니다.

이 프로세스를 더 쉽게 만들기 위해 @Meta annotation을 적용하여 쿼리든 다른 작업이든 거의 모든 JPA 작업에 사용자 정의 annotation을 삽입할 수 있습니다.

Example 22. Apply @Meta annotation to repository operations

public interface RoleRepository extends JpaRepository<Role, Integer> {

	@Meta(comment = "find roles by name")
	List<Role> findByName(String name);

	@Override
	@Meta(comment = "find roles using QBE")
	<S extends Role> List<S> findAll(Example<S> example);

	@Meta(comment = "count roles for a given name")
	long countByName(String name);

	@Override
	@Meta(comment = "exists based on QBE")
	<S extends Role> boolean exists(Example<S> example);
}

이 샘플 저장소에는 JpaRepository에서 상속된 작업을 재정의할 뿐만 아니라 사용자 정의 파인더가 혼합되어 있습니다. 어느 쪽이든 @Meta annotation을 사용하면 쿼리가 데이터베이스로 전송되기 전에 쿼리에 삽입될 comment을 추가할 수 있습니다.

이 기능은 쿼리에만 국한되지 않는다는 점도 중요합니다. 이것은 countexists 작업까지 확장됩니다. 표시되지는 않지만 특정 delete 작업까지 확장됩니다.

[Important]
가능한 모든 곳에 이 기능을 적용하려고 시도했지만 기본 EntityManager의 일부 작업은 annotation을 지원하지 않습니다. 예를 들어, entityManager.createQuery()는 지원 annotaion으로 명확하게 문서화되어 있지만 entityManager.find() 작업은 그렇지 않습니다.

JPQL 로깅이나 SQL 로깅은 JPA의 표준이 아니므로 아래 섹션에 표시된 것처럼 각 공급자에는 사용자 지정 구성이 필요합니다.

Activating Hibernate comments

Hibernate에서 쿼리 comments을 활성화하려면 hibernate.use_sql_commentstrue로 설정해야 합니다. Java 기반 구성 설정을 사용하는 경우 다음과 같이 수행할 수 있습니다.

Example 23. Java-based JPA configuration

@Bean
public Properties jpaProperties() {

	Properties properties = new Properties();
	properties.setProperty("hibernate.use_sql_comments", "true");
	return properties;
}

persistence.xml 파일이 있는 경우 해당 파일에 적용할 수 있습니다.

<persistence-unit name="my-persistence-unit">

   ...registered classes...

	<properties>
		<property name="hibernate.use_sql_comments" value="true" />
	</properties>
</persistence-unit>

마지막으로 Spring Boot를 사용하는 경우 application.properties 파일 내에서 이를 설정할 수 있습니다.

Example 25. Spring Boot property-based configuration

spring.jpa.properties.hibernate.use_sql_comments=true

EclipseLink에서 쿼리 주석을 활성화하려면 eclipselink.logging.level.sqlFINE으로 설정해야 합니다. Java 기반 구성 설정을 사용하는 경우 다음과 같이 수행할 수 있습니다.

Example 26. Java-based JPA configuration

@Bean
public Properties jpaProperties() {

	Properties properties = new Properties();
	properties.setProperty("eclipselink.logging.level.sql", "FINE");
	return properties;
}

persistence.xml 파일이 있는 경우 해당 파일에 적용할 수 있습니다.

<persistence-unit name="my-persistence-unit">

   ...registered classes...

	<properties>
		<property name="eclipselink.logging.level.sql" value="FINE" />
	</properties>
</persistence-unit>

마지막으로 Spring Boot를 사용하는 경우 application.properties 파일 내에서 이를 설정할 수 있습니다.

Example 28. Spring Boot property-based configuration

spring.jpa.properties.eclipselink.logging.level.sql=FINE

Configuring Fetch- and LoadGraphs

JPA 2.1 사양에는 @NamedEntityGraph 정의를 참조할 수 있는 @EntityGraph annotation을 통해 지원하는 Fetch 및 LoadGraph 지정에 대한 지원이 도입되었습니다. 엔터티에서 해당 annotation을 사용하여 결과 쿼리의 가져오기 계획을 구성할 수 있습니다. 가져오기 type(Fetch 또는 Locad)은 @EntityGraph annotation의 유형 속성을 사용하여 구성할 수 있습니다. 자세한 내용은 JPA 2.1 Spec 3.7.4를 참조하세요.

다음 예에서는 엔터티에 명명된 엔터티 그래프를 정의하는 방법을 보여줍니다.

Example 29. Defining a named entity graph on an entity.

@Entity
@NamedEntityGraph(name = "GroupInfo.detail",
  attributeNodes = @NamedAttributeNode("members"))
public class GroupInfo {

  // default fetch mode is lazy.
  @ManyToMany
  List<GroupMember> members = new ArrayList<GroupMember>();}

다음 예에서는 리포지토리 쿼리 메서드에서 명명된 엔터티 그래프를 참조하는 방법을 보여줍니다.

Example 30. Referencing a named entity graph definition on a repository query method.

public interface GroupRepository extends CrudRepository<GroupInfo, String> {

  @EntityGraph(value = "GroupInfo.detail", type = EntityGraphType.LOAD)
  GroupInfo getByGroupName(String name);

}

@EntityGraph를 사용하여 임시 엔터티 그래프를 정의하는 것도 가능합니다. 다음 예제와 같이 @NamedEntityGraph를 도메인 유형에 명시적으로 추가할 필요 없이 제공된 attributePaths가 해당 EntityGraph로 변환됩니다.

Example 31. Using AD-HOC entity graph definition on an repository query method.

public interface GroupRepository extends CrudRepository<GroupInfo, String> {

  @EntityGraph(attributePaths = { "members" })
  GroupInfo getByGroupName(String name);

}

Scrolling

스크롤은 더 큰 결과 세트 청크를 반복하는 보다 세분화된 접근 방식입니다. 스크롤링은 안정적인 정렬, 스크롤 유형(오프셋 또는 키셋 기반 스크롤링) 및 결과 제한으로 구성됩니다. 속성 이름을 사용하여 간단한 정렬 식을 정의하고 쿼리 파생을 통해 Top 또는 First 키워드를 사용하여 정적 결과 제한을 정의할 수 있습니다. 표현식을 연결하여 여러 기준을 하나의 표현식으로 수집할 수 있습니다.

스크롤 쿼리는 애플리케이션이 전체 쿼리 결과를 사용할 때까지 다음 Window<T>를 얻기 위해 스크롤 위치를 다시 얻을 수 있는 Window<T>를 반환합니다. 다음 결과 배치를 획득하여 Java Iterator<List<…>>를 사용하는 것과 유사하게 쿼리 결과 스크롤을 사용하면 Window.positionAt(…​)를 통해 ScrollPosition에 액세스할 수 있습니다.

Window<User> users = repository.findFirst10ByLastnameOrderByFirstname("Doe", ScrollPosition.offset());
do {

  for (User u : users) {
    // consume the user
  }

  // obtain the next Scroll
  users = repository.findFirst10ByLastnameOrderByFirstname("Doe", users.positionAt(users.size() - 1));
} while (!users.isEmpty() && users.hasNext());

WindowIterator는 다음 Window가 있는지 확인하고 ScrollPosition을 적용할 필요를 없애 Windows 전체에서 스크롤을 단순화하는 유틸리티를 제공합니다.

WindowIterator<User> users = WindowIterator.of(position -> repository.findFirst10ByLastnameOrderByFirstname("Doe", position))
  .startingAt(OffsetScrollPosition.initial());

while (users.hasNext()) {
  User u = users.next();
  // consume the user
}

Scrolling using Offset

오프셋 스크롤은 페이지 매김과 유사한 오프셋 카운터를 사용하여 여러 결과를 건너뛰고 데이터 소스가 지정된 오프셋에서 시작하는 결과만 반환하도록 합니다. 이 간단한 메커니즘은 큰 결과가 클라이언트 애플리케이션으로 전송되는 것을 방지합니다. 그러나 대부분의 데이터베이스에서는 서버가 결과를 반환하기 전에 전체 쿼리 결과를 구체화해야 합니다.

Example 32. Using OffsetScrollPosition with Repository Query Methods

interface UserRepository extends Repository<User, Long> {

  Window<User> findFirst10ByLastnameOrderByFirstname(String lastname, OffsetScrollPosition position);
}

WindowIterator<User> users = WindowIterator.of(position -> repository.findFirst10ByLastnameOrderByFirstname("Doe", position))
  .startingAt(OffsetScrollPosition.initial()); // (1)

(1) 위치 0의 초기 오프셋부터 시작합니다.

Scrolling using Keyset-Filtering

오프셋 기반을 사용하려면 대부분의 데이터베이스에서 서버가 결과를 반환하기 전에 전체 결과를 구체화해야 합니다. 따라서 클라이언트는 요청된 결과 중 일부만 볼 수 있지만 서버는 전체 결과를 빌드해야 하므로 추가 로드가 발생합니다.

키 집합 필터링 접근 방식은 개별 쿼리에 대한 계산 및 I/O 요구 사항을 줄이는 것을 목표로 데이터베이스에 내장된 기능을 활용하여 하위 집합을 검색합니다. 이 접근 방식은 키를 쿼리에 전달하여 스크롤을 재개하는 키 세트를 유지 관리하고 필터 기준을 효과적으로 수정합니다.

Keyset-Filtering의 핵심 아이디어는 안정적인 정렬 순서를 사용하여 결과 검색을 시작하는 것입니다. 다음 청크로 스크롤하려면 정렬된 결과 내에서 위치를 재구성하는 데 사용되는 ScrollPosition을 얻습니다. ScrollPosition은 현재 Window 내 마지막 엔터티의 키 세트를 캡처합니다. 쿼리를 실행하기 위해 재구성에서는 데이터베이스가 잠재적인 인덱스를 활용하여 쿼리를 실행할 수 있도록 모든 정렬 필드와 기본 키를 포함하도록 기준 절을 다시 작성합니다. 데이터베이스는 큰 결과를 완전히 구체화한 다음 특정 오프셋에 도달할 때까지 결과를 건너뛸 필요 없이 지정된 키 세트 위치에서 훨씬 작은 결과만 구성하면 됩니다.

[Warning]
키 세트 필터링에서는 키 세트 속성(정렬에 사용되는 속성)이 null을 허용하지 않아야 합니다. 이 제한은 비교 연산자의 저장소별 null 값 처리와 인덱싱된 소스에 대해 쿼리를 실행해야 하기 때문에 적용됩니다. Null 허용 속성에 대한 키 집합 필터링은 예상치 못한 결과를 초래합니다.

Using KeysetScrollPosition with Repository Query Methods

interface UserRepository extends Repository<User, Long> {

  Window<User> findFirst10ByLastnameOrderByFirstname(String lastname, KeysetScrollPosition position);
}

WindowIterator<User> users = WindowIterator.of(position -> repository.findFirst10ByLastnameOrderByFirstname("Doe", position))
  .startingAt(ScrollPosition.keyset()); // (1) 

(1) 처음부터 시작하고 추가 필터링을 적용하지 마십시오.

키 집합 필터링은 데이터베이스에 정렬 필드와 일치하는 인덱스가 포함되어 있을 때 가장 잘 작동하므로 정적 정렬이 잘 작동합니다. Keyset-Filtering을 적용한 스크롤 쿼리는 정렬 순서에 사용된 속성이 쿼리에 의해 반환되어야 하며, 이러한 속성은 반환된 엔터티에 매핑되어야 합니다.

인터페이스 및 DTO 프로젝션을 사용할 수 있지만 키 세트 추출 실패를 방지하려면 정렬한 모든 속성을 포함해야 합니다.

Sort 순서를 지정할 때 쿼리와 관련된 정렬 속성을 포함하면 충분합니다. 원하지 않는 경우 고유한 쿼리 결과를 보장할 필요가 없습니다. 키 세트 쿼리 메커니즘은 각 쿼리 결과가 고유하도록 기본 키(또는 나머지 복합 기본 키)를 포함하여 정렬 순서를 수정합니다.

0개의 댓글