파라미터 바인딩
파라미터 바인딩에는 위치 기반과 이름 기반이 있다. 이름기반을 사용하자!! (유지보수와 가독성이 좋음)
@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("select m from Member m where m.username in :names")
List<Member> findByNames(@Param("names") List<String> names);
반환값 & 페이징과 정렬
List<Member> findMemlistByUsername(String name);
Member findOneByUsername(String name);
Optional<Member> findOptionalByUsername(String name);
스프링 데이터 레포지토리에 추가
@Test
public void returnType() {
Member member1 = new Member("M11",10);
Member member2 = new Member("M11",20);
Member member3 = new Member("AAA",12);
memberRepository.save(member1);
memberRepository.save(member2);
memberRepository.save(member3);
List<Member> memList = memberRepository.findMemlistByUsername("M11");
Optioanl<Member> opMem = memberRepository.findOptionalByUsername("AAA");
}
반환값이 없을 수 있기 때문에 Optional로 감싸주는 것이 좋다. Optional로 찾아오는 sql 쿼리의 결과가 2개 이상이면 에러가 발생할 것이다.
👉 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();
}
@Test
public void test() {
Member member1 = new Member("M11",10);
Member member2 = new Member("M12",10);
Member member3 = new Member("AAA",10);
Member member4 = new Member("ABBAA",10);
Member member5 = new Member("a",10);
memberRepository.save(member1);
memberRepository.save(member2);
memberRepository.save(member3);
memberRepository.save(member4);
memberRepository.save(member5);
List<Member> byPage = memberJpaRepo.findByPage(10, 0, 3);
assertThat(byPage.size()).isEqualTo(3);
}
👉 스프링 데이터 jpa에서의 페이징
//레포지토리 코드 추가
Page<Member> findByAge(int age, Pageable pageable);
@Test
public void pagingTest(){
Member member1 = new Member("M11",10);
Member member2 = new Member("M12",10);
Member member3 = new Member("AAA",10);
Member member4 = new Member("ABBAA",10);
Member member5 = new Member("a",10);
memberRepository.save(member1);
memberRepository.save(member2);
memberRepository.save(member3);
memberRepository.save(member4);
memberRepository.save(member5);
PageRequest pageReq = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "username"));
//when
Page<Member> page = memberRepository.findByAge(10, pageReq);
//then
List<Member> content = page.getContent();
long totalElements = page.getTotalElements();
}
테스트는 이렇게 구성할 수 있다. 한가지 재밌는 점은
페이지 계산을 위해 count 쿼리도 나간다는 것이다. 스프링 데이터 jpa 가 제공하는 페이징에는 부가적인 정보들이 많이 있다. 이를 확인해보기 위해 위의 테스트 코드 아랫부분에 다음의 코드를 추가한다.
List<Member> content = page.getContent();
long totalElements = page.getTotalElements();
assertThat(content.size()).isEqualTo(3);
assertThat(page.getTotalElements()).isEqualTo(5);
assertThat(page.getNumber()).isEqualTo(0); //페이지 번호
assertThat(page.getTotalPages()).isEqualTo(2); //총 페이지 번호
assertThat(page.isFirst()).isEqualTo(true);
assertThat(page.hasNext()).isEqualTo(true);
Page클래스를 들어가보면 Slice 클래스를 상속하고 있음을 알 수 있다. Slice 클래스에는 getTotalElements, getTotalPage 등 페이지에 관한 기능이 없다. 그리고 한가지 재밌는 점은 Page클래스를 사용하면 limit 쿼리가 지정한 값만큼 그대로 나간다!
하지만 코드를 다음과 같이 바꾸면
//레포지토리에서 반환값을 Page -> Slice로 변경
Slice<Member> findByAge(int age, Pageable pageable);
//테스트 코드도 그에 맞게 반환 타입을 Slice로 변경
Slice<Member> page = memberRepository.findByAge(10, pageReq);
List<Member> content = page.getContent();
// long totalElements = page.getTotalElements();
assertThat(content.size()).isEqualTo(3);
// assertThat(page.getTotalElements()).isEqualTo(5);
assertThat(page.getNumber()).isEqualTo(0); //페이지 번호
// assertThat(page.getTotalPages()).isEqualTo(2); //총 페이지 번호
assertThat(page.isFirst()).isEqualTo(true);
assertThat(page.hasNext()).isEqualTo(true);
다음 페이지가 있는지 확인하기 위해 한개 더 많이 조회해온다.
🧐 count 쿼리 성능 관련
total count는 데이터가 많고 조인이 복잡할수록 성능이 안나올 수 있다. 그래서 count쿼리는 따로 지정할 수 있다.
@Query(value="select m from Member m left join m.team t")
Page<Member> findByAge(int age, Pageable pageable);
만약 이렇게 되어있다면 select쿼리랑 Page 클래스가 하는 count 쿼리 두개 모두 위의 left join을 하는 쿼리가 나갈 것이다.
실무에서 만약 여러 테이블을 join하는 상황으로 이어진다면 성능이 많이 안좋아질 수 있다. 그래서 아래와 같이 count 쿼리는 따로 지정할 수 있다.
@Query(value="select m from Member m left join m.team t", countQuery = "select count(m.username) from Member m")
Page<Member> findByAge(int age, Pageable pageable);
count 쿼리가 많이 단순해졌다!!
😎 page는 추가로 map 함수를 제공하는데 엔티티를 DTO로 변환할때 사용하면 유용하다.
//테스트 코드
Page<Member> page = memberRepository.findByAge(10, pageReq);
Page<MemberDto> map = page.map(m -> new MemberDto(m.getId(), m.getUsername(), null));
정리
반환타입은 3가지
- Page : 추가 count 쿼리 결과를 포함하는 페이징
- Slice : 추가 count 쿼리 없음. 다음 페이지 유무만 확인가능 (limit + 1 조회)
- List : 일반적인 컬렉션
페이징의 인덱스는 0부터이다
불크 연산
jpa에서의 불크연산
public int bulkUpdateAge(int age){
return em.createQuery("update Member m set m.age = m.age+1 where m.age >= :age")
.setParameter("age", age).executeUpdate();
}
이전에 배웠던 jpa에서의 불크연산은 이렇게 했었다.
스프링 데이터 jpa의 불크연산
@Modifying //이게 있어야 update 실행
@Query("update Member m set m.age = m.age+1 where m.age >= :age")
int bulkUpdate(@Param("int") int age);
- bulk 연산은 1차캐시에 반영되지 않으므로 영속성 컨텍스트를 초기화해야한다.
- em.flush는 하지 않아도 된다. (JPQL 직전에 flush)
- em.clear 만 해주면 되는데 스프링데이터 jpa는 @Modifying(clearAutomatically=true)
@Modifying(clearAutomatically=true)
@Query("update Member m set m.age = m.age+1 where m.age >= :age")
int bulkUpdate(@Param("age") int age);
결론은 위와같이 쓰면 된다.
페치조인
스프링 데이터 jpa에서는 페치조인을 쓰려면 레포지토리에 @Query로 넣어주면 된다.
@Query("select m from Member m left join fetch m.team")
List<Member> findMemberFetch();
복잡한 쿼리는 이렇게 하면 되지만 간단한 쿼리까지 매번 이렇게 하는 것은 번거로우므로 스프링 데이터 jpa에서는 더 나은 방법을 제공한다.
👉 메서드 이름으로 해결 + 페치조인
@Override
@EntityGraph(attributePaths = {"team"})
List<Member> findAll();
@EntityGraph(attributePaths = {"team"})
@Query("select m from Member m")
List<Member> findAll2();
@EntityGraph(attributePaths = {"team"})
List<Member> findByUsername(@Param("username") String username);
엔티티 그래프 어노테이션을 사용하면 메서드 이름으로 쿼리를 만들어내면서 페치조인을 할 수 있다. 이와 같은 방법은 복잡한 쿼리에는 적합하지 않고 그냥 간단한 쿼리의 경우에 사용하는 것이다.
JPA Hint 와 Lock
👉 Hint
jpa로 select 쿼리를 날려서 가져오면 영속성 컨텍스트에는 항상 더티 체킹을 위한 객체를 따로 만들어둔다. 복사본을 만드는 건데 만약 읽기전용으로만 가져올 것이라면 이는 비효율적일 수 있다. 그런 경우에 사용한다!
@QueryHints(value = @QueryHint(name="org.hibernate.readOnly", value ="true"))
Member findReadOnlyByUsername(String name);
이렇게 하면 읽기전용으로 가져와서 변경이 일어나도 update문이 나가지 않는다.
정리 : 이 방법은 사실 크게 성능이 좋아지진 않아서 매번 이렇게 할 필요는 없다. 그냥 엄청 트래픽 큰 api정도만 하자.
👉 Lock
@Lock(LockModeType.PESSIMISTIC_WRITE)
List<Member> findLockByUsername(String n);
실시간 트래픽이 많은 서비스는 락을 걸면 안된다.
실시간 트래픽보다는 돈을 맞춰야하는 건 위의 락을 걸면 좋은 방법일 수 있다.
관련글
'백엔드 > JPA' 카테고리의 다른 글
스프링 데이터 jpa - 구현체 코드 구경, 쿼리 dsl (0) | 2021.08.23 |
---|---|
스프링 데이터 JPA - 사용자 정의 클래스, Auditing, MVC 페이징 (0) | 2021.08.23 |
스프링 데이터 JPA 기초 (0) | 2021.08.22 |
JPQL 페치조인의 한계, 다양한 쿼리 (0) | 2021.08.09 |
JPQL 중급문법 (0) | 2021.08.08 |