본문 바로가기
JPA

실전! 스프링 데이터 JPA - 5. 쿼리 메소드 기능

by 문자메일 2024. 9. 29.

 

 순수 JPA 페이징과 정렬

 

JPA에서 페이징을 어떻게 할 것인가?

 

다음 조건으로 페이징과 정렬을 사용하는 예제 코드를 보자

 

검색 조건 : 나이가 10살

정렬 조건 : 이름으로 내림차순

페이징 조건 : 첫 번째 페이지, 페이지당 보여줄 데이터는 3건

 

아래는 순수 JPA 페이징으로 정렬

 

MemberJpaRepository.java

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

 

 

 

MemberJpaRepositoryTest.java

@Test
public void paging(){
    //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 = 1;
    int limit = 3;

    //when
    List<Member> members = memberJpaRepository.findByPage(age, offset, limit);
    long totalCount = memberJpaRepository.totalCount(age);

    // 페이지 계산 공식 적용
    // totalPage = totalCount / size ...
    // 마지막 페이지 ...
    // 최초 페이지 ...

    Assertions.assertThat(members.size()).isEqualTo(3);
    Assertions.assertThat(totalCount).isEqualTo(5);

}

 

스프링 데이터 JPA에 최초 페이지, 마지막 페이지 등 페이지 계산 공식 같은 것 이미 기능으로 다 제공하고 있다.

 

 

 

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

 

 

 

아래처럼 @Query 사용하여 페이징 쿼리와 카운팅 쿼리 분리할 수 있다.

MemberRepository.java

@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);

 

 

MemberRepositoryTest.java

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

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

    // then
    List<Member> content = page.getContent();
    long totalElements = page.getTotalElements();

    Assertions.assertThat(content.size()).isEqualTo(3);
    Assertions.assertThat(page.getTotalElements()).isEqualTo(5);
    Assertions.assertThat(page.getNumber()).isEqualTo(0);
    Assertions.assertThat(page.getTotalPages()).isEqualTo(2);
    Assertions.assertThat(page.isFirst()).isTrue();
    Assertions.assertThat(page.hasNext()).isTrue();

}

 

 

 

벌크성 수정 쿼리

 

순수 JPA의 벌크성 수정 쿼리

 

MemberJpaRepository.java

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;
}

 

MemberJpaRepositoryTest.java

@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
    Assertions.assertThat(resultCount).isEqualTo(3);
    
}

 

 

 

스프링 Data Jpa 레퍼지토리

 

JPA에서 수정하는 쿼리의 경우 @Modifying(clearAutomatically = true) 어노테이션이 필수이다.

그래야 순수 JPA에서 .executeUpdate() 메서드가 실행되게 된다.

 

아래는 JPA에서 벌크 업데이트 로직 수행할 때 주의사항이다.

List<Member> result = memberRepository.findByUsername("member5");
Member member5 = result.get(0);
// 벌크 update 연산은 영속성 컨텍스트 안 거치고 바로 DB에 반영하기에, 컨텍스트에는 40살, DB에는 41살로 되어 있다.
// 따라서 위에 em.flush, em.clear 하여 JPA 영속성 컨텍스트에 있는 캐시된 데이터들을 날리고, DB에서 읽어오도록 해야 한다.
// 아니면 @Modifying(clearAutomatically = true) 설정해도 된다.
System.out.println("member5 = " + member5);

 

 

MemberRepository.java

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

 

 

MemberRepositoryTest.java

    @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);
        em.flush();
        em.clear();

//        List<Member> result = memberRepository.findByUsername("member5");
//        Member member5 = result.get(0);
//        // 벌크 update 연산은 영속성 컨텍스트 안 거치고 바로 DB에 반영하기에, 컨텍스트에는 40살, DB에는 41살로 되어 있다.
//        // 따라서 위에 em.flush, em.clear 하여 JPA 영속성 컨텍스트에 있는 캐시된 데이터들을 날리고, DB에서 읽어오도록 해야 한다.
//        // 아니면 @Modifying(clearAutomatically = true) 설정해도 된다.
//        System.out.println("member5 = " + member5);
        // then
        Assertions.assertThat(resultCount).isEqualTo(3);

    }

 

 

 

 

@EntityGraph

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

 

JPA N+1 문제를 해결하기 위한 방법으로 JPQL에서 fetch join이 있다.

이 방법은 jpql을 직접 작성하는 경우에 사용할 수 있고,

jpql을 작성하지 않는 경우는 아래처럼 @EntityGraph(attributePaths = {"필드명"}) 으로 가짜(프록시) 객체가 아니라 연관된 엔티티를 바로 가져올 수 있다.

 

MemberRepository.java

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

@Override
@EntityGraph(attributePaths = {"team"})
List<Member> findAll();

@EntityGraph(attributePaths = {"team"})
@Query("select m from Member m")
List<Member> findMemberEntityGraph();

@EntityGraph(attributePaths = {"team"})
List<Member> findEntityGraphByUsername(@Param("username") String username);

 

Member.java

public class Member {

    @Id @GeneratedValue
    @Column(name = "member_id")
    private Long id;
    private String username;
    private int age;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "team_id")
    private Team team;

 

 

 

JPA Hint & Lock

 

JPA 영속성 컨텍스트에 엔티티를 가져오면 추후 수정 시 변경 감지를 위하여 복사본이 생성되는데, 그 엔티티의 값을 수정할 필요가 없는 경우는 복사본 생성은 불필요한 리소스 낭비가 된다.

따라서 그런 readOnly 쿼리의 경우 아래처럼 값을 주면 엔티티의 값을 복사하지 않아서 나름의 최적화를 할 수 있다.

@QueryHints(value = @QueryHint(name = "org.hibernate.readOnly", value="true"))

 

하지만 성능에 매우 큰 영향을 주는 요소는 아니여서, 모든 readOnly 쿼리 메서드에 일일이 붙일 필요는 없다고 한다.

 

 

MemberRepository.java

@QueryHints(value = @QueryHint(name = "org.hibernate.readOnly", value="true"))
Member findReadOnlyByUsername(String username);

@Lock(LockModeType.PESSIMISTIC_WRITE)
List<Member> findLockByUsername(String username);

 

 

MemberRepositoryTest.java

@Test
public void queryHint(){
    // given
    Member member1 = new Member("member1", 10);
    memberRepository.save(member1);
    em.flush();
    em.clear();

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

    em.flush();
}

@Test
public void lock(){
    // given
    Member member1 = new Member("member1", 10);
    memberRepository.save(member1);
    em.flush();
    em.clear();

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

 

 

 

댓글