쿼리 메소드 기능

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

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

 

✏️ 스프링 데이터 JPA가 제공하는 쿼리 메서드 기능

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

 

📚 1. 메소드 이름으로 쿼리 생성

메서드 이름을 분석해서 JPQL 쿼리 실행

이름과 나이를 기준으로 회원을 조회

📖 A. 순수 JPA 리포지토리

MemberJpaRepository에 추가 - 순수 JPA 리포지토리

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

 

MemberJpaRepositoryTest에 추가 - 순수 JPA 테스트 코드

    @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);
    }
스크린샷 2022-04-25 오후 5 20 56

 

📖 B. 스프링 데이터 JPA

MemberRepository - 스프링 데이터 JPA

public interface MemberRepository extends JpaRepository<Member, Long> {
    List<Member> findByUsernameAndAgeGreaterThan(String username, int age);

    List<Member> findTop3HelloBy();
}
스크린샷 2022-05-02 오후 3 33 58

findByUsernameAndAgeGreaterThan : member0_.username=? + and member0_.age>?

스프링 데이터 JPA는 메서드 이름을 분석해서 JPQL을 생성하고 실행한다.
→ 그래서 필드명을 틀리면 안된다!

 

실행 결과

스크린샷 2022-04-25 오후 5 29 39

 

📌 쿼리 메소드 필터 조건
스프링 데이터 JPA 공식 문서 참고

 

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

  • 조회 : find...By ,read...By ,query...By get...By
  • COUNT : count...By 반환타입 long
  • EXISTS : exists...By 반환타입 boolean
  • 삭제 : delete...By, remove...By 반환타입 long
  • DISTINCT : findDistinct, findMemberDistinctBy
  • LIMIT : findFirst3, findFirst, findTop, findTop3

 

💡 참고
클래스 필드명과 테이블 필드명이 다를 경우, No property age found for type Member! Di you mean 'age2'? 와 같은 오류가 발생한다.

  • 이 기능은 엔티티의 필드명이 변경되면 인터페이스에 정의한 메서드 이름도 꼭 함께 변경해야 한다. 그렇지 않으면 애플리케이션을 시작하는 시점에 오류가 발생한다.
  • 이렇게 애플리케이션 로딩 시점에 오류를 인지할 수 있는 것이 스프링 데이터 JPA의 매우 큰 장점이다.

 

📚 2. JPA NamedQuery (x)

실무에서는 거의 쓸 일이 없어 중요하지 않다.

📖 A. 순수 JPA 이용

@NamedQuery 어노테이션으로 Named 쿼리 정의

@NamedQuery(
        name="Member.findByUsername",
        query="select m from Member m where m.username = :username"
)
public class Member { ... }
  • 엔티티에 @NameQuery를 정의한다.

 

JPA를 직접 사용해서 Name 쿼리 호출

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

테스트 실행 코드

    @Test
    public void testNameQuery() {
        Member m1 = new Member("AAA", 10);
        Member m2 = new Member("BBB", 20);
        memberJpaRepository.save(m1);
        memberJpaRepository.save(m2);

        List<Member> result = memberJpaRepository.findByUsername("AAA");
        Member findMember = result.get(0);
        assertThat(findMember).isEqualTo(m1);
    }

테스트 실행 결과

스크린샷 2022-04-25 오후 5 46 38
  • 구현하기 상당히 귀찮다.

 

📖 B. 스프링 데이터 JPA로 NamedQuery 사용

스프링 데이터 JPA로 NamedQuery 사용

@Query(name = "Member.findByUsername")
List<Member> findByUsername(@Param("username") String username);
  • @Query를 생략하고 메서드 이름만으로 NamedQuery를 호출할 수 있다.

 

스프링 데이터 JPA로 Named 쿼리 호출

public interface MemberRepository extends JpaRepository<Member, Long> { //** 여기 선언한 Member 도메인 클래스
	List<Member> findByUsername(@Param("username") String username);
}

 

테스트 실행 코드

    @Test
    public void namedQuery() {
        Member m1 = new Member("AAA", 10);
        Member m2 = new Member("BBB", 20);
        memberRepository.save(m1);
        memberRepository.save(m2);

        List<Member> result = memberRepository.findByUsername("AAA");
        Member findMember = result.get(0);
        assertThat(findMember).isEqualTo(m1);
    }

테스트 실행 결과
스크린샷 2022-04-25 오후 5 51 04

  • 스프링 데이터 JPA는 선언한 "도메인 클래스 + . + 메서드 이름"으로 Named 쿼리를 찾아서 실행한다.
  • 만약 실행할 Named 쿼리가 없으면 메서드 이름으로 쿼리 생성 전략을 사용한다.
  • 전략을 변경하는 것은 추천하지 않는다.

 

💡 참고
스프링 데이터 JPA를 사용하면 실무에서 Named Query를 직접 등록해서 사용하는 일은 드물다. 대신 @Query 를 사용해서 리파지토리 메소드에 쿼리를 직접 정의한다.

 

📚 3. @Query, 리포지토리 메소드에 쿼리 정의하기

실무에서 많이 사용되는 기술

✔️ 메서드에 JPQL 쿼리 작성

