Web/JPA

[Data JPA] EP 2. 쿼리 메소드 기능

목차 (클릭시 해당 목차로 이동)


     

     

    메소드 이름으로 쿼리 생성

     

    Data JPA에서 공통적으로 제공하는 메소드 이외에도 필요한 메소드가 있을 수 있습니다.

     

    예를들어 "어떤 이름을 가진 회원중 나이가 15 이상인 회원을 구해라" 같은 쿼리는 엔티티에 의존한 쿼리입니다.

     

    이럴 때도 Data JPA를 이용할 수 있습니다.

     

    메소드 이름으로 쿼리를 생성할 수 있습니다.

     

     

     

    이렇게 MemberRepository 인터페이스에서 해당 쿼리 이름으로 메소드를 선언만 해도 동작합니다.

     

     

     

    하지만 이 방법을 사용하려면 Data JPA에서 정하는 메소드 이름 프로퍼티를 정확하게 맞추어 사용해야합니다.

     

    Spring Data JPA에서 메소드 이름에 대한 규칙은 공식문서에서 확인이 가능합니다.

    https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa.query-methods.query-creation

     

    Spring Data JPA - Reference Documentation

    Example 109. Using @Transactional at query methods @Transactional(readOnly = true) interface UserRepository extends JpaRepository { List findByLastname(String lastname); @Modifying @Transactional @Query("delete from User u where u.active = false") void del

    docs.spring.io

     

     

    기본적인 규칙은 아래와 같습니다.

    • 조회 : 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
      (...에 distinct 할 것을 넣어준다.)
    • LIMIT : findFirst3, findFirst, findTop, findTop3
      (뜻 : 위에서 ~ 3개)

     

     

    주의할 점

    엔티티의 필드명이 변경되면 인터페이스에 정의한 메소드의 이름도 함께 변경해야합니다.

    (애플리케이션 시작하는 시점에 오류가 발생을 인지할 수 있다.)

     

     

     

    단점

    1. 메소드 이름이 길어짐
    2. 복잡한 jpql못씀
    3. 애플리케이션 로딩시점에 sql문법오류 탐지 불가

     

     

     

     

     

    @Query (리포지토리 메소드에 쿼리 정의하기)

     

     

    위 단점들을 모두 해결한 기능이 있습니다.

     

    사용방법

     

    1. @Query 안에 jpql을 직접 적습니다.
    2. jpql안에서 쓰는 파라미터는 메소드에서 @Param을 이용해서 받습니다.

     

    package study.datajpa.repository;
    
    import org.springframework.data.jpa.repository.JpaRepository;
    import org.springframework.data.jpa.repository.Query;
    import org.springframework.data.repository.query.Param;
    import study.datajpa.entity.Member;
    
    import java.util.List;
    
    public interface MemberRepository extends JpaRepository<Member, Long> {
        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);
    }
    
    

     

        @Test
        public void testQuery() throws Exception {
            Member member1 = new Member("A", 10);
            Member member2 = new Member("A", 20);
    
            memberRepository.save(member1);
            memberRepository.save(member2);
    
            List<Member> result = memberRepository.findUser("A", 10);
            assertThat(result.get(0)).isEqualTo(member1);
        }

     

     

    findByUsernameAndAge() vs findUser()

    메소드 이름을 직접 지정할 수 있어서 길어지지 않게 할 수 있습니다.

     

    직접 작성해야하는 jpql도 NamedQuery를 이용하지않고 간편하게 작성이 가능합니다.

     

     

    또한 애플리케이션 로딩 시점에 문법오류를 감지할 수 있습니다.

     

     

     

     

    @Query (DTO를 직접 조회하기)

     

    DTO를 직접 조회하려면 jpql의 new operation을 사용해야합니다.

     

    select 절에서 new dto경로(생성자주입)을 하면 됩니다.

     

     

        @Query("select new study.datajpa.dto.MemberDto(m.id, m.username, t.name) from Member m join m.team t")
        List<MemberDto> findMemberDto();
        @Test
        public void findMemberDto() throws Exception {
            Team team = new Team("teamA");
            teamRepository.save(team);
    
            Member member1 = new Member("A", 10);
            member1.setTeam(team);
            memberRepository.save(member1);
    
            List<MemberDto> memberDto = memberRepository.findMemberDto();
            for (MemberDto dto : memberDto) {
                System.out.println("dto = " + dto);
            }
    
        }

     

     

     

     

    파라미터 바인딩

     

    사용법

    1. jpql에서 파라미터로 받고싶은 부분에 :{파라미터이름} 을 하고,
    2. 매개변수를 받아올때 @Param("{파라미터이름}") 변수로 하면 됩니다.

     

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

     

     

     

    컬렉션 파라미터 바인딩

    in절에 컬렉션을 넣을 때 사용하는 기능입니다.

     

    예를들어 username이 컬렉션 안에 있는 이름인 엔티티를 반환하는 jpql을 짜보겠습니다.

     

        @Query("select m from Member m where m.username in :names")
        List<Member> findByNames(@Param("names") Collection<String> names);
        @Test
        public void findByNames() throws Exception {
            Member member1 = new Member("A", 10);
            Member member2 = new Member("B", 20);
    
            memberRepository.save(member1);
            memberRepository.save(member2);
    
            List<Member> result = memberRepository.findByNames(Arrays.asList("A", "B"));
            for (Member member : result) {
                System.out.println("member = " + member);
            }
        }

     

     

     

     

     

     

     

    반환타입

     

    spring Data JPA에서는 반환타입을 자유롭게 지정할 수 있습니다..

        List<Member> findListByUsername(String username); // 컬렉션
        Member findMemberByUsername(String username); // 단건
        Optional<Member> findOptionalByUsername(String username); // Optional

     

        @Test
        public void returnType() throws Exception {
            Member member1 = new Member("A", 10);
            Member member2 = new Member("B", 20);
    
            memberRepository.save(member1);
            memberRepository.save(member2);
    
            List<Member> a = memberRepository.findListByUsername("A");
            System.out.println("a = " + a);
    
            Member a1 = memberRepository.findMemberByUsername("A");
            System.out.println("a1 = " + a1);
    
            Optional<Member> a2 = memberRepository.findOptionalByUsername("A");
            System.out.println("a2 = " + a2);
    
            List<Member> result = memberRepository.findListByUsername("asdfg");
            System.out.println("result = " + result);
    
            Member findMember = memberRepository.findMemberByUsername("asdds");
            System.out.println("findMember = " + findMember);
    
        }

     

     

     

    예제코드 이외에도 여러 반환타입을 지정할 수 있습니다.

    공식문서에서 반환타입들을 확인할 수 있습니다.

    https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#repository-query-return-types

     

    Spring Data JPA - Reference Documentation

    Example 109. Using @Transactional at query methods @Transactional(readOnly = true) interface UserRepository extends JpaRepository { List findByLastname(String lastname); @Modifying @Transactional @Query("delete from User u where u.active = false") void del

    docs.spring.io

     

     

     

     

    주의할 점

     

    조회결과가 없을 때

    JPA와 달리 Data JPA에서는 조회결과가 없을 때 null을 반환합니다.

            List<Member> a = memberRepository.findListByUsername("ASD");
            System.out.println("a = " + a);
    
            Member a1 = memberRepository.findMemberByUsername("ASD");
            System.out.println("a1 = " + a1);
    
            Optional<Member> a2 = memberRepository.findOptionalByUsername("ASD");
            System.out.println("a2 = " + a2);

     

     

    그런데 실무에서는 데이터가 없을 가능성이 있으면 Optional을 쓰는 것이 좋습니다.

     

     

    단건조회시 여러개일때

     

    단건조회시 여러개일때 -> NonUniqueResultException이 터짐(JPA의 예외처리) -> spring data jpa가 spring framework의 IncorrectResultSizeDataAccessException으로 변환

     

     

     

    위와같이 jpa의 예외에서 spring의 예외로 변환되는 이유

    Repository의 기술은 jpa가 아닌 mongoDB 등 다른 기술일 수 있습니다. 그런데 repository를 사용하는 service 계층의 클라이언트 코드들은 이런 JPA하나에만 의존하는 것이 아니라 spring이 추상화한것에 의존하기 때문입니다.

     

     

     

     

     

     

    스프링 데이터 JPA 페이징과 정렬

     

    Page

     

    page의 index는 0부터 시작합니다.

     

    메서드만들기, 사용하기

    • 메서드를 만들때는 Pageable을 인자로 받습니다.
    • 인자를 넘겨줄때는 PageRequest를 세팅해서 넘겨주어야 합니다.
      PageRequest에 (시작page, limit, 정렬)을 세팅에 넘겨줄 수 있습니다.
    • 페이지처리를 하기위해 totalCount나 totalPages등이 필요할 수 있습니다. 페이지처리에 필요한 값들은 Page에서 제공되므로 메서드의 반환값을 Page로 합니다.

     

        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();
            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()).isTrue();
            assertThat(page.hasNext()).isTrue();
    
        }

     

     

    Slice

     

    메서드를 만들때 반환값을 Page로 해도 Slice로 받아집니다.

    Page가 Slice를 상속받고있어서 가능한데 이렇게 사용하면 안됩니다.

    Slice는 Slice로 받습니다.

     

     

     

    어느때에 사용?

    모바일같은 곳에서 페이지를 1,2,3 ... 이렇게 나누지 않고 그냥 10개가 나오고 더보기를 누르면 그 다음 10개가 나오고 이런식으로 반복되는 페이징을 볼 수 있습니다.

    이 경우에 Slice를 사용해서 눈속임을 하는 것입니다.

    처음에 +1로 한페이지의 데이터를 더 가져오고 더 가져온 것은 더보기 버튼에 숨깁니다. 그리고 더보기 버튼을 누르면 숨겨진 데이터를 보여주고 다음 +1을 더 가져와 더보기에 숨깁니다. 이런식으로 구현되는 것이 Slice입니다.

     

     

     

    특징

    • slice는 total관련된 것들을 가져오지 않습니다. (1,2,3,..같은 페이징처리를 하지 않기 때문입니다.)
    • limit의 +1을 더 가져옵니다.

     

    getTotalElements(), getTotalPages() 같은 값들을 가져올 수 없습니다.

        Slice<Member> findByAge(int age, Pageable pageable);
    
        @Test
        public void slicing() 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();
    
        }

     

     

     

     

    List

    List는 컨텐츠만 딱 가져옵니다.

    페이징 처리에 필요한 값들은 모두 가져오지 않습니다.

     

     

     

     

    주의점

    totalCount의 성능문제

    데이터 수가 많고, 만약 join을 한다면 totalCount를 가져오는 쿼리도 join을 하게됩니다.

    어차피 join을 하든 하지 않든 totalCount의 값은 같습니다. 하지만 join을 하게되면 성능저하가 일어납니다.

     

    그래서 Data JPA 에서는 countQuery를 따로 짤 수 있도록 합니다.

     

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

     

    Page를 Dto로 변환하기

     

    rest api를 만들때 Page를 절대로 클라이언트에 그대로 반환하면 안됩니다.

    Dto로 변환해 반환해야합니다.

     

    Page를 Dto로 쉽게 변환할 수 있습니다.

     

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

     

    'Web > JPA' 카테고리의 다른 글

    [Data JPA] EP 4. 확장 기능  (0) 2021.05.31
    [Data JPA] EP 3. 쿼리 메소드 기능 -2  (0) 2021.05.28
    [Data JPA] EP 1. 공통 인터페이스 기능  (0) 2021.05.25
    EP5. OSIV와 성능 최적화  (0) 2021.04.09
    EP4. API 개발 순서  (0) 2021.04.08