티스토리 뷰

JPA

스프링 데이터 JPA - 쿼리 메서드(Query Method)

배워서 남 주는 Jaime.Lee 2021. 6. 28. 18:43
728x90
반응형

모든 소스 코드는 여기에서 확인 가능합니다.

스프링 데이터 JPA를 사용하면 기본 인터페이스 외에도 메서드 추가 만으로 직접 구현체를 구현하지 않아도 기능을 사용할 수 있습니다.

쿼리 메서드 기능

메서드 이름으로 쿼리 생성

메서드 이름을 분석하여 JPQL 쿼리를 수행합니다.

package io.lcalmsky.springdatajpa.domain.entity;

import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;

import javax.persistence.*;

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString
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")
    @ToString.Exclude
    private Team team;

    public Member(String username, int age, Team team) {
        this.username = username;
        this.age = age;
        if (team != null) {
            changeTeam(team);
        }
    }

    public void changeTeam(Team team) {
        this.team = team;
        team.getMembers().add(this);
    }
}

위와 같은 Member Entity가 있을 때 이름과 나이를 기준으로 조회한다면,

package io.lcalmsky.springdatajpa.domain.repository;

import io.lcalmsky.springdatajpa.domain.entity.Member;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.List;

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

List<Member> findByUsernameAndAgeGreaterThan(String username, int age) 이런식으로 표현할 수 있습니다.

스프링 데이터 JPA가 제공하는 쿼리 메서드 세부 기능은 다음과 같습니다.

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

위 feature들을 적절하게 섞어 SQL문을 작성하듯이 메서드를 작성할 수 있습니다. IntelliJ 같은 IDE에서는 메서드를 만들 때 자동완성을 지원해주기도 합니다.

Entity의 필드명이 변경될 경우 인터페이스의 메서드명도 같이 변경되어야 합니다.

NamedQuery

@Entity 클래스에 @NamedQuery 애너테이션을 추가하여 사용합니다.

package io.lcalmsky.springdatajpa.domain.entity;

import javax.persistence.*;

@Entity
@NamedQuery(
        name = "Member.findByUsername",
        query = "select m from Member where m.username = :username"
)
public class Member {
    ...
}
public interface MemberRepository extends JpaRepository<Member, Long> {
    List<Member> findByUsername(@Param("username") String username);
} 
  • 도메인 클래스 + "." + 메서드 이름으로 NamedQuery를 찾아 실행합니다.

실무에선 거의 사용하지 않으므로 자세한 내용은 생략하겠습니다.

@Query 애너테이션 사용

Repository 인터페이스 메서드에 @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);
}
  • 이름 없는 NamedQuery라고 할 수 있습니다.
  • 앱 실행 시점에 문법 오류를 발견할 수 있습니다.
  • 파라미터가 증가하면 메서드 이름만으로 개발하기에 한계가 있으므로 실무에서 많이 사용하는 방법입니다.

@Query로 바로 클래스에 매핑하기

Entity나 Entity List로 매핑하는 게 아닌 다른 데이터 타입 클래스로 매핑할 수 있습니다.

usernameString 이므로 List<String>으로 반환 타입을 지정하면 알아서 매핑해줍니다.

@Repository
public interface MemberRepository extends JpaRepository<Member, Long> {
    @Query("select m.username from Member m")
    List<String> findUsernameList();
}

실무에서 성능을 위해 DTO로 바로 매핑해야 하는 경우가 있습니다.

그럴 땐 아래 처럼 new operation을 이용해 바로 객체를 생성해 반환할 수 있습니다.

package io.lcalmsky.springdatajpa.domain.dto;

import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class MemberDto {
    private Long id;
    private String username;
    private String teamName;
}
@Repository
public interface MemberRepository extends JpaRepository<Member, Long> {
    @Query("select new io.lcalmsky.springdatajpa.domain.dto.MemberDto(m.id, m.username, t.name) from Member m join m.team t")
    List<MemberDto> findMemberDto();
}

파라미터 바인딩(Parameter Binding)

JPQL에서 파라미터를 위치 기반, 이름 기반으로 바인딩 할 수 있습니다.

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

위치 기반 파라미터 바인딩은 파라미터의 위치가 바뀌거나 중간에 where 조건이 추가되면 반드시 같이 수정되어야 하므로 실무에선 잘 쓰이지 않습니다.

되도록이면 이름 기반 파라미터 바인딩을 사용해야 합니다.

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

이름 기반 파라미터 바인딩을 사용하는 경우 @Param 애너테이션으로 어떤 이름에 매핑시킬지 정의합니다.

@Query를 쓴 것이 아니라 메서드 이름으로 쿼리를 생성했을 경우 @Param 은 생략 가능합니다.

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

컬렉션 파라미터를 바인딩 할 수도 있습니다.

@Repository
public interface MemberRepository extends JpaRepository<Member, Long> {
    @Query("select m from Member m where m.username in :names")
    List<Member> findByNames(@Param("names") Collection<String> names);
}

반환 타입

Optional<T>, T, List<T> 등 유연한 반환 타입을 제공합니다.

@Repository
public interface TeamRepository extends JpaRepository<Team, Long> {
    List<Team> findTeamsByName(String name); // 여러 개 조회
    Team findTeamByName(String name); // 한 개 조회
    Optional<Team> findNullableTeamByName(String name); // 0..1개 조회
}

한 개만 조회해야 하는 경우 여러 개가 조회되면 에러가 날 수 있습니다. 따라서 반드시 한 개가 조회되는 경우에만 반환 타입을 Entity의 타입으로 지정하셔야 합니다.

하나도 조회되지 않을 경우 null을 반환하니 NullPointerException을 방지하기 위한 로직을 추가하든지 Optional을 사용하는 것이 더 좋은 방법입니다.

이 외에도 void, primitives, wrapper types, Stream<T>, Future<T>, CompletableFuture<T>, Page<T> 등 어마어마하게 많은 타입으로 반환할 수 있으니 자세한 내용은 스프링 공식 문서를 참고하시면 됩니다.

728x90
반응형
댓글
댓글쓰기 폼