public interface MemberRepository extends JpaRepository<Member, Long> {
	@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() {
        Member m1 = new Member("AAA", 10);
        Member m2 = new Member("BBB", 20);
        memberRepository.save(m1);
        memberRepository.save(m2);

        List<Member> result = memberRepository.findUser("AAA", 10);
        assertThat(result.get(0)).isEqualTo(m1);
    }
스크린샷 2022-04-25 오후 6 35 29
  • @org.springframework.data.jpa.repository.Query 어노테이션을 사용한다.
  • 실행할 메서드에 정적 쿼리를 직접 작성하므로 이름 없는 Named 쿼리라 할 수 있다.
  • JPA Named 쿼리처럼 애플리케이션 실행 시점에 문법 오류를 발견할 수 있다. 이는 매우 큰 장점이다.

 

💡 참고

  • 실무에서는 메소드 이름으로 쿼리 생성 기능은 파라미터가 증가하면 메서드 이름이 매우 지저분해진다. 따라서 @Query 기능을 자주 사용하게 된다.
  • 동적 쿼리는 Querydsl을 써야 한다.

 

📚 4. @Query, 값, DTO 조회하기

이전까지는 엔티티만 조회했었다.
이번에는 단순한 값이나 DTO를 조회하는 방법에 대해 알아보자!

📖 A. 단순히 값 하나를 조회

MemberRepository에 추가

@Query("select m.username from Member m")
List<String> findUsernameList();
  • JPA 값 타입 (@Embedded)도 이 방식으로 조회할 수 있다.

테스트 코드 및 실행

    @Test
    public void findUsenameList() {
        Member m1 = new Member("AAA", 10);
        Member m2 = new Member("BBB", 20);
        memberRepository.save(m1);
        memberRepository.save(m2);

        List<String> result = memberRepository.findUsernameList();
        for (String s : result) {
            System.out.println("s = " + s);
        }
    }
스크린샷 2022-04-25 오후 6 46 08

 

📖 B. DTO로 직접 조회

MemberRepository에 추가

@Query("select new study.datajpa.dto.MemberDto(m.id, m.username, t.name) from Member m join m.team t")
List<MemberDto> findMemberDto();
  • DTO로 직접 조회하려면
    • JPA의 new 명령어를 사용해야 한다.
    • 패키지 경로도 모두 적어줘야 한다.
  • 그리고 다음 MemberDto와 같이 생성자가 맞는 DTO도 필요하다. (JPA와 사용방식이 동일하다.)

 

MemberDto.java

@Data
public class MemberDto {

    private Long id;
    private String username;
    private String teamName;

    public MemberDto(Long id, String username, String teamName) {
        this.id = id;
        this.username = username;
        this.teamName = teamName;
    }
}

테스트 코드 및 실행

    @Test
    public void findMemberDto() {
        Team team = new Team("teamA");
        teamRepository.save(team);

        Member m1 = new Member("AAA", 10);
        memberRepository.save(m1);
        m1.setTeam(team);


        List<MemberDto> memberDto = memberRepository.findMemberDto();
        for (MemberDto dto : memberDto) {
            System.out.println("dto = " + dto);
        }
    }
스크린샷 2022-04-25 오후 6 52 18

 

📚 5. 파라미터 바인딩

파라미터 바인딩은 위치 기반이름 기반 2가지가 있다.

select m from Member m where m.username = ?0 // 위치 기반 
select m from Member m where m.username = :name // 이름 기반

 

📖 A. 파라미터 바인딩

이전에 공부한 내용

MemberRepository에 추가

@Query("select m from Member m where m.username = :name")
Member findMembers(@Param("name") String username);
  • 위치 기반은 가독성도 떨어지고, 유지보수하기도 어렵다. 그러므로 이름 기반을 사용하자!

 

📖 B. 컬렉션 파라미터 바인딩

많이 사용하는 기능이다.

@Query("select m from Member m where m.username in :names")
List<Member> findByNames(@Param("names") Collection<String> names);
  • Collection 타입으로 in절 지원

 

테스트 코드 및 실행

    @Test
    public void findByNames() {
        Member m1 = new Member("AAA", 10);
        Member m2 = new Member("BBB", 20);
        memberRepository.save(m1);
        memberRepository.save(m2);

        List<Member> result = memberRepository.findByNames(Arrays.asList("AAA", "BBB"));

        for (Member member : result) {
            System.out.println("member = " + member);
        }
    }
스크린샷 2022-04-25 오후 7 06 05

 

📚 6. 반환 타입

스프링 데이터 JPA는 유연한 반환 타입을 지원한다.

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

 

💡 참고
스프링 데이터 JPA 공식 문서

 

  • DB에서 조회했을 때 데이터가 있는 경우도 있고 없을 경우도 있다. → 그럴 경우 Optional을 사용한다.
  • 스크린샷 2022-04-26 오후 1 40 42

 

✔️ 조회 결과가 없을 경우
스크린샷 2022-04-26 오후 1 44 32

스크린샷 2022-04-26 오후 1 47 32
  • 컬렉션 : 빈 컬렉션 반환
  • 단건 조회 : null 반환

 

✔️ 조회 결과가 많을 경우

스크린샷 2022-04-26 오후 1 45 10 스크린샷 2022-04-26 오후 1 46 31
  • 단건 조회(결과가 2건 이상) : javax.persistence.NonUniqueResultException 예외 발생

 

💡 참고

  • 단건으로 지정한 메서드를 호출하면 스프링 데이터 JPA는 내부에서 JPQL의 Query.getSingleResult() 메서드를 호출한다.
  • 이 메서드를 호출했을 때 조회 결과가 없으면 javax.persistence.NoResultException 예외가 발생하는데 개발자 입장에서 다루기가 상당히 불편하다.
  • 스프링 데이터 JPA는 단건을 조회할 때 이 예외가 발생하면 예외를 무시하고 대신에 null 을 반환한다.

 

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

0개의 댓글