본문 바로가기
SpringBoot

Spring data JPA

by moonchance 2024. 1. 11.

JPA 쿼리 방법

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

쿼리 조회 전략

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

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

선언된 쿼리

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

쿼리 생성

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

예 1. 메소드 이름으로 쿼리 생성
공용 인터페이스 UserRepository는 Repository<User, Long> {를 확장합니다.

  List<User> findByEmailAddressAndLastname(String emailAddress, String lastname);
}

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

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

표 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)
  In또한 배열이나 가변 인수뿐만 아니라 NotIn의 하위 클래스를 매개 변수로 사용합니다 . Collection동일한 논리 연산자의 다른 구문 버전에 대해서는 저장소 쿼리 키워드를 확인하세요 .
 
DISTINCT까다로울 수 있으며 항상 예상한 결과가 나오지 않을 수도 있습니다. 예를 들어 는 와 select distinct u from User u완전히 다른 결과를 생성합니다 select distinct u.lastname from User u. 첫 번째 경우에는 을 포함하므로 User.id아무것도 중복되지 않으므로 전체 테이블을 얻게 되며 테이블은 User개체로 구성됩니다.
User.lastname그러나 후자의 쿼리는 해당 테이블의 고유한 성을 모두 찾는 것으로 초점을 좁힙니다 . List<String>또한 > 결과 집합 대신 결과 집합이 생성됩니다 List<User.
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

주석 기반 구성

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

예시 2. 주석 기반 명명된 쿼리 구성
@Entity
@NamedQuery(name = "User.findByEmailAddress",
  query = "select u from User u where u.emailAddress = ?1")
public class User {

}
복사되었습니다!

JPA 명명된 쿼리 사용

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

XML 명명된 쿼리 정의

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

예 3. XML 명명된 쿼리 구성
<named-query name="User.findByLastname">
  <query>select u from User u where u.lastname = ?1</query>
</named-query>
복사되었습니다!

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

인터페이스 선언

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

예 4. UserRepository의 쿼리 메서드 선언
public interface UserRepository extends JpaRepository<User, Long> {

  List<User> findByLastname(String lastname);

  User findByEmailAddress(String emailAddress);
}
복사되었습니다!

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

사용@Query

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

쿼리 메서드에 주석이 추가된 쿼리는 @NamedQuery에서 선언된 쿼리를 사용하여 정의되거나 명명된 쿼리 보다 우선합니다 orm.xml.

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

예 5. 다음을 사용하여 쿼리 메서드에서 쿼리를 선언합니다.@Query
public interface UserRepository extends JpaRepository<User, Long> {

  @Query("select u from User u where u.emailAddress = ?1")
  User findByEmailAddress(String emailAddress);
}
복사되었습니다!

QueryRewriter 적용

때로는 얼마나 많은 기능을 적용하려고 해도 쿼리가 EntityManager.

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

예 6. 다음을 사용하여 QueryRewriter 선언@Query
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. 이 시나리오에서 Spring Data JPA는 해당 유형의 애플리케이션 컨텍스트에 등록된 Bean을 찾습니다.

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

예시 7. 예시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의 기반 주석 중 하나를 적용하거나 클래스 내부 메서드 의 일부로 포함하여 QueryRewriter애플리케이션 컨텍스트에 등록되었는지 확인해야 합니다 .@Component@Bean@Configuration

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

예제 8. 다음을 제공하는 리포지토리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각각 애플리케이션 컨텍스트에 등록된 둘 이상을 갖는 것이 바람직할 수 있습니다.

  CDI 기반 환경에서 Spring Data JPA BeanManager는 QueryRewriter.

LIKE고급 표현식 사용

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

예 9. like@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패턴으로 보강됩니다.

네이티브 쿼리

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

예 10. @Query를 사용하여 쿼리 메서드에서 기본 쿼리 선언
public interface UserRepository extends JpaRepository<User, Long> {

  @Query(value = "SELECT * FROM USERS WHERE EMAIL_ADDRESS = ?1", nativeQuery = true)
  User findByEmailAddress(String emailAddress);
}
복사되었습니다!
  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쿼리 복사본에 접미사를 추가하여 명명된 기본 쿼리에서도 작동합니다. 하지만 개수 쿼리에 대한 결과 집합 매핑을 등록해야 할 수도 있습니다.

정렬 사용

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

  참조할 수 없는 경로 표현식을 사용하면 Exception.

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

다음 예에서는 에 대한 안전하지 않은 옵션을 포함하여 Sort및 를 사용합니다 .JpaSortJpaSort

예제 12. Sort및 사용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"));                
repo.findByAndSort("stark", Sort.by("LENGTH(firstname)"));            
repo.findByAndSort("targaryen", JpaSort.unsafe("LENGTH(firstname)")); 
repo.findByAsArrayAndSort("bolton", Sort.by("fn_len"));
복사되었습니다!
  Sort도메인 모델의 속성을 가리키는 유효한 표현식입니다.
  포함된 함수 호출이 잘못되었습니다 Sort. 예외가 발생합니다.
  명시적으로 unsafe 를Sort 포함하는 유효합니다 . Order
  Sort별칭이 지정된 함수를 가리키는 유효한 표현식입니다.

대규모 쿼리 결과 스크롤

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

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

  1. 페이징 . Pageable이전 장에서 및 에 대해 배웠습니다 PageRequest.
  2. 오프셋 기반 스크롤 . 이는 총 결과 개수가 필요하지 않기 때문에 페이징보다 가벼운 변형입니다.
  3. 키셋-베이스 스크롤 . 이 방법은 데이터베이스 인덱스를 활용하여 오프셋 기반 결과 검색의 단점을 방지합니다 .

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

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

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

명명된 매개변수 사용

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

예 13. 명명된 매개변수 사용
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);
}
복사되었습니다!
  메소드 매개변수는 정의된 쿼리의 순서에 따라 전환됩니다.
  버전 4부터 Spring은 -parameters컴파일러 플래그를 기반으로 Java 8의 매개변수 이름 검색을 완벽하게 지원합니다. 디버그 정보 대신 빌드에서 이 플래그를 사용하면 @Param명명된 매개변수에 대한 주석을 생략할 수 있습니다.

SpEL 표현식 사용

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

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

예 14. 저장소 쿼리 메소드에서 SpEL 표현식 사용 - 엔터티 이름
@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사용할 수 있습니다 #{#entityName}.

  entityName주석을 사용하여 사용자 정의할 수 있습니다 @Entity. orm.xmlSpEL 표현식 에는 사용자 정의가 지원되지 않습니다.

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

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

예 15. 저장소 쿼리 메소드에서 SpEL 표현식 사용 - 상속이 포함된 엔터티 이름
@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. findAllByAttribute(…)또한 특수 저장소 인터페이스의 인스턴스에서 사용할 수 있는 일반 방법을 정의합니다 . findByAllAttribute(…)이제 on 을 호출하면 ConcreteRepository쿼리는 이 됩니다 select t from ConcreteType t where t.attribute = ?1.

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

예 16. 저장소 쿼리 메소드에서 SpEL 표현식 사용 - 인수 액세스.
@Query("select u from User u where u.firstname = ?1 and u.firstname=?#{[0]} and u.emailAddress = ?#{principal.emailAddress}")
List<User> findByFirstnameAndCurrentUserWithCustomQuery(String firstname);
복사되었습니다!

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

예 17. 저장소 쿼리 방법에서 SpEL 표현식 사용 - 와일드카드 단축키.
@Query("select u from User u where u.lastname like %:#{[0]}% and u.lastname like %:lastname%")
List<User> findByLastnameWithSpelExpression(@Param("lastname") String lastname);
복사되었습니다!

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

예 18. 저장소 쿼리 방법에서 SpEL 표현식 사용 - 입력 값 삭제.
@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. 사용되는 이스케이프 문자는 주석 escapeCharacter의 설정을 통해 구성할 수 있습니다 @EnableJpaRepositories. SpEL 컨텍스트에서 사용할 수 있는 메서드는 escape(String)SQL 및 JPQL 표준 와일드카드 _와 %. 기본 데이터베이스 또는 JPA 구현이 추가 와일드카드를 지원하는 경우 이러한 와일드카드는 이스케이프되지 않습니다.

다른 방법

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

  • 아직 작성하지 않았다면 를 사용하여 직접 쿼리를 작성하세요 @Query.
  • 이것이 귀하의 요구 사항에 맞지 않으면 맞춤 구현을 구현하는 것을 고려해 보세요 . 이를 통해 구현을 완전히 사용자에게 맡기면서 저장소에 메소드를 등록할 수 있습니다. 이는 다음과 같은 기능을 제공합니다.
    • EntityManager(순수한 HQL/JPQL/EQL/네이티브 SQL 작성 또는 Criteria API 사용 ) 과 직접 대화
    • Spring Framework의 JdbcTemplate(네이티브 SQL) 활용
    • 다른 타사 데이터베이스 도구 키트를 사용하세요.
  • 또 다른 옵션은 쿼리를 데이터베이스 내부에 넣은 다음 Spring Data JPA의 @StoredProcedure주석을 사용하거나 @Query주석  사용하고 CALL.

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

쿼리 수정

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

예 19. 조작 쿼리 선언
@Modifying
@Query("update User u set u.firstname = ?1 where u.lastname = ?2")
int setFixedFirstnameFor(String firstname, String lastname);
복사되었습니다!

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

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

파생된 삭제 쿼리

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

예 20. 파생된 삭제 쿼리 사용
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 쿼리(주석에 정의된 쿼리)를 실행합니다. 이는 현재 로드된 인스턴스라도 User호출된 수명 주기 콜백을 볼 수 없음을 의미합니다.

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

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

쿼리 힌트 적용

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

예제 21. 리포지토리 메서드와 함께 QueryHints 사용
public interface UserRepository extends Repository<User, Long> {

  @QueryHints(value = { @QueryHint(name = "name", value = "value")},
              forCounting = false)
  Page<User> findByLastname(String lastname, Pageable pageable);
}
복사되었습니다!

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

쿼리에 설명 추가

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

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

예시 22. @Meta저장소 작업에 주석 적용
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추가할 수 있습니다 .comment

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

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

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

최대 절전 모드 주석 활성화

Hibernate에서 쿼리 주석을 활성화하려면 hibernate.use_sql_comments로 설정해야 합니다 true.

Java 기반 구성 설정을 사용하는 경우 다음과 같이 수행할 수 있습니다.

예제 23. Java 기반 JPA 구성
@Bean
public Properties jpaProperties() {

	Properties properties = new Properties();
	properties.setProperty("hibernate.use_sql_comments", "true");
	return properties;
}
복사되었습니다!

파일이 있으면 persistence.xml거기에 적용할 수 있습니다.

예제 24. 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.

예제 25. Spring Boot 속성 기반 구성
spring.jpa.properties.hibernate.use_sql_comments=true

EclipseLink에서 쿼리 설명을 활성화하려면 eclipselink.logging.level.sql로 설정해야 합니다 FINE.

Java 기반 구성 설정을 사용하는 경우 다음과 같이 수행할 수 있습니다.

예제 26. Java 기반 JPA 구성
@Bean
public Properties jpaProperties() {

	Properties properties = new Properties();
	properties.setProperty("eclipselink.logging.level.sql", "FINE");
	return properties;
}
복사되었습니다!

파일이 있으면 persistence.xml거기에 적용할 수 있습니다.

예제 27. 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.

예제 28. Spring Boot 속성 기반 구성
spring.jpa.properties.eclipselink.logging.level.sql=FINE

Fetch 및 LoadGraph 구성

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

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

예 29. 엔터티에 명명된 엔터티 그래프 정의
@Entity
@NamedEntityGraph(name = "GroupInfo.detail",
  attributeNodes = @NamedAttributeNode("members"))
public class GroupInfo {

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

  …
}
복사되었습니다!

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

예 30. 저장소 쿼리 방법에서 명명된 엔터티 그래프 정의 참조.
public interface GroupRepository extends CrudRepository<GroupInfo, String> {

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

}
복사되었습니다!

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

예 31. 저장소 쿼리 방법에서 AD-HOC 엔터티 그래프 정의 사용.
public interface GroupRepository extends CrudRepository<GroupInfo, String> {

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

}
복사되었습니다!

스크롤링

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

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

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있는지 확인 하고 .WindowScrollPosition

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

while (users.hasNext()) {
  User u = users.next();
  // consume the user
}
복사되었습니다!

오프셋을 사용하여 스크롤

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

OffsetScrollPosition예 32. 리포지토리 쿼리 방법과 함께 사용
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());
복사되었습니다!
  위치의 초기 오프셋에서 시작합니다 0.

키셋 필터링을 사용한 스크롤

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

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

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

 
키 세트 필터링에서는 키 세트 속성(정렬에 사용되는 속성)이 null을 허용하지 않아야 합니다. 이 제한은 비교 연산자의 저장소별 null값 처리와 인덱싱된 소스에 대해 쿼리를 실행해야 하기 때문에 적용됩니다. Null 허용 속성에 대한 키 집합 필터링은 예상치 못한 결과를 초래합니다.
KeysetScrollPosition리포지토리 쿼리 방법과 함께 사용
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());
복사되었습니다!
  처음부터 시작하고 추가 필터링을 적용하지 마십시오.

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

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

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

 

 

- 출처 Spring Data JPA 공식문서

'SpringBoot' 카테고리의 다른 글

Querydsl 배워서 사용하기  (1) 2024.01.11