[Spring]스프링 데이터 JPA - 쿼리 메소드 기능
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를 사용한다는 기준으로 다음과 같이 구현될 수 있다.
사실 가독성이 별로고 직접 지정해줘야해서 불편하다.
📌 스프링 데이터 JPA로 발전시키기!!
public interface MemberRepository extends JpaRepository<Member, Long> {
List<Member> findByUsernameAndAgeGreaterThan(String username, int age);
}
스프링 데이터 JPA는 쿼리 메소드의 이름을 통해 필터 조건을 제시할 수 있다.
위의 순수 JPA보다 만들기도 쉽고 가독성도 매우 뛰어나다.
스프링 데이터 JPA가 제공하는 쿼리 메소드 기능
- 조회 메소드
- 해당 조건으로 엔티티를 반환하는 메소드이다.
List<User> findByLastName(String lastName);
List<User> readByFirstName(String firstName);
List<User> queryByAge(int age);
List<User> getByEmail(String email);
- COUNT 메소드
- 조건에 맞는 엔티티의 개수를 반환한다.
long countByStatus(String status);
- EXISTS 메소드
- 조건에 맞는 엔티티가 존재하는지 여부를 반환한다.
boolean existsByEmail(String email);
- DELETE 메소드
- 조건에 맞는 엔티티를 삭제한다.
long deleteById(Long id);
long removeByStatus(String status);
- LIMIT 메소드
- 결과의 개수를 제한하여 조회합니다.
- findFirst3, findTop5
List<User> findFirst3ByOrderByAgeAsc();
User findFirstByOrderByLastNameAsc();
List<User> findTopByOrderBySalaryDesc();
List<User> findTop3ByOrderByScoreDesc();
📌 @Query 사용하기
- 해당 기능은 리포지토리에서 쿼리를 직접 정의하는 것이다.
public interface MemberRepository extends JpaRepository<Member, Long> {
@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);
}
해당처럼 파라미터를 바인딩해서 받고 직접 정의한 메소드를 사용할 수 있다.
DTO 커스텀해서 직접 조회하기
- @Query는 받아오는 DTO를 직접 정의할 수 있다.
@Query("select new study.datajpa.dto.MemberDto(m.id, m.username, t.name) " +
"from Member m join m.team t")
List<MemberDto> findMemberDto();
@Data
public class MemberDto {
private Long id;
private String username;
private String teamName;
}
📌 JPA 페이징과 정렬
- 스프링 데이터 JPA에서 사용하는 페이징은 패키지로 구현되어 있다.
- org.springframework.data.domain.Sort : 정렬 기능
- org.springframework.data.domain.Pageable : 정렬 기능
- 특별한 반환 타입도 있다.
- org.springframework.data.domain.Page : 추가 count 쿼리 결과를 포함하는 페이징
- org.springframework.data.domain.Slice : 추가 count 쿼리 없이 다음 페이지만 확인 가능
슬라이스는 다음 더보기만 존재할 때 사용하고 페이지는 페이지를 게시판 같은 페이징 기능이 필요할 때 사용한다.
public interface MemberRepository extends Repository<Member, Long> {
Page<Member> findByAge(int age, Pageable pageable);
}
페이징을 통해 다음과 같은 정보를 얻을 수 있다.
Pageable pageable = PageRequest.of(0, 10); // 첫 페이지, 한 페이지에 10개의 요소
Page<User> usersPage = userRepository.findAll(pageable);
// 총 페이지 수
int totalPages = usersPage.getTotalPages();
// 총 요소 수
long totalElements = usersPage.getTotalElements();
// 현재 페이지 번호 (0부터 시작)
int currentPage = usersPage.getNumber();
// 현재 페이지의 데이터
List<User> users = usersPage.getContent();
// 다음 페이지가 있는지 여부
boolean hasNext = usersPage.hasNext();
// 이전 페이지가 있는지 여부
boolean hasPrevious = usersPage.hasPrevious();
📝 벌크성 수정 쿼리
모든 객체에 대한 정보를 수정하는 쿼리이다.
이런 것을 벌크성 수정 쿼리라 부른다.
순수한 JPA를 사용한 벌크성 수정 쿼리
public int bulkAgePlus(int age) {
int resultCount = em.createQuery(
"update Member m set m.age = m.age + 1" +
"where m.age >= :age")
.setParameter("age", age)
.executeUpdate();
return resultCount;
}
스프링 JPA를 사용한 벌크성 수정 쿼리
@Modifying
@Query("update Member m set m.age = m.age + 1 where m.age >= :age")
int bulkAgePlus(@Param("age") int age);
순수 JPA를 사용했을 때는 기존과 비슷한 방식을 사용한다.
그러나 스프링 JPA를 사용했을 때는 @Modifying이 들어간다.
📌 @Modifying
- 벌크성 수정, 삭제 쿼리는 @Modifying 어노테이션을 사용해야한다.
- 사용하지 않으면 예외가 발생한다.
@Test
public void bulkUpdate() throws Exception {
//given
memberRepository.save(new Member("member1", 10));
memberRepository.save(new Member("member2", 19));
memberRepository.save(new Member("member3", 20));
memberRepository.save(new Member("member4", 21));
memberRepository.save(new Member("member5", 40));
//when
int resultCount = memberRepository.bulkAgePlus(20);
Member member = memberRepository.findByUsername("member5");
System.out.println(member)
//then
assertThat(resultCount).isEqualTo(3);
}
해당 조회의 경우 벌크 연산으로 인해 DB가 41살이 되지만 영속성 컨텍스트로 인해 출력은 40살로 나오기에
벌크 연산 진행 후 영속성 컨텍스트를 초기화 해줘야 한다.
- 벌크성 쿼리를 실행하고 나서 영속성 컨텍스트 초기화 여부를 설정할 수 있다.
- @Modifying(clearAutomatically = true) = 벌크 연산 실행 후 자동으로 영속성 컨텍스트 초기화
- 이 옵션 없이 회원을 findById로 다시 조회하면 영속성 컨텍스트에 과거 값이 남아서 문제가 될 수 있으므로 만약 다시 조회해야 한다면 꼭 영속성 컨텍스트를 초기화해야 한다.
참고 : 벌크 연산은 영속성 컨텍스트를 무시하고 실행하기 때문에, 영속성 컨텍스트에 있는 엔티티의 상태와 DB에 엔티티 상태가 달라질 수 있다.
권장하는 방안은
1. 영속성 컨텍스트에 엔티티가 없는 상태에서 벌크 연산을 먼저 실행
2. 벌크 연산 직후 영속성 컨텍스트 초기화
📝 @EntityGraph
나는 전 포스팅에서 모든 관계는 지연 로딩관계를 하라고 배웠다.
이것을 효율적으로 조회하는 것을 알아보자
✔ 무슨 효율성?
지연 로딩 관계에서 객체 조회 지연로딩 관계인 것을 조회하면 그때서야 조회 쿼리를 날려 지연로딩 관계를 조회한다.
만약 우리가 객체 조회 후 지연 로딩 관계의 정보들을 모두 조회한다면 어떻게 될까??
@Test
public void findMemberLazy() throws Exception {
//given
//member1 -> teamA
//member2 -> teamB
Team teamA = new Team("teamA");
Team teamB = new Team("teamB");
teamRepository.save(teamA);
teamRepository.save(teamB);
memberRepository.save(new Member("member1", 10, teamA));
memberRepository.save(new Member("member2", 20, teamB));
em.flush();
em.clear();
//when
List<Member> members = memberRepository.findAll();
//then
for (Member member : members) {
member.getTeam().getName();
}
}
이렇게 되면
N + 1번의 쿼리가 날아오게 된다.
- List members = memberRepository.findAll() : 1번
- member.getTeam().getName() : 조회된 모든 맴버에 대한 Team 쿼리를 날림
📌 페치 조인
이러한 N + 1번의 문제를 해결할 때는 페치 조인을 사용한다.
즉 처음 조회 때 모든 조회를 다하여 1번만 조회하게 하는 것이다.
@Query("select m from Member m left join fetch m.team")
List<Member> findMemberFetchJoin();
위 코드는 Member를 조회할 때 그의 지연로딩인 team도 같이 조회하는 것이다.
📌 @EntityGraph
@EntityGraph는 사실상 페치 조인의 간편 버전이다.
참고로, LEFT OUTER JOIN을 사용한다.
//공통 메서드 오버라이드
@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)
해당 코드처럼 어노테이션만 붙이면 페치조인을 자동으로 해준다.
참고, 간단간단할 때는 사용해도 되나 왠만하면 페치조인을 직접 사용하자...
📝 JPA Hint
JPA 힌트는 쿼리 실행 시 JPA 구현체에게 특정 동작을 지시하거나
성능을 최적화하기 위해 추가 정보를 제공하는 방법이다.
public interface MemberRepository extends JpaRepository<Member, Long> {
@Query("SELECT m FROM Member m WHERE m.status = :status")
@QueryHints(@QueryHint(name = "org.hibernate.cacheable", value = "true"))
List<Member> findByStatus(@Param("status") String status);
}
위처럼 쉽게 적용할 수 있고 다양한 전략이 존재한다.
캐시 관련 힌트
- org.hibernate.cacheable: 결과를 2차 캐시에 저장할지 여부를 지정합니다.
@QueryHint(name = "org.hibernate.cacheable", value = "true")
- javax.persistence.cache.retrieveMode: 엔티티를 가져올 때 캐시에서 검색할지 여부를 지정합니다.
@QueryHint(name = "javax.persistence.cache.retrieveMode", value = "USE")
- javax.persistence.cache.storeMode: 엔티티를 저장할 때 캐시에 저장할지 여부를 지정합니다.
@QueryHint(name = "javax.persistence.cache.storeMode", value = "REFRESH")
락킹 관련 힌트
- javax.persistence.lock.timeout: 락을 얻기 위해 대기할 최대 시간을 지정합니다.
@QueryHint(name = "javax.persistence.lock.timeout", value = "3000")
페치 전략 힌트
- org.hibernate.fetchSize: 페치할 데이터의 크기를 지정합니다
@QueryHint(name = "org.hibernate.fetchSize", value = "50")
기타 성능 최적화 힌트
- org.hibernate.readOnly: 읽기 전용 쿼리로 설정합니다.
@QueryHint(name = "org.hibernate.readOnly", value = "true")
- javax.persistence.query.timeout: 쿼리 실행 시간 초과를 설정합니다.
@QueryHint(name = "javax.persistence.query.timeout", value = "5000")