[실전! 스프링 데이터 JPA] WEEK 15

enxnong·2023년 12월 31일

김영환님의 강의 실전! 스프링 데이터 JPA 보면서 공부한 내용입니다.

🏊‍ 섹션 4

메소드 이름으로 쿼리생성

드 기능

  • 메소드 이름으로 쿼리 생성
  • 메소드 이름으로 JPA NamedQuery 호출
  • @Query 어노테이션을 사용해서 리파지토리 인터페이스에 쿼리 직접 정의

✅ 메소드 이름으로 쿼리 생성

    // MemberJpaRepository.java
    public List<Member> findByUsernameAndAgeGreaterThen(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();
    }
    
    @Test
    public void findByUsernameAndAgeGreaterThen() {
        Member m1 = new Member("AAA", 10);
        Member m2 = new Member("AAA", 20);
        memberJpaRepository.save(m1);
        memberJpaRepository.save(m2);

        List<Member> result = memberJpaRepository.findByUsernameAndAgeGreaterThen("AAA", 15);

        assertThat(result.get(0).getUsername()).isEqualTo("AAA");
        assertThat(result.get(0).getAge()).isEqualTo(20);
        assertThat(result.size()).isEqualTo(1);
    }

📝 문제점

  • 쿼리를 직접 짜야함 → 스프링 데이터 JPA가 해결해줌
// MemberRepository.java
List<Member> findByUsernameAndAgeGreaterThan(String username, int age);
// 쿼리 직접 작성 X
// 인터페이스만 만들고 구현체 안만듬

// 테스트 동일    

💡 관례

  • Username And Age => 관계조건으로 묶임
  • Username이 equal로 컨디션을 먹임
  • AgeGreaterThan 은 파라미터 조건보다 크면 이라고 되어있음
  • 이름을 다르게하면 안먹힘
  • 엔티티 필드명이 변경되면 인터페이스에 정의한 메서드 이름도 함께 변경해야함
    → 변경하지 않을 시 애플리케이션을 시작하는 시점에서 오류 발생

💡쿼리 메소드 필터 조건 스프링 데이터 JPA 공식 문서 참고 https://docs.spring.io/spring-data/jpa/reference/jpa/query-methods.html#jpa.query-methods.query-creation

📝 스프링 데이터 JPA가 제공하는 쿼리 메소드 기능

  • 조회: find...By... ,read...By, query...By, get...By
    단, findHelloBy 처럼 ...에 식별하기 위한 내용(설명)이 들어가도 된다. findHelloBy는 By 뒤에 아무것도 안들어가있으므로 전체 조회됨
  • COUNT: count…By → 반환타입 long
  • EXISTS: exists…By → 반환타입 boolean
  • 삭제: delete…By, remove…By → 반환타입 long
  • DISTINCT: findDistinct, findMemberDistinctBy
  • LIMIT: findFirst3, findFirst, findTop, findTop3

JPA NamedQuery

✅ JPA NamedQuery
→ 실무에서 거의 쓸 일 없음 ㅎㅎ

📝 장점

  • 쿼리에서 문법오류가 있으면 애플리케이션 실행 전에 알려줌

📝 단점

  • 기능을 실행하기 전까지 문법이 맞는지 틀린지 모름
@NamedQuery(
        name="Member.findByUsername",
        query = "select m from Member m where m.username = :username"
)
public class Member {}

// MemberRepository.java
@Query(name = "Member.findByUsername")
List<Member> findByUsername(@Param("username") String username);

// MemberJpaRepository.java
public List<Member> findByUsername(String username){
    return em.createNamedQuery("Member.findByUsername", Member.class)
          .setParameter("username", username)
          .getResultList();
}

@Query

📝 장점

  • sql에서 오타가 있는 경우 애플리케이션 로딩시점에 문법 오류를 발생해서 바로 알게해줌
  • 파라미터 값이 많은 경우 사용하기 좋음
    // @Query 어노테이션
    @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 어노테이션 (값 조회)
    @Query("select m.username from Member m")
    List<String> findUsernameList();

    // @Query 어노테이션 (DTO 조회)
    @Query("select new study.datajpa.dto.MemberDto(m.id, m.username, t.name) from Member m join m.team t")
    List<MemberDto> findMemberDto();

파라미터 바인딩

📝 종류

  • 위치기반 (거의 사용X)
    select m from Member m where m.username = ?1
  • 이름기반 (주로 사용O)
    select m from Member m where m.username = :username
    // 컬렉션 파라미터 바인딩
    @Query("select m from Member m where m.username in :names")
    List<Member> findByNames(@Param("names") Collection<String> names);

반환타입

// 반환 타입
// 반환 타입 유연하게 사용 가능
List<Member> findListByUsername(String username); // 컬렉션
Member findMemberByUsername(String username); // 단건
Optional<Member> findOptinalByUsername(String username); // 단건 Optional

순수 JPA 페이징과 정렬

📝 정렬 예제

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

    // 페이지 정렬
    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();
    }
@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 = 0;
        int limit = 3;

        //when
        List<Member> members = memberJpaRepository.findByPage(age, offset, limit); // 0번부터 3개를 뽑음
        long totalCount = memberJpaRepository.totalCount(age);

        // then
        assertThat(members.size()).isEqualTo(3);
        assertThat(totalCount).isEqualTo(5);

    }

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

📝 페이징과 정렬 파라미터

  • org.springframework.data.domain.Sort : 정렬 기능
  • org.springframework.data.domain.Pageable : 페이징 기능 (내부에 Sort 포함)

📝 특별한 반환 타입

  • org.springframework.data.domain.Page : 추가 count 쿼리 결과를 포함하는 페이징 (totalCount를 필요로하는 페이지)
  • org.springframework.data.domain.Slice : 추가 count 쿼리 없이 다음 페이지만 확인 가능(내부적으로 limit + 1조회, 더보기 버튼이 있는 페이지)
  • List (자바 컬렉션): 추가 count 쿼리 없이 결과만 반환

✅ Page

  • totalCount 날라감
    // 페이지 테스트
    @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"));
        // pageNumber은 1이 아닌 0부터 시작
        // pageSize 3 : 0페이지에서 3개 가져와

        //when
        Page<Member> page = memberRepository.findByAge(10, pageRequest); // 0번부터 3개를 뽑음
        // 반환 타입을 페이지로 받으면 알아서 totalCount쿼리를 날려줌

        // then
        List<Member> content = page.getContent(); // 페이지에있는 값들 가져옴
        long totalElements = page.getTotalElements();// totalCount

        assertThat(content.size()).isEqualTo(3); // 조회된 데이터
        assertThat(totalElements).isEqualTo(5); // 전체 조회된 테이터
        assertThat(page.getNumber()).isEqualTo(0); // 페이지 번호
        assertThat(page.getTotalPages()).isEqualTo(2); // 전체 페이지 갯수
        assertThat(page.isFirst()).isTrue(); // 첫번째 페이지인가?
        assertThat(page.hasNext()).isTrue(); // 다음 페이지가 있어?

    }
  • limit 갯수 3개를 가져오는 것을 확인할 수 있다

✅ Slice

  • totalCount 안날라감
// MemberRespository
Slice<Member> findByAge(int age, Pageable pageable);
  
  
// Test
@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"));
        // pageNumber은 1이 아닌 0부터 시작
        // pageSize 3 : 0페이지에서 3개 가져와

        //when
        Slice<Member> page = memberRepository.findByAge(10, pageRequest); // 3개를 요청했지만 limit + 1 해서 총 4개를 요청함
        // 그렇기 때문에 totalCount를 가져오지않음

        // then
        List<Member> content = page.getContent(); // 페이지에있는 값들 가져옴


        assertThat(content.size()).isEqualTo(3); // 조회된 데이터
        assertThat(page.getNumber()).isEqualTo(0); // 페이지 번호
        assertThat(page.isFirst()).isTrue(); // 첫번째 페이지인가?
        assertThat(page.hasNext()).isTrue(); // 다음 페이지가 있어?

    }
  • limit + 1인 4개를 요청한 것을 확인할 수 있다
profile
높은 곳을 향해서

0개의 댓글