JPA/SpringDataJPA

[SpringDataJPA] 쿼리 메소드 (Query Method)

걸어가는 신사 2022. 6. 3. 18:05

Query Method 란?

Spring Data JPA가 Method를 통해 쿼리를 생성하는 기능인 Query Method을 제공한다.

 

Query Method 기능 3가지

  • 메서드 이름으로 쿼리 생성
  • 메서드 이름으로 JPA NamedQuery 호출
  • @Query 어노테이션을 사용해서 Repository Interface에 쿼리 직접 정의

1. 메서드 이름으로 쿼리 생성

메서드 이름을 분석해서 JPQL 쿼리 실행

(1) 사용

순수 JPA 사용 시

public List<Member> findByUsernameAndAgeGreaterThan(String username, int age) {
		 return em.createQuery("select m from Member m where m.username = :username and m.age > :age")
		 .setParameter("username", username)
		 .setParameter("age", age)
		 .getResultList();
}
  • 순수 JPA 사용 시 em.createQuery를 통해서 직접 Query를 작성해주어야 한다.

SpringDataJPA 사용 시

public interface MemberRepository extends JpaRepository<Member, Long> {
		 List<Member> findByUsernameAndAgeGreaterThan(String username, int age);
}
  • SpringDataJPA는 메서드 이름을 분석해서 JPQL을 생성하고 실행

(2) SpringDataJPA가 제공하는 메서드 이름 내 지원되는 키워드

출처 : Spring Data JPA 공식 문서

Keyword Sample JPQL snippet
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 (parameter bound with appended %)
EndingWith findByFirstnameEndingWith … where x.firstname like ?1 (parameter bound with prepended %)
Containing findByFirstnameContaining … where x.firstname like ?1 (parameter bound wrapped in %)
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)
  • 조회 : find…By, read…By, query…By, get…By
  • COUNT : count…By
    • 반환 타입 : long
  • EXISTS : exists…By
    • 반환 타입 : boolean
  • 삭제 : delete…By, remove…By
    • 반환타입 : long
  • DISTINCT : findDistinct, findMemberDistinctBy
  • LIMIT : findFirst3, findFirst, findTop, findTop3

참고 : 이 기능은 엔티티의 필드명이 변경되면 인터페이스에 정의한 메서드 이름도 꼭 함께 변경해야 한다.
그렇지 않으면 애플리케이션을 시작하는 시점에 오류가 발생한다.

 

2. JPA NamedQuery

Spring Data JPA는 메서드 이름으로 JPA의 NamedQuery를 호출하는 기능을 제공한다.

(1) 사용

Entity에 NamedQuery 정의

@Entity
@NamedQuery(
 name="Member.findByUsername",
 query="select m from Member m where m.username = :username")
public class Member {
 ...
}
  • @NamedQuery 어노테이션으로 Named 쿼리 정의

순수 JPA에서 NamedQuery 호출

public class MemberRepository {
	 public List<Member> findByUsername(String username) {
 ...
		 List<Member> resultList =
				 em.createNamedQuery("Member.findByUsername", Member.class)
				 .setParameter("username", username)
				 .getResultList();
	}
}

SpringDataJPA에서 NamedQuery 호출

public interface MemberRepository extends JpaRepository<Member, Long> {
		 @Query(name = "Member.findByUsername")
		 List<Member> findByUsername(@Param("username") String username);
}
  • @Query 생략 가능
    • 메서드 이름만으로 Named 쿼리를 호출할 수 있다.
  • SpringDataJPA는 선언한 “도메인 클래스 + “,” + 메서드 이름”으로 Named 쿼리를 찾아서 실행
  • 만약 실행할 Named 쿼리가 없으면 메서드 이름으로 쿼리 생성 전략을 사용한다.

스프링 데이터 JPA를 사용하면 실무에서 NamedQuery를 직접 등록해서 사용하는 일은 드물다.
대신 @Query를 사용해서 Repository 메서드를 쿼리를 직접 정의한다.

 

3. @Query, Repository 메서드에 쿼리 정의하기

(1) 사용

SpringDataJPA

public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom {

    @Query("select m from Member m where m.username = :username and m.age = :age")
    List<Member> findUser(@Param("username") String username, @Param("age") int age);
}
  • @org.springframework.data.jpa.repository.Query 어노테이션을 사용한다.
  • 실행할 메서드에 정적 쿼리를 직접 작성하므로 이름 없는 Named 쿼리라 할 수 있다.
  • JPA Named 쿼리처럼 애플리케이션 실행 시점에 문법 오류를 발견할 수 있다.

실무에서는 메서드 이름으로 쿼리 생성 기능을 파라미터가 증가하면 메서드 이름이 매우 지저분해진다.
따라서 @Query 기능을 자주 사용하게 된다.

(2) @Query, 값 DTO 조회하기

단순히 값 하나를 조회

@Query("select m.username from Member m")
List<String> findUsernameList();

DTO로 직접 조회

@Query("select new study.data.jpa.dto.MemberDto(m.id, m.username, t.name) from Member m join m.team t")
List<MemberDto> findMemberDto();
  • DTO로 직접 조회하려면 JPA의 new 명령어를 사용해야 한다. 그리고 다음과 같이 생성자가 맞는 DTO가 필요하다.
public class MemberDto {

    private Long id;
    private String username;
    private String teamName;

    public MemberDto(Long id, String username, String teamName) {
        this.id = id;
        this.username = username;
        this.teamName = teamName;
    }
}

(3) 네이티브 SQL 사용

@Query(value = "SELECT * FROM MEMBER WHERE USERNAME = :username", nativeQuery = true)
List<Member> findByUsername(@Param("username") String username);
  • 네이티브 SQL을 사용하려면 @Query 어노테이션에 nativeQuery = true를 설정한다.
Spring Data JPA가 지원하는 파라미터 바인딩을 사용하면 JPQL은 위치 기반 파라미터를 1부터 시작하지만
네이티브 SQL은 0부터 시작한다.

 

Query Method 추가 기능

1. 파라미터 바인딩

(1) 위치 기반 VS 이름 기반

select m from Member m where m.username = ?0 //위치 기반
select m from Member m where m.username = :name //이름 기반

코드 가독성과 유지보수를 위해 이름 기반 파라미터 바인딩을 사용하자.

(2) 컬렉션 파라미터 바인딩

@Query("select m from Member m where m.username in :names")
List<Member> findByNames(@Param("names") List<String> names);
  • Collection 타입으로 in절 지원한다.

 

2. 반환 타입

  • 스프링 데이터 JPA는 유연한 반환 타입을 지원한다.
List<Member> findListByUsername(String username); //컬렉션
Member findMemberByUsername(String username); //단건
Optional<Member> findOptionalByUsername(String username); //단건 Optional

조회 결과가 많거나 없으면?

  • 컬렉션
    • 결과 없음 : 빈 컬렉션 반환
  • 단건 조회
    • 결과 없음 : null 반환
    • 결과가 2건 이상 : javax.persistence.NonUniqueResultException 예외 발생

 

3. 벌크성 수정 쿼리

JPA를 사용한 벌크성 수정 쿼리

public int bulkAgePlus(int age) {
        return em.createQuery("update Member m set m.age = m.age + 1"
                + " where m.age >= :age")
            .setParameter("age", age)
            .executeUpdate();
    }

Spring Data JPA 사용한 벌크성 수정 쿼리

@Modifying(clearAutomatically = true)
@Query("update Member m set m.age = m.age + 1 where m.age >= :age")
int bulkAgePlus(@Param("age") int age);
  • 벌크성 수정, 삭제 쿼리는 @Modifying 어노테이션을 사용한다.
    • 사용하지 않으면 다음 예외 발생
      • org.hibernate.hql.internal.QueryExecutionRequestException: Not supported for DML operations
    • executeUpdate()를 호출한다.
      • @Modifying 사용하지 않으면 .getResultList(), 혹은 .getSingleResult를 사용한다.
  • 벌크성 쿼리를 실행하고 나서 영속성 컨텍스트 초기화 : @Modifying(clearAutomatically = true)
    • 이 옵션 없이 회원을 findById로 다시 조회하면 영속성 컨텍스트에 과거 값이 남아서 문제가 될 수 있다.
    • 만약 다시 조회해야 한다면 영속성 컨텍스트를 초기화 하자.

벌크 연산은 영속성 컨텍스트를 무시하고 실행하기 때문에,
영속성 컨텍스트에 있는 엔티티의 상태와 DB에 엔티티 상태가 달라질 수 있다.

권장하는 방안

  1. 영속성 컨텍스트에 엔티티가 없는 상태에서 벌크 연산을 먼저 실행한다.
  2. 부득이하게 영속성 컨텍스트에 엔티티가 있으면 벌크 연산 직후 영속성 컨텍스트를 초기화 한다

 

4. @EntityGraph

연관된 엔티티들을 SQL 한 번에 조회하는 방법

Example ) member ⇒ team은 지연로딩 관계이다.

  • 따라서 다음과 같이 team의 데이터를 조회할 때마다 쿼리가 실행된다. (N+1 문제 발생)

(1) N+1 문제를 해결할 수 있는 방법

JPQL 페치 조인

@Query("select m from Member m left join fetch m.team")
List<Member> findMemberFetchJoin();
  • 페치 조인을 사용해서 Member select시에 연관된 team 엔티티를 한 번에 조회한다.

EntityGraph

  • 스프링 데이터 JPA는 JPA가 제공하는 엔티티 그래프 기능을 편리하게 사용하게 도와준다.
  • @EntityGraph 기능을 사용하면 JPQL 없이 페치 조인을 사용할 수 있다. (JPQL + 엔티티 그래프도 가능하다.)
//공통 메서드 오버라이드
@Override
@EntityGraph(attributePaths = {"team"})
List<Member> findAll();

//JPQL + 엔티티 그래프
@EntityGraph(attributePaths = {"team"})
@Query("select m from Member m")
List<Member> findMemberEntityGraph();

//메서드 이름으로 쿼리에서 특히 편리하다.
@EntityGraph(attributePaths = {"team"})
List<Member> findByUsername(String username)
  • 사실상 페치 조인 (FETCH JOIN)의 간편 버전
  • LEFT OUTER JOIN 사용

(2) NamedEntityGraph 사용 방법

@NamedEntityGraph(name = "Member.all", attributeNodes = @NamedAttributeNode("team"))
@Entity
public class Member {}
  • Entity Class 상단에 @NamedEntityGraph 어노테이션을 사용해서 정의한다.
@EntityGraph("Member.all")
@Query("select m from Member m")
List<Member> findMemberEntityGraph();
실무에서 NamedEntityGraph를 직접 등록해서 사용하는 일은 드물다.

 

5. JPA Hint

일종의 지시 구문인 Hint, SQL에서 Hint를 사용하듯이 JPA에서도 힌트를 전달할 수 있다.
  • SQL 힌트가 아니라 JPA 구현체에게 제공하는 힌트
    • JPA 표준이 제고하는 것이 아니라 Hibernate가 제공한다.

JPA Hint 사용

@QueryHints(value = { @QueryHint(name = "org.hibernate.readOnly",
 value = "true")},
 forCounting = true)
Page<Member> findByUsername(String name, Pagable pageable);
  • org.springframework.data.jpa.repository.QueryHints 어노테이션을 사용
  • "org.hibernate.readOnly" 값을 주게 되면 조회만 하고 변경 감지 기능을 사용하지 않는다.
  • forCounting : 반환 타입으로 Page 인터페이스를 적용하면 추가로 호출하는 페이징을 위한 count 쿼리도 쿼리 힌트 적용 (기본값 true)
항상 이러한 성능 튜닝에 관한 부분은 성능 테스트 후 판단하는 것이 좋다.
불필요하다고 판단되는 경우는 사용하지 않아도 된다.
반응형