쿼리 메소드 기능
스프링 데이터 JPA가 제공하는 마법 같은 기능
쿼리 메소드 기능 3가지
- 메소드 이름으로 쿼리 생성
- 메소드 이름으로 JPA NamedQuery 호출
- @Query 어노테이션을 사용해서 리파지토리 인터페이스에 쿼리 직접 정의
메소드 이름으로 쿼리 생성
이름과 나이를 기준으로 회원을 조회하는 메소드를
순수 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로 구현하면
public interface MemberRepository extends JpaRepository<Member, Long> {
List<Member> findByUsernameAndAgeGreaterThan(String username, int age);
}
스프링 데이터 JPA는 메소드 이름을 분석해서 JPQL을 생성하고 실행
쿼리 메소드 필터 조건
스프링 데이터 JPA 공식 문서 참고
스프링 데이터 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의 매우 큰 장점이다.
JPA NamedQuery
JPA의 NamedQuery를 호출할 수 있음
[ 실무에서 거의 쓸 일이 없다. ]
@NamedQuery 어노테이션으로 Named 쿼리 정의
@NamedQuery(
name = "Member.findByUsername",
query = "select m from Member m where m.username = :username"
)
public class Member {
JPA를 직접 사용해서 Named 쿼리 호출
public List<Member> findByUsername(String username) {
return em.createNamedQuery("Member.findByUsername").setParameter("username", username).getResultList();
}
스프링 데이터 JPA로 NamedQuery 사용
기본적으로 엔티티에 @NamedQuery로 정의가 되어있어야하고, 스프링 데이터 jpa로 편하게 불러와서 사용하는법
public interface MemberRepository extends JpaRepository<Member, Long> {
@Query(name = "Member.findByUsername")
List<Member> findByUsername(@Param("username") String username);
}
@Query를 사용한다면 메소드명은 자유롭게 적어도된다.
@Query 를 생략하고 메서드 이름만으로 Named 쿼리를 호출할 수 있다.
public interface MemberRepository extends JpaRepository<Member, Long> {
List<Member> findByUsername(@Param("username") String username);
}
스프링 데이터 JPA는 선언한 "도메인 클래스 + .(점) + 메서드 이름"으로 Named 쿼리를 찾아서 실행
[name = "Member.findByUsername" 과 같이 설정해놨기에 되는것 ]
만약 실행할 Named 쿼리가 없으면 메서드 이름으로 쿼리 생성 전략을 사용한다.
필요하면 전략을 변경할 수 있지만 권장하지 않는다.
Named 쿼리는 어플리케이션 로딩 시점에 해당 SQL문을 파싱하는데 그때 오류가 있다면 에러를 발생시켜서 문법 오류가 있다는것을 알려준다.
그래서 개발자는 어플리케이션 로딩시점에 오류를 해결할 수 있다.
참고
스프링 데이터 JPA를 사용하면 실무에서 Named Query를 직접 등록해서 사용하는 일은 드물다.
대신 @Query 를 사용해서 리파지토리 메소드에 쿼리를 직접 정의한다
@Query, 리포지토리 메소드에 쿼리 정의하기
메서드에 JPQL 쿼리 작성
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);
}
메소드 이름으로 쿼리 작성 방법은 이름이 길어질 가능성이 있다.
@Query를 이용하여 직접 JPQL을 작성해서 해당 메소드에 쿼리를 정의할 수 있다.
@org.springframework.data.jpa.repository.Query 어노테이션을 사용
실행할 메서드에 정적 쿼리를 직접 작성하므로 이름 없는 Named 쿼리라 할 수 있음
JPA Named 쿼리처럼 애플리케이션 실행 시점에 문법 오류를 발견할 수 있음(매우 큰 장점!)
간단한 쿼리는 메소드이름으로 작성하고, 조금 복잡한것은 @Query를 이용하여 직접 작성
동적쿼리는 queryDSL을 이용한다.
참고
실무에서는 메소드 이름으로 쿼리 생성 기능은 파라미터가 증가하면 메서드 이름이 매우 지저분해진다.
따라서 @Query 기능을 자주 사용하게 된다.
@Query, 값, DTO 조회하기
이번에는 @Query를 이용하여 엔티티가 아닌, 값이나 DTO를 조회하는법
단순히 값 하나를 조회
@Query("select m.username from Member m")
List<String> findUserNameList();
JPA 값 타입( @Embedded )도 이 방식으로 조회할 수 있다.
DTO로 직접 조회
@Data
@AllArgsConstructor
public class MemberDto {
private Long id;
private String username;
private String teamName;
}
@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가 필요하다. (JPA와 사용방식이 동일하다.)
파라미터 바인딩
- 위치 기반
- 이름 기반
JPQL을 만들때 파라미터의 바인딩 방법 2가지
select m from Member m where m.username = ?0 //위치 기반
select m from Member m where m.username = :name //이름 기반
결론 : 이름기반을 사용해야한다. [ 위치 기반은 중간에 새로운 파라미터가 추가되면 위치값을 다 바꿔야한다. ]
참고: 코드 가독성과 유지보수를 위해 이름 기반 파라미터 바인딩을 사용하자 (위치기반은 순서 실수가 바꾸면…)
컬렉션 파라미터 바인딩
Collection 타입으로 in절 지원
[ 많이 사용하는 기능 ]
@Query("select m from Member m where m.username in :names")
List<Member> findByNames(@Param("names")List<String> names);
반환 타입
스프링 데이터 JPA는 유연한 반환 타입 지원
List<Member> findByUsername(String name); //컬렉션
Member findByUsername(String name); //단건
Optional<Member> findByUsername(String name); //단건 Optional
@Query("select m from Member m where m.username in :names")
Stream<Member> fff(@Param("names")List<String> names);
조회 결과가 많거나 없으면?
컬렉션
결과 없음 : 빈 컬렉션 반환
단건 조회
결과 없음 : null 반환
결과가 2건 이상: javax.persistence.NonUniqueResultException 예외 발생
[ 위의 예외는 jpa예외이고 jpa가 알아서 스프링예외로 변환한것도 같이 발생 ]
단건 조회일때 데이터가 있을수도있고 없을수도있다면 Optional 사용
단건 조회인데 결과가 2개이상이면 Optional이든 아니든 상관없이 예외발생
List는 optional로 감싸는게 아니다.
List는 비어있던,아니던 항상 존재하니까
Optional이 비어있을수가없다.
List 자체를 반환타입으로 받고 비어있는지아닌지를 체크해서 예외처리를 해야한다.
순수 JPA 페이징과 정렬
JPA에서 페이징을 어떻게 할 것인가?
다음 조건으로 페이징과 정렬을 사용하는 예제 코드를 보자.
검색 조건: 나이가 10살
정렬 조건: 이름으로 내림차순
페이징 조건: 첫 번째 페이지, 페이지당 보여줄 데이터는 3건
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();
}
현재 몇번페이지이고, 다음페이지가 있는지 등 과 관련된 페이징 관련 로직은 순수 jpa를 사용한다면
직접 구현해야하지만
spring data jpa는 제공한다.
@Test
public void paging() throws Exception {
//given
memberJpaRepository.save(new Member("member1", 10));
memberJpaRepository.save(new Member("member2", 10));
memberJpaRepository.save(new Member("member3", 10));
memberJpaRepository.save(new Member("member4", 10));
memberJpaRepository.save(new Member("member5", 10));
int age = 10;
int offset = 0;
int limit = 3;
//when
List<Member> members = memberJpaRepository.findByPage(age, offset, limit);
long totalCount = memberJpaRepository.totalCount(age);
//페이지 계산 공식 적용...
// totalPage = totalCount / size ...
// 마지막 페이지 ...
// 최초 페이지 ..
//then
assertThat(members.size()).isEqualTo(3);
assertThat(totalCount).isEqualTo(5);
}
dialect라는 데이터베이스 방언을 데이터베이스에 맞게 변경해준다면
데이터베이스가 변경되어도 같은 메소드로 잘 동작한다.
스프링 데이터 JPA 페이징과 정렬
페이징과 정렬 파라미터
org.springframework.data.domain.Sort : 정렬 기능
org.springframework.data.domain.Pageable : 페이징 기능 (내부에 Sort 포함)
해당 인터페이스2개로 어떤 DB든 페이징과 정렬 기능을 공통화 시켜놨다.
특별한 반환 타입
org.springframework.data.domain.Page : 추가 count 쿼리 결과를 포함하는 페이징
현재 몇번째 페이지인지 알기위해 totalcount까지 반환해주는 페이징
org.springframework.data.domain.Slice : 추가 count 쿼리 없이 다음 페이지만 확인 가능(내부적으로 limit + 1조회)
페이지없이 더보기 버튼누르면 게시글 몇개 더 보이는 그런 기능을 구현하기 위한 페이징
[ 한개 더가져와서, 한개 더가져와지면 더보기 버튼을 활성화 시킨다던지, 미리 다음 데이터를 가져와놓는다던지 등에 쓰임]
List (자바 컬렉션): 추가 count 쿼리 없이 결과만 반환
Page 사용 예제 정의 코드
public interface MemberRepository extends JpaRepository<Member, Long> {
Page<Member> findByAge(int age, Pageable pageable); // Pageable에는 쿼리에 대한 조건이 들어간다.
}
Page 사용 예제 실행 코드
//페이징 조건과 정렬 조건 설정
@Test
public void page() 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));
//when , PageRequest의 부모 인터페이스는 Pageable이다 즉 페이징 조건을 설정하는 클래스
PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC,"username"));
// 스프링 데이터 jpa는 0번페이지 부터 시작한다 , [몇번페이지,몇개,정렬기준] , 정렬기준은 없어도 된다.
Page<Member> page = memberRepository.findByAge(10, pageRequest);
// 반환 타입이 Page라면 totalcount를 위한 쿼리까지 알아서 실행되면서 totalcount를 구해온다.
//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(); //다음 페이지가 있는가?
}
두 번째 파라미터로 받은 Pageable 은 인터페이스다.
따라서 실제 사용할 때는 해당 인터페이스를 구현한 org.springframework.data.domain.PageRequest 객체를 사용한다.
PageRequest 생성자의 첫 번째 파라미터에는 현재 페이지를, 두 번째 파라미터에는 조회할 데이터 수를 입력 한다.
여기에 추가로 정렬 정보도 파라미터로 사용할 수 있다. 참고로 페이지는 0부터 시작한다.
Page 인터페이스
public interface Page<T> extends Slice<T> {
int getTotalPages(); //전체 페이지 수
long getTotalElements(); //전체 데이터 수
<U> Page<U> map(Function<? super T, ? extends U> converter); //변환기
}
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); //변환기
}
그리고 page 객체가 아닌
List<Member> findByAge(int age, Pageable pageable);
List<Member> members = memberRepository.findByAge(10, pageRequest);
로도 가능하다.
페이징 정보가 필요없고 해당 페이지의 데이터만 필요하다면 바로 데이터만 받아올 수 있다.
페이징쿼리가 복잡해지면 totalcount쿼리 또한 복잡해진다.
[페이징쿼리에서는 left outer join을 한다고 치면 , totalcount를 구하는 쿼리에서는 안해도된다.
근데 내부적으로 생성되는 totalcount를 구하는 쿼리는 left outer join을 하는듯하다. 그래서 성능이 안나옴
그래서 직접 totalcount 를 구하는 쿼리를 작성하여 성능을 높이는 작업을 실무에서는 매우 중요하다고함]
최적화를 위해 따로 작성하고싶다면 count 쿼리를 다음과 같이 분리할 수 있다.
sorting 또한 복잡해지면 직접 작성하면된다.
@Query(value = "select m from Member m",
countQuery = "select count(m.username) from Member m")
Page<Member> findMemberAllCountBy(Pageable pageable);
페이지를 유지하면서 엔티티를 DTO로 변환하기
Page<Member> page = memberRepository.findByAge(10, pageRequest);
Page<MemberDto> dtoPage = page.map(m -> new MemberDto(m.getId(),m.getUsername(),null));
엔티티를 API반환으로 사용하면 안되니까 DTO로 변환하는것을 map이라는 메소드를 지원한다.
Page<DTO> 자체를 API의 반환으로 줘도
Page안에 데이터가 알아서 JSON으로 반환된다. [ 페이징 정보라던지, 데이터라던지가 잘 JSON으로 변환되어 전달됨]
참고 -> 스프링 부트 3 - 하이버네이트 6 left join 최적화
스프링 부트 3 이상을 사용하면 하이버네이트 6이 적용된다.
이 경우 하이버네이트 6에서 의미없는 left join을 최적화 해버린다.
따라서 다음을 실행하면 SQL이 LEFT JOIN을 하지 않는 것으로 보인다
@Query(value = "select m from Member m left join m.team t")
Page<Member> findByAge(int age, Pageable pageable);
실행 결과 - SQL
select
m1_0.member_id,
m1_0.age,
m1_0.team_id,
m1_0.username
from
member m1_0
실행한 JPQL을 보면 left join을 사용하고 있다.
select m from Member m left join m.team t Member 와 Team 을 조인을 하지만
사실 이 쿼리를 Team 을 전혀 사용하지 않는다.
select 절이나, where 절에서 사용하지 않는다는 뜻이다.
그렇다면 이 JPQL은 사실상 다음과 같다. select m from Member m
left join 이기 때문에 왼쪽에 있는 member 자체를 다 조회한다는 뜻이 된다.
만약 select 나, where 에 team 의 조건이 들어간다면 정상적인 join 문이 보인다.
여기서 만약 Member 와 Team 을 하나의 SQL로 한번에 조회하고 싶으시다면 JPA가 제공하는 fetch join을 사용해야 한다. ( fetch join 은 JPA 기본편 참고)
select m from Member m left join fetch m.team t 이 경우에도 SQL에서 join문은 정상 수행된다.
벌크성 수정 쿼리
jpa에서는 변경감지를 통해 update를 진행한다.
하지만 많은 데이터를 조회해와서 수정을 진행하고 변경감지로 update하는것보다는
update 쿼리 한번으로 데이터들을 수정하는게 편할것이다.
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();
}
반환된값 == 수정된 데이터 수
스프링 데이터 JPA를 사용한 벌크성 수정 쿼리
@Modifying // 이 어노테이션이 있어야 순수 jpa에서 사용했던 executeUpdate()를 실행해준다. 이게 없다면 getSingleResult()같은걸 실행해준다. 해당 어노테이션으로 수정이라는것을 명시해준다.
@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
jpa는 영속성컨텍스트를 통해 엔티티를 관리하면서 쿼리를 날리는데
벌크성 수정쿼리는 영속성컨텍스트와 관련없이 그냥 DB에 쿼리를 날리는 형태이다.
@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);
//then
assertThat(resultCount).isEqualTo(3);
}
이 코드를 보면 테스트코드이고, 테스트 클래스레벨에 @Transactional이 존재한다.
즉 이 메소드가 시작하면 트랜잭션이 시작하고, 이 메소드가 종료하면 트랜잭션이 종료된다.
여기서 new Member한 엔티티들은 영속성 컨텍스트 1차캐시에서 관리가 되지만 아직 커밋,강제 flush등이 없기에 DB에는 쿼리가 적용되지않은 상태이다.
이때 벌크성 수정쿼리로 DB에 쿼리를 실행시키면 존재하지도않은 데이터를 수정하는것이므로 에러가 발생한다.
[ JPQL 쿼리 진행전에 jpa가 flush를 진행하므로 위에 문제는 발생하지않는다.]
[ 위에서 bulkAgePlus는 JPQL 쿼리 이므로 해당 메소드 실행전 flush 진행된다.]
벌크성 수정쿼리로 DB에 쿼리를 실행시켰고 수정된 데이터가 영속성컨텍스트에서 관리되고 엔티티였다면
DB에서의 데이터 정보와 현재 영속성컨텍스트에서 관리되고있는 엔티티 정보가 다를 수 있다.
그러므로 벌크성 수정쿼리를 진행하고 나서 영속성컨텍스트를 초기화해줘야한다.
그러면 다시 조회해오기때문에 최신화된 엔티티를 가져올 수 있다.
벌크성 쿼리를 실행하고 나서 영속성 컨텍스트 초기화: @Modifying(clearAutomatically = true)
(이 옵션의 기본값은 false )
이 옵션 없이 회원을 findById 로 다시 조회하면 영속성 컨텍스트에 과거 값이 남아서 문제가 될 수 있다.
만약 다시 조회해야 하면 꼭 영속성 컨텍스트를 초기화 하자.
@Modifying(clearAutomatically = true)
@Query("update Member m set m.age = m.age +1 where m.age > :age")
int bulkAgePlus(@Param("age") int age);
참고
벌크 연산은 영속성 컨텍스트를 무시하고 실행하기 때문에,
영속성 컨텍스트에 있는 엔티티의 상태와 DB에 엔티티 상태가 달라질 수 있다.
권장하는 방안
1. 영속성 컨텍스트에 엔티티가 없는 상태에서 벌크 연산을 먼저 실행한다.
2. 부득이하게 영속성 컨텍스트에 엔티티가 있으면 벌크 연산 직후 영속성 컨텍스트를 초기화 한다.
마이바티스까지 연동해서 사용하는경우도 조심해야한다.
마이바티스로 진행한 쿼리는 영속성컨텍스트에서 관리되고있는 엔티티에는 적용이 안되는 사항이므로
마이바티스 쿼리를 진행했다면 영속성컨텍스트를 초기화해준다.
@EntityGraph
연관된 엔티티들을 SQL 한번에 조회하는 방법
@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();
}
}
member -> team은 지연로딩 관계이다. 따라서 다음과 같이 team의 데이터를 조회할 때 마다 쿼리가 실행된다. [member.getTeam().getName()을 하게되면 Team은 프록시객체이므로 프록시초기화를 위해 팀을 조회하는 쿼리가 발생한다. 이로인해 멤버를 조회하는 쿼리1번 + 멤버조회쿼리결과데이터갯수마다 팀을 조회해야하니까 N번 ]
N+1문제 발생 -> N은 조회쿼리1번에서 조회된 데이터 갯수
N+1문제를 해결하기위해 연관된 엔티티를 한번에 조회하려면 페치 조인이 필요하다.
참고
//Hibernate 기능으로 확인
Hibernate.isInitialized(member.getTeam())
//JPA 표준 방법으로 확인
PersistenceUnitUtil util =
em.getEntityManagerFactory().getPersistenceUnitUtil();
util.isLoaded(member.getTeam());
JPQL 페치 조인
public interface MemberRepository extends JpaRepository<Member, Long> {
@Query("select m from Member m left join fetch m.team")
List<Member> findMemberFetchJoin();
}
스프링 데이터 JPA는 JPA가 제공하는 엔티티 그래프 기능을 편리하게 사용하게 도와준다.
이 기능을 사용하면 JPQL 없이 페치 조인을 사용할 수 있다. (JPQL + 엔티티 그래프도 가능)
EntityGraph
//공통 메서드 오버라이드 -> interface JpaRepository<T, ID>에서 제공되는 findAll()을 오버라이드[재정의]
@Override
@EntityGraph(attributePaths = {"team"})
List<Member> findAll();
@EntityGraph를 이용해서 fetch join할 속성[필드]를 설정해주면 객체그래프를 한번에 엮어서
JPQL의 fetch join이 알아서 진행된다.
//JPQL + 엔티티 그래프
@EntityGraph(attributePaths = {"team"})
@Query("select m from Member m")
List<Member> findMemberEntityGraph();
직접 JPQL 쿼리를 짜고 거기다가 fetch join을 추가하고싶을때도 가능하다.
//메서드 이름으로 쿼리에서 특히 편리하다.
@EntityGraph(attributePaths = {"team"})
List<Member> findByUsername(String username)
EntityGraph 정리
- 사실상 페치 조인(FETCH JOIN)의 간편 버전
- 기본값으로 LEFT OUTER JOIN 사용된다.
NamedEntityGraph 사용 방법 [거의 사용하지않음]
@NamedEntityGraph(name = "Member.all", attributeNodes =
@NamedAttributeNode("team"))
@Entity
public class Member {}
@EntityGraph("Member.all")
@Query("select m from Member m")
List<Member> findMemberEntityGraph();
간단한 fetch join이면 EntityGraph를 사용하고
아니면 JPQL로 직접짠다.
+ @EntityGraph는 left outer join만 지원한다.
그래서 다른 방식이 필요하면 JPQL을 작성하고 fetch join을 직접 사용해야 한다.
JPA Hint & Lock
JPA 쿼리 힌트(SQL 힌트가 아니라 JPA 구현체에게 제공하는 힌트)
쿼리 힌트 사용
@QueryHints(value = @QueryHint(name = "org.hibernate.readOnly", value = "true"))
Member findReadOnlyByUsername(String username);
조회를 하면 변경감지를 위해 영속성컨텍스트에서 관리를 한다.
변경을 하지않고 조회로만 사용할거라면 readOnly를 true로 설정한다.
Hibernate는 조회된 엔티티를 읽기 전용 모드로 관리 , 이는 엔티티가 변경될 수 없음을 의미
조회된 엔티티는 여전히 영속성 컨텍스트에서 관리되지만, 변경 추적이 비활성
즉, 엔티티의 속성을 변경하더라도 변경 사항이 데이터베이스에 반영되지 않는다.
이런 기능을 추가하더라도 성능 향상은 미미하다.
쿼리 자체를 개선하는게 성능 향상에 더 도움이된다.
아주 중요한 쿼리같은경우에만 추가하는것을 고려한다.
쿼리 힌트 사용 확인
@Test
public void queryHint() throws Exception {
//given
memberRepository.save(new Member("member1", 10));
em.flush();
em.clear();
//when
Member member = memberRepository.findReadOnlyByUsername("member1");
member.setUsername("member2");
em.flush(); //Update Query 실행X
}
쿼리 힌트 Page 추가 예제
@QueryHints(value = { @QueryHint(name = "org.hibernate.readOnly",
value = "true")},
forCounting = true)
Page<Member> findByUsername(String name, Pageable pageable);
org.springframework.data.jpa.repository.QueryHints 어노테이션을 사용
forCounting : 반환 타입으로 Page 인터페이스를 적용하면 추가로 호출하는 페이징을 위한 count 쿼리도 쿼리 힌트 적용
(기본값 true )
Lock
@Lock(LockModeType.PESSIMISTIC_WRITE)
List<Member> findByUsername(String name);
org.springframework.data.jpa.repository.Lock 어노테이션을 사용
JPA가 제공하는 락은 JPA 책 16.1 트랜잭션과 락 절을 참고
'인프런 > 실전! 스프링 데이터 JPA' 카테고리의 다른 글
끝) [미완] 나머지 기능들 - Specifications(명세),Query By Example,Projections,네이티브 쿼리 (0) | 2024.06.14 |
---|---|
5) 스프링 데이터 JPA 구현체 분석,merge,새로운 엔티티 구별방법 (0) | 2024.06.14 |
4) WEB 확장기능 - 도메인 클래스 컨버터, 페이징과 정렬 (0) | 2024.06.13 |
3) 확장 기능 - 사용자 정의 리포지토리 구현, Auditing (0) | 2024.06.12 |
1) 예제 도메인 모델, 공통 인터페이스 기능 소개 (0) | 2024.06.11 |
댓글