쿼리 메소드 기능 2

LeeKyoungChang·2022년 4월 27일
0
post-thumbnail

실전! 스프링 데이터 JPA 수업을 듣고 정리한 내용입니다.

 

📚 6. 순수 JPA 페이징과 정렬

✏️ 페이징, 정렬 조건 예시

  • 검색 조건 : 나이 10살
  • 정렬 조건 : 이름으로 내림차순
  • 페이징 조건 : 첫 번째 페이지, 페이지당 보여줄 데이터는 3건

 

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();
    }
  • totalCount : 내 페이지가 몇 페이지인지 조회할 때 사용한다.

 

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);
    }
  • offset 입력이 0이므로 실행 결과에 offset이 보이지 않는다.

 

실행 결과

스크린샷 2022-04-26 오후 3 35 41

 

📚 7. 스프링 데이터 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 쿼리 없이 결과만 반환

 

✔️ 페이징과 정렬 사용 예제

Page<Member> findByUsername(String name, Pageable pageable); //count 쿼리 사용
Slice<Member> findByUsername(String name, Pageable pageable); //count 쿼리 사용 X
List<Member> findByUsername(String name, Pageable pageable); //count 쿼리 사용 X
List<Member> findByUsername(String name, Sort sort);

 

📖 A. 페이징과 정렬을 사용하는 예제

✏️ 조건

  • 검색 조건 : 나이가 10살
  • 정렬 조건 : 이름으로 내림차순
  • 페이징 조건 : 첫 번째 페이지, 페이지당 보여줄 데이터는 3건

 

✔️ Page 사용 예제

MemberRepository에 추가 - Page 사용 예제 코드

public interface MemberRepository extends Repository<Member, Long> {
	Page<Member> findByAge(int age, Pageable pageable);
}

 

MemberRepositoryTest에 추가 - Page 사용 예제 실행 코드

    //페이징 조건과 정렬 조건 설정
    @Test
    public void paging() 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 pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "username"));
		Page<Member> page = memberRepository.findByAge(10, 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(); // 다음 페이지가 있는가?
    }
  • 두 번째 파라미터로 받은 Pagable 은 인터페이스다. 따라서 실제 사용할 때는 해당 인터페이스를 구현한 org.springframework.data.domain.PageRequest 객체를 사용한다.
  • PageRequest 생성자의 첫 번째 파라미터에는 현재 페이지를, 두 번째 파라미터에는 조회할 데이터 수를 입력한다.
    • 여기에 추가로 정렬 정보도 파라미터로 사용할 수 있다.

 

💡 참고
Page는 0부터 시작이다.

 

실행 결과
스크린샷 2022-04-26 오후 4 10 21
스크린샷 2022-04-26 오후 4 10 30

 

✔️ Slice 사용 예제

MemberRepository에 추가 - Slice 사용 예제 코드

public interface MemberRepository extends Repository<Member, Long> {
	Slice<Member> findByAge(int age, Pageable pageable);
}

 

MemberRepositoryTest에 추가 - Slice 사용 예제 실행 코드

    @Test
    public void paging() 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;
        PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "username"));

        // when
        Slice<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();
    }

 

실행 결과
스크린샷 2022-04-26 오후 4 16 29

  • limit + 1을 조회한다.
  • 다음 페이지 여부를 확인한다.
  • totalCount를 조회할 수 없다.

 

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); // 변환기
}

 

✔️ count 쿼리를 분리할 수 있다.

  • 복잡한 sql에서 사용한다.
  • 데이터는 left join, 카운트는 left join 하지 않아도 된다.
  • 실무에서 많이 사용된다.
public interface MemberRepository extends Repository<Member, Long> {
    @Query(value = "select m from Member m left join m.team t",
            countQuery = "select count(m) from Member m")
    Page<Member> findByAge(int age, Pageable pageable);
}

 

테스트 코드

    @Test
    public void paging() 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;
        PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "username"));

        // when
        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();
    }

 

실행 결과
스크린샷 2022-04-26 오후 4 54 14
스크린샷 2022-04-26 오후 4 54 37

  • 두 번째 select문에 left join이 없다.

 

💡 참고
전체 count 쿼리는 매우 무겁다.

 

✔️ DTO로 쉽게 변환하는 방법

페이지를 유지하면서 엔티티를 DTO로 변환하기

@Test  
public void paging() 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 pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "username"));  
    Page<Member> page = memberRepository.findByAge(10, pageRequest);  
  
    Page<MemberDto> toMap = page.map(member -> new MemberDto(member.getId(), member.getUsername(), null));  
    // map에서 결과를 얻을 수 있다.  
    // MemberDto로 바꼈다.  
    // 이를 API로 반환해도 된다.  
    // 내부가 엔티티가 아닌, MemberDto로 변환되었기 때문이다.  
  
  
    // 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(); // 다음 페이지가 있는가?  
}

Page<MemberDto> toMap = page.map(member -> new MemberDto(member.getId(), member.getUsername(), null));

  • map에서 결과를 얻을 수 있다.
  • MemberDto로 바꼈다.
  • 이를 API로 반환해도 된다. 내부가 엔티티가 아닌, MemberDto로 변환되었기 때문이다.

 

📚 8. 벌크성 수정 쿼리

📖 A. 순수 JPA 이용

MemberJpaRepository에 추가 - 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();
    }

 

MemberJpaRepositoryTest - JPA를 사용한 벌크성 수정 쿼리 테스트

    @Test
    public void bulkUpdate() {
        // given
        memberJpaRepository.save(new Member("member1", 10));
        memberJpaRepository.save(new Member("member2", 19));
        memberJpaRepository.save(new Member("member3", 20));
        memberJpaRepository.save(new Member("member4", 21));
        memberJpaRepository.save(new Member("member5", 40));

        // when
        int resultCount = memberJpaRepository.bulkAgePlus(20);

        // then
        assertThat(resultCount).isEqualTo(3);
    }

 

실행 결과

스크린샷 2022-04-26 오후 5 19 01 스크린샷 2022-04-26 오후 5 44 04
  • 20일 이상인 사람만 age + 1 한다.

 

📖 B. 스프링 데이터 JPA

MemberRepository - 스프링 데이터 JPA를 사용한 벌크성 수정 쿼리

@Modifying
@Query("update Member m set m.age = m.age + 1 where m.age >= :age")
int bulkAgePlus(@Param("age") int age);

 

MemberRepositoryTest - 스프링 데이터 JPA를 사용한 벌크성 수정 쿼리 테스트

    @Test
    public void bulkUpdate() {
        // 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);
    }

 

실행 결과
스크린샷 2022-04-26 오후 5 22 29

  • 벌크성 수정, 삭제 쿼리는 @Modifying 어노테이션을 사용한다.
    • 사용하지 않으면 다음 예외 발생
    • org.hibernate.hql.internal.QueryExecutionRequestException: Not supported for DML operations

 

✔️ 벌크 연산에서 주의할 점

    @Test
    public void bulkUpdate() {
        // 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);

        List<Member> result = memberRepository.findByUsername("member5");
        Member member = result.get(0);
        System.out.println("member = " + member);

        // then
        assertThat(resultCount).isEqualTo(3);
    }
스크린샷 2022-04-26 오후 5 25 36
  • age=40 으로 나온다.
    • 회원을 findById로 다시 조회하면 영속성 컨텍스트에 과거 값이 남아서 문제가 될 수 있다. 다시 조회해야한다면, 영속성 컨텍스트를 초기화해야 한다!
  • 벌크 연산에서 조심해야할 점이다.
  • 벌크연산일 때는 영속성 컨텍스트를 무시하고 바로 db에 반영하기에, 영속성 컨텍스트에서는 변경 사항에 대해 모른다.
    • 그러므로, 벌크 연산 이후에는 영속성 컨텍스트를 다 날려버려야 한다. (영속성 컨텍스트 초기화)

 

그러므로, 벌크 연산을 사용할 때,
@Modifying(clearAutomatically = true)를 추가시 (em.flush() + em.clear()와 같은 작업을 한다.)
db에서 쿼리가 나가고 나면 자동으로 영속성 컨텍스트 clear를 실행해준다.

스크린샷 2022-04-26 오후 5 33 01

 

실행 결과
스크린샷 2022-04-26 오후 5 32 08

 

💡 참고

  • @Modifying(clearAutomatically = true)의 기본 값은 false
  • 벌크 연산은 영속성 컨텍스트를 무시하고 실행하기 때문에, 영속성 컨텍스트에 있는 엔티티의 상태와 DB에 엔티티 상태가 달라질 수 있다.
  • 권장하는 방안
    • 영속성 컨텍스트에 엔티티가 없는 상태에서 벌크 연산을 먼저 실행한다.
    • 부득이하게 영속성 컨텍스트에 엔티티가 있으면 벌크 연산 직후 영속성 컨텍스트를 초기화 한다.

 

📚 9. @EntityGraph

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

✔️ N + 1 문제
member → team은 지연로딩 관계이다. 따라서 다음과 같이 team의 데이터를 조회할 때 마다 쿼리가 실행된다. (N+1 문제 발생)

    @Test
    public void findMemberLazy() {
        // given
        // member1 -> teamA
        // member2 -> teamB

        Team teamA = new Team("teamA");
        Team teamB = new Team("teamB");
        teamRepository.save(teamA);
        teamRepository.save(teamB);
        Member member1 = new Member("member1", 10, teamA);
        Member member2 = new Member("member2", 10, teamB);

        memberRepository.save(member1);
        memberRepository.save(member2);

        em.flush();
        em.clear();

        // when
        List<Member> members = memberRepository.findAll(); // member만 db에서 끌어온다.
        // 현재 team은 프록시로 가져온다. (지연 로딩)

        for (Member member : members) {
            System.out.println("member = " + member.getUsername());
            System.out.println("member.teamClass = " + member.getTeam().getClass());
            System.out.println("member.team = " + member.getTeam().getName());
            // getTeam()에서 getName()을 호출할 때, Team의 select문이 실행된다. 진짜 team을 가져온다.
        }
    }

 

실행 결과

스크린샷 2022-04-26 오후 6 17 21 스크린샷 2022-04-26 오후 6 17 50

연관된 엔티티를 한 번에 조회하려면 페치 조인이 필요하다.

 

💡 참고
지연 로딩 여부 확인하는 방법

// Hibernate 기능으로 확인
Hibernate.isInitialized(member.getTeam())

// JPA 표준 방법으로 확인
PersistenceUnitUtil util = em.getEntityManagerFactory().getPersistenceUnitUtil();
util.isLoaded(member.getTeam());

 

📖 A. JPQL 페치 조인

MemberRepository에 추가

    @Query("select m from Member m left join fetch m.team")
    List<Member> findMemberFetchJoin();

 

MemberRepositoryTest에 추가

    @Test
    public void findMemberLazy() {
        // given
        // member1 -> teamA
        // member2 -> teamB

        Team teamA = new Team("teamA");
        Team teamB = new Team("teamB");
        teamRepository.save(teamA);
        teamRepository.save(teamB);
        Member member1 = new Member("member1", 10, teamA);
        Member member2 = new Member("member2", 10, teamB);

        memberRepository.save(member1);
        memberRepository.save(member2);

        em.flush();
        em.clear();

        // when
        List<Member> members = memberRepository.findMemberFetchJoin();
        // member와 team을 한번에 다 가져온다.

        for (Member member : members) {
            System.out.println("member = " + member.getUsername());
            System.out.println("member.teamClass = " + member.getTeam().getClass());
            System.out.println("member.team = " + member.getTeam().getName());
        }
    }

 

실행 결과
스크린샷 2022-04-26 오후 6 21 20

  • 연관된 엔티티를 한 번에 join하여 다 조회한다.
  • join하여 select절에 연관된 데이터를 다 넣어준다.

 

📖 B. @EntityGraph

  • 스프링 데이터 JPA는 JPA가 제공하는 엔티티 그래프 기능을 사용하면 JPQL 없이 페치 조인을 사용할 수 있다. (JPQL + 엔티티 그래프도 가능하다.)
  • 곧 페치 조인이다. (JPA가 제공하는 기능이다.)

 

MemberRepository에 추가

// 공통 메서드 오버라이드
@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> findEntityGraphByUsername(@Param("username") String username);

 

MemberRepositoryTest에 추가

    @Test
    public void findMemberLazy() {
        // given
        // member1 -> teamA
        // member2 -> teamB

        Team teamA = new Team("teamA");
        Team teamB = new Team("teamB");
        teamRepository.save(teamA);
        teamRepository.save(teamB);
        Member member1 = new Member("member1", 10, teamA);
        Member member2 = new Member("member2", 10, teamB);

        memberRepository.save(member1);
        memberRepository.save(member2);

        em.flush();
        em.clear();

        // when
        List<Member> members = memberRepository.findEntityGraphByUsername("member1");
        // member와 team을 한번에 다 가져온다.

        for (Member member : members) {
            System.out.println("member = " + member.getUsername());
            System.out.println("member.teamClass = " + member.getTeam().getClass());
            System.out.println("member.team = " + member.getTeam().getName());
        }
    }

 

실행 결과
스크린샷 2022-04-26 오후 6 55 18

 

📌 EntityGraph 정리

  • 사실상 페치 조인(FETCH JOIN)의 간편 버전
  • LEFT OUTER JOIN 사용

 

💡 참고
연관된 엔티티를 한 번에 조회할 때

  • 쿼리가 복잡할 때는 페치 조인을 사용한다.
  • 쿼리가 간단할 때는 @EntityGraph를 사용한다.

 

📖 C. NamedEntityGraph 사용 방법

NamedEntityGraph는 거의 사용하지 않는다.

@NamedEntityGraph(name = "Member.all", attributeNodes = @NamedAttributeNode("team"))
@Entity
public class Member {}
@EntityGraph("Member.all")
@Query("select m from Member m")
List<Member> findMemberEntityGraph();

 

📚 10. JPA Hint & Lock

JPA Hint : JPA 쿼리 힌트(SQL 힌트가 아니라 JPA 구현체에게 제공하는 힌트)

 

📖 A. QueryHint

✔️ QueryHint을 적용하지 않았을 때
MemberRepositoryTest에 추가

    @Test
    public void queryHint() {
        // given

        Member member1 = memberRepository.save(new Member("member1", 10));
        memberRepository.save(member1);
        em.flush();
        em.clear();


        // when
        Member findMember = memberRepository.findById(member1.getId()).get();
        findMember.setUsername("member2");

        em.flush();
    }

 

실행 결과

스크린샷 2022-04-27 오후 1 06 58
  • 변경 감지 체크를 한다.

 

✔️ QueryHint을 적용
MemberRepository에 추가

@QueryHints(value = @QueryHint(name = "org.hibernate.readOnly", value = "true"))
Member findReadOnlyByUsername(String username);
  • 쿼리를 읽기 전용으로 조회한다고 힌트를 준다.
  • org.springframework.data.jpa.repository.QueryHints 어노테이션을 사용
  • forCounting : 반환 타입으로 Page 인터페이스를 적용하면 추가로 호출하는 페이징을 위한 count 쿼리도 쿼리 힌트 적용(기본값 true )

 

MemberRepositoryTest에 추가

    @Test
    public void queryHint() {
        // given

        Member member1 = memberRepository.save(new Member("member1", 10));
        memberRepository.save(member1);
        em.flush();
        em.clear();


        // when
        Member findMember = memberRepository.findReadOnlyByUsername("member1");
        findMember.setUsername("member2");

        em.flush();
    }

 

실행 결과

스크린샷 2022-04-27 오후 1 14 11
  • readOnly는 변경감지를 체크하지 않는다.

 

📖 B. Lock

MemberRepository에 추가

    @Lock(LockModeType.PESSIMISTIC_WRITE)
    List<Member> findLockByUsername(String username);
  • org.springframework.data.jpa.repository.Lock 어노테이션을 사용
  • 실시간 트래픽이 많은 곳에서는 Lock을 걸면 안된다.

 

MemberRepositoryTest에 추가

    @Test
    public void lock() {
        // given

        Member member1 = memberRepository.save(new Member("member1", 10));
        memberRepository.save(member1);
        em.flush();
        em.clear();


        // when
        List<Member> result = memberRepository.findLockByUsername("member1");

    }

 

실행 결과

스크린샷 2022-04-27 오후 1 22 57

 

profile
"야, (오류 만났어?) 너두 (해결) 할 수 있어"

0개의 댓글