목차
쿼리 메소드 기능
•
메소드 이름으로 쿼리 생성
•
NamedQuery
•
@Query - 리포지토리 메소드에 쿼리 정의
•
파라미터 바인딩
•
반환 타입
•
페이징과 정렬
•
벌크성 수정 쿼리
•
@EntityGraph
스프링 데이터 JPA가 제공하는 마법 같은 기능
쿼리 메소드 기능 3가지
•
메소드 이름으로 쿼리 생성
•
메소드 이름으로 JPA NamedQuery 호출
•
@Query 언노테이션을 사용해서 리파지토리 인터페스에 쿼리 직접 정의
메소드 이름으로 쿼리 생성
메소드 이름을 분석해 JPQL 쿼리 실행
이름과 나이를 기준으로 회원을 조회하려면?
순수 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();
}
Java
복사
순수 JPA 테스트 코드
@Test
public void findByUserNameAndGreaterThen() {
Member m1 = new Member("AAA", 10);
Member m2 = new Member("AAA", 20);
jpaRepository.save(m1);
jpaRepository.save(m2);
List<Member> result = jpaRepository.findByUserNameAndAgeGreaterThan("AAA", 15);
assertThat(result.get(0).getUserName()).isEqualTo("AAA");
assertThat(result.get(0).getAge()).isEqualTo(20);
assertThat(result.get(0)).isEqualTo(m2);
}
Java
복사
스프링 데이터 JPA
package study.datajpa.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import study.datajpa.Entity.Member;
import java.util.List;
public interface MemberRepository extends JpaRepository<Member, Long> {
public List<Member> findByUserNameAndAgeGreaterThan(String userName, int age);
}
Java
복사
•
스프링 데이터 JPA는 메소드 이름을 분석해서 JPQL을 생성하고 실행
쿼리 메소드 필터 조건
스프링 데이터 JPA가 제공하는 쿼리 메소드 기능
•
조회: find...By, read...By, query...By, get...By
◦
예): findHelloBy 처럼..에 식별하기 위한 내용(설명)이 들어가도 된다.
•
COUNT: count...By 반환타입 long
•
EXISTS: exists...By 반환타입 boolean
•
삭제: delete...By, remove...By 반환타입 long
•
DISTINCT: findDistinct, findMemberDistinctBy
•
LIMIT: findFirst3, findFirst, findTop, findTop3
참고: 이 기능은 엔티티의 필드명이 변경되면 인터페이스에 정의한 메서드 이름도 꼭 함께 변경해야 한다. 그렇지 않으면 애플리케이션을 시작하는 시점에 오류가 발생한다.
이렇게 애플리케이션 로딩 시점에 오류를 인지할 수 있는 것이 스프링 데이터 JPA의 매우 큰 장점이다.
@Query, 리포지토리 메소드 쿼리 정의하기
메서드에 JPQL 쿼리 작성
public interface MemberRepository extends JpaRepository<Member, Long> {
public List<Member> findByUserNameAndAgeGreaterThan(String userName, int age);
@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);
}
Java
복사
•
@org.springfraework.data.jpa.repository.Query 언노테이션을 사용
•
실행할 메서드에 정적 쿼리를 직접 작성하므로 이름 없는 Named 쿼리라 할 수 있음
•
JPA Named 쿼리처럼 애플리케이션 실행 시점에 문법 오류를 발견할 수 있음(매우큰 장점)
참고: 실무에서는 메소드 이름으로 쿼리 생성 기능은 파라미터가 증갛면 메서드 이름이 매우 지저분해진다. 따라서 @Query기능을 자주 사용하게 된다.
@Query, 값, DTO 조회하기
단순히 값 하나를 조회
@Query("select m.userName from Member m")
List<String> findUserNameList();
Java
복사
JPA 값 타입(@Embedded )도 이 방식으로 조회할 수 있다.
DTO로 직접 조회
@Query("select new study.datajpa.dto.MemberDto(m.id, m.userName, t.name) from Member m join m.team t")
List<MemberDto> findMemberDto();
Java
복사
주의! DTO로 직접 조회 하려면 JPA의 new 명령어를 사용해야 한다. 그리고 다음과 같이 생성자가 맞는 DTO 가 필요하다.(JPA와 사용방식이 동일하다.)
package study.datajpa.dto;
import lombok.Data;
@Data
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;
TeamName = teamName;
}
}
Java
복사
파라미터 바인딩
컬렉션 파라미터 바인딩
Collection 타입으로 in절 지원
@Query("select m from Member m where m.userName in :names")
List<Member> findByNames(@Param("names") Collection<String> names);
Java
복사
반환 타입
스프링 데이터 JPA는 유연한 반환 타입 지원
//컬렌션 조회
List<Member> findMemberListByUserName(String name);
//단건 조회
Member findMemberByUserName(String name);
//단건 Optional 조회
Optional<Member> findOptionalMemberByUserName(String name);
Java
복사
조회 결과가 많거나 없으면?
•
컬렉션
◦
결과없음: 빈 컬렉션 반환
•
단건조회
◦
결과 없음: null 반환
◦
결과과 2건 이상: javax.persistence.NonUniqueResultException 예외 발생
참고: 단건으로 지정한 메서드를 호출하면 스프링 데이터 JPA는 내부에서 JPQL의 Query.getSingleResult() 메서드를 호출한다. 이 메서드를 호출했을 때 조회 결과값이 없으면
java.persistence.NoResultException예외가 발생하는데 개발자 입장에서 다루기가 상당히 불편하다. 스프링 데이터 JPA는 단건을 조회할 때 이 예외가 발생하면 예외를 무시하고 대신에 null을 반환한다.
순수 JPA페이징과 정렬
jpa에서 페이징을 어떻게 할 것인가?
다음 조건으로 페이징과 정렬을 사용하는 예제 코드를 보자.
•
검색조건: 나이가 10살
•
정렬조건: 이름으로 내림차순
•
페이징 조건: 첫 번째 페이지, 페이지당 보여줄 데이터는 3건
JPA페이징 리포지토리 코드
//순수 jpa 페이징
public List<Member> findByPage(int age, int offset, int limit) {
return em.createQuery("select m from Member m where m.age = :age order by m.userName desc")
.setParameter("age", age)
.setFirstResult(offset)
.setMaxResults(limit)
.getResultList();
}
// 총 카운트
public long totalCount(int age) {
return em.createQuery("select count(m) from Member m where m.age = :age ", Long.class)
.setParameter("age", age)
.getSingleResult();
}
Java
복사
JPA페이징 테스트 코드
@Test
public void 페이징확인() throws Exception {
//given
jpaRepository.save(new Member("member1", 10));
jpaRepository.save(new Member("member2", 10));
jpaRepository.save(new Member("member3", 10));
jpaRepository.save(new Member("member4", 10));
jpaRepository.save(new Member("member5", 10));
int age = 10;
int offset = 0;
int limit = 3;
//when
List<Member> members = jpaRepository.findByPage(age, offset, limit);
long totalCount = jpaRepository.totalCount(age);
//then
assertThat(members.size()).isEqualTo(3);
}
Java
복사
스프링 데이터 JPA 페이징과 정렬
실무에서 페이징 처리할때 많이 사용됨
페이징과 정렬 파라미터
•
org.springframework.data.domain.Sort: 정렬기능
•
org.springframework.data.domain.Pageable: 페이징 기능 (내부에 Sort 포함)
특별한 반환 타입
•
org.springframework.data.domain.Page: 추가 count 쿼리 결과를 포함하는 페이징
•
org.springframework.data.domain.Slice: 추가 count 쿼리 없이 다음 페이지만 확인 가능
(내부적으로 limit + 1 조회)
•
List(자바 컬렉션):추가 count 쿼리 없이 결과만 반환
페이징과 정렬 사용 예제
//count 쿼리 사용
Page<Member> findMemberPageByUserName(String name, Pageable pageable);
//count 쿼리 사용안함
Slice<Member> findMemberSliceByUserName(String name, Pageable pageable);
//count 쿼리 사용 안함
List<Member> findMemberListByUserName(String name, Pageable pageable);
List<Member> findByUserName(String name, Sort sort);
Java
복사
다음 조건으로 페이징과 정렬을 사용하는 예제 코드를 보자.
•
검색 조건: 나이가 10살
•
정렬 조건: 이름으로 내림차순
•
페이징 조건: 첫 번째 페이지, 페이지당 보여줄 데이터는 3건
Page 사용 예제 정의 코드
public interface MemberRepository extends JpaRepository<Member, Long> {
Page<Member> findByAge(int age, Pageable pageable);
}
Java
복사
Page 사용 예제 실행 코드
@Test
public void 페이징확인() throws Exception {
//given
memberRepository.save(new Member("member1", 10));
memberRepository.save(new Member("member2", 10));
memberRepository.save(new Member("member3", 10));
memberRepository.save(new Member("member4", 10));
memberRepository.save(new Member("member5", 10));
int age = 10;
//when
PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "userName"));
Page<Member> page = memberRepository.findByAge(age, pageRequest);
//then
List<Member> content = page.getContent(); // 조회된 데이터
assertThat(content.size()).isEqualTo(3); // 조회된 데이터 수
assertThat(page.getTotalElements()).isEqualTo(5); //전체 데이터 수
assertThat(page.getNumber()).isEqualTo(0); //페이지 번호
assertThat(page.getTotalPages()).isEqualTo(2); // 전체 페이지 번호
assertThat(page.isFirst()).isTrue(); // 첫번째 항목인가?
assertThat(page.hasNext()).isTrue(); // 다음 페이지가 있는가?
}
Java
복사
•
두 번째 파라미터로 받은 Pageable은 인터페이스다. 따라서 실제 사용할 때는 해당 인터페이스를 구현한 org.springframework.data.domain.PageRequest 객체를 사용한다.
•
pageRequest생성자의 첫 번째 파라미터에는 현재 페이지를, 두 번째 파라미터에는 조회할 데이터 수를 입력한다. 여기에 추가로 정렬 정보도 파라미터로 사용할 수 있다. 참고 페이지는 0부터 시작한다.
주의: page는 1부터 시작이 아니라 0부터 시작이다.
Page 인터페이스
public interface Page<T> extends Slice<T> {
int getTotalPages(); //전체 페이지 수
long getTotalElements(); //전체 데이터 수
<U> Page<U> map(Function<? super T, ? extends U> converter); //변환기
}
Java
복사
Slice 인터페이스
public interface Slice<T> extends Streamable<T> {
int getNumber(); //현재 페이지
int getSize(); //페이지 크기
int getNumberOfElements(); //현재 페이지에 나올 데이터 수
List<T> getContent(); //조회된 데이터
boolean hasContent(); //조회된 데이터 존재 여부
Sort getSort(); //정렬 정보
boolean isFirst(); //현재 페이지가 첫 페이지 인지 여부
boolean isLast(); //현재 페이지가 마지막 페이지 인지 여부
boolean hasNext(); //다음 페이지 여부
boolean hasPrevious(); //이전 페이지 여부
Pageable getPageable(); //페이지 요청 정보
Pageable nextPageable(); //다음 페이지 객체
Pageable previousPageable();//이전 페이지 객체
<U> Slice<U> map(Function<? super T, ? extends U> converter); //변환기
}
Java
복사
참고: Count 쿼리를 다음과 같이 분리할 수 있음
@Query(value = "select m from Member m", countQuery = "select count(m.userName) from Member m")
Page<Member> findMemberAllCountBy(Pageable pageable);
Java
복사
Top, First 사용 참고
List<Member> findTop3By();
Java
복사
페이지를 유지하면서 엔티티를 DTO로 변환하기
Page<Member> page = memberRepository.findByAge(age, pageRequest);
page.map(m -> new MemberDto(m.getId(), m.getUserName(), null));
Java
복사
실습
•
Page
•
Slice (count X) 추가로 limit + 1을 조회한다. 그래서 다음페이지 여부 확인(최근 모바일 리스트 생각해보면 됨)
•
List (Count X)
•
카운트 쿼리 분리(이건 복잡한 sql에서 사용, 데이터는 left join, 카운트는 left join 안해도 돰)
◦
실무에서 매우 중요!!
참고: 전체 count쿼리는 매우 무겁다.
벌크성 수정 쿼리
JPA를 사용한 벌크성 수정 쿼리
// 벌크성 수정 쿼리
public int bulkAgePlus(int age) {
int result = em.createQuery(
" update Member m set m.age = m.age + 1 " +
" where m.age <= : age")
.setParameter("age", age)
.executeUpdate();
return result;
}
Java
복사
JPA를 사용한 벌크성 수정 쿼리 테스트
@Test
public void 벌크업데이트테스트() {
//given
memberRepository.save(new Member("member1", 10));
memberRepository.save(new Member("member1", 19));
memberRepository.save(new Member("member1", 20));
memberRepository.save(new Member("member1", 21));
memberRepository.save(new Member("member1", 40));
//when
int resultCount = memberRepository.bulkAgePlus(20);
//then
assertThat(resultCount).isEqualTo(3);
}
Java
복사
스프링 데이터 JPA를 사용한 벌크성 수정 쿼리
@Modifying
@Query("update Member m set m.age = m.age + 1 where m.age >= :age")
int bulkAgePlus(@Param("age") int age);
Java
복사
스프링 데이터 JPA를 사용한 벌크성 수정 쿼리 테스트
@Test
public void 벌크업데이트테스트() {
//given
memberRepository.save(new Member("member1", 10));
memberRepository.save(new Member("member1", 19));
memberRepository.save(new Member("member1", 20));
memberRepository.save(new Member("member1", 21));
memberRepository.save(new Member("member1", 40));
//when
int resultCount = memberRepository.bulkAgePlus(20);
//then
assertThat(resultCount).isEqualTo(3);
}
Java
복사
•
벌크성 수정, 삭제 쿼리는 @Modifying 어노테이션을 사용
◦
사용하지 않으면 다음 예외 발생
◦
org.hibernate.hql.internal.QueryExecutionRequestException: Not supported for
DML operations
•
벌크성 쿼리를 실행하고 나서 영속성 컨텍스트 초기화: @Modifying(clearAutomatically = true)(이 옵션의 기본값은 false)
◦
이 옵션 없이 회원을 findById로 다시 조회하면 영속성 컨텍스트에 과거 값이 남아서 문제가 될 수 있다. 만약 다시 조회해야 하면 꼭 영속성 컨텍스트를 초기화 하자.
참고: 벌크 연산은 영속성 컨텍스트를 무시하고 실행하기 때문에, 영속성 컨텍스트에 있는 엔티티의 상태와 DB에 엔티티 상태가 달라질 수 있다.
권장하는 방안
1.
영속성 컨텍스트에 엔티티가 없는 상태에서 벌크 연산을 먼저 실행한다.
2.
부득이하게 영속성 컨텍스트에 에니티가 있으면 벌크 연산 직후 영속성 컨텍스트를 초기화 한다.
@EntityGraph
연관된 엔티티들을 SQL 한번에 조회하는 방법
member → team은 지연로딩 관계이다. 따라서 다음과 같이 team의 데이터를 조회할 때 마다 쿼리가 실행된다. (N+1 문제 발생)
연관된 엔티티를 한번에 조회하려면 페치 조인이 필요하다.
JPQL 페치 조인
//JPQL 페치조인
@Query("select m from Member m left join fetch m.team")
List<Member> findMemberFetchJoin();
Java
복사
스프링 데이터 JPA는 JPA가 제공하는 엔티티 그래프 기능을 편리하게 사용하게 도와준다. 이 기능ㅇ르 사용하면 JPQL없이 페치 조인을 사용할 수 있다.
EntityGraph
//공통 메서드 오버리아드 EntityGraph
@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);
Java
복사
EnitityGraph 정리
•
사실상 페치 조인(FETCH JOIN)의 간편 버전
•
LEFT OUTER JOIN 사용