[Spring] JPA 쿼리 사용하기

김재연·2023년 3월 12일
0

수숙관

목록 보기
6/17
post-thumbnail

🧩Query Method

🤔문제상황

이번에 만들어야하는 쿼리는 다음과 같다.

  1. 수업A에 딸린 수업일지들 중에 가장 최근거 가져오기
  2. 수업A에 딸린 수업일지 a,b,c,... 에 딸린 복습항목들 모두 가져와서 미완료 우선, 최신순 정렬하기

내가 지금 쓸 줄 아는 조회 쿼리는 findByIdfindAll 밖에 없는데.. 저런 세세한 조건으로 필터링을 대체 어떻게 해서 가져오는거지❗🤔

그렇게 🌊구글링의 바다🌊에서 헤엄치다가 스프링 데이터 JPA가 제공하는 쿼리메소드라는 것을 알게 되었다.

💡쿼리 메소드

쿼리메소드를 사용하는 방법에는 세가지가 있다.

  1. 메소드 이름 정의하기 (자동생성)
  2. @NamedQuery 사용하기
  3. @Query로 직접 쿼리 작성하기

1. 메소드 이름 정의하기 (자동생성)

Repository 안에 정해진 공식에 따라 메소드 이름을 정의하고 실행하면, JPA가 메소드 이름을 분석해서 자동으로 JPQL을 생성한 후 실행하는 방식이다.

메소드 이름 공식은 JPA 공식문서⬇️에서 확인할 수 있다.

예를 들어 이메일과 이름으로 유저를 찾기 위해 Repository 안에 findByEmailAndName이라는 이름의 메소드를 만들어놓으면,

public interface UserRepository extends Repository<User, Long> {
  List<User> findByEmailAndName(String email, String name);
}

이를 실행했을 때 select u from User u where u.email = ?1 and u.name = ?2 라는 JPQL이 만들어지고 실행되는 것이다.

너무 복잡한 쿼리는 오히려 이 방법이 더 헷갈리는거 같아서, 비교적 작성해야하는 쿼리가 간단한 1번(수업A에 딸린 수업일지들 중에 가장 최근거 가져오기)을 이 방식으로 작성했다.

/* repository */
public interface NoteRepository extends JpaRepository<Note, Long> {
    List<Note> findByTutoringOrderByDateTimeDesc(Tutoring tutoring);
}
/* service */
Optional<Tutoring> tutoring = tutoringRepository.findById(1L);
List<Note> notes = noteRepository.findByTutoringOrderByDateTimeDesc(tutoring.get());
Note note = notes.get(0); // 1번 수업에 달린 가장 최근 수업일지

이 쿼리메소드를 실행하면 터미널에 아래와 같이 자동으로 생성된 JPQL이 찍힌다.

참으로 신기한 기능이당당.

➕ 2023.06.12 추가

// 시간역순으로 정렬 후 가장 위에 있는 row 1개만 가져오기 //
// 즉, 가장 최근 row 가져오기 //
Optional<Note> findFirst1ByTutoringOrderByDateTimeDesc(Tutoring tutoring);

2. @NamedQuery 사용하기

JPA는 메소드 이름으로 JPA Named 쿼리를 호출하는 기능도 가지고 있다. @NamedQuery 어노테이션으로 Named 쿼리를 정의해두고, JPA로 Named 쿼리를 호출하는 방식인데, 실무에서는 잘 쓰이지 않는다고 한다.


3. @Query로 직접 쿼리 작성하기

@Query 어노테이션을 이용해서 Repository에 쿼리를 직접 정의해 사용하는 방식인데, Repository에서 쿼리를 관리하기 때문에 쿼리에 대한 관리를 Repository에서만 집중할 수 있어 유지보수성도 뛰어나다는 장점이 있다.

public interface UserRepository extends JpaRepository<User, Long> {
  @Query("select u from User u where u.emailAddress = ?1")
  User findByEmailAddress(String emailAddress);
}

findByEmailAddress를 실행하면 그 위에 정의된 쿼리문이 실행된다.

기본적으로 @Query 안에는 JPQL을 써야 하는데, nativeQuery = true 옵션을 주면 MySQL을 직접 쓸 수 있다. 나는 그냥 바로 MySQL Workbench에서 차근차근 SQL 작성한 다음에 복붙해오는게 더 편한거 같아서 네이티브쿼리를 사용했다. 근데 JPQL이라고 해봤자 차이점은 SQL문으로 select * from User인 거를 JPQL로는 select u from User u 이런식으로 테이블 이름 별칭을 꼭 지정해줘야 한다는 점? 이보다 더 자세히는 알아보지 않아서 모르겠다.

아무튼 처음에 했던 바보짓은 @Query 안에 쿼리문을 여러개 썼던건데, 이 안에는 쿼리문 한개씩만 실행되는거 같다. 그래서 다중쿼리문으로 바꿈!

(개인적인 생각) 복잡한 쿼리를 작성할때는 내맘대로 이것저것 막 할 수 있으니까 오히려 이 방법이 나은거 같다. 그래서 2번(수업A에 딸린 수업일지 a,b,c,... 에 딸린 복습항목들 모두 가져와서 미완료 우선, 최신순 정렬하기)을 이 방법으로 작성했다.

/* repository */
public interface ReviewRepository extends JpaRepository<Review, Long> {
    @Query(
            value = "SELECT * FROM susukgwan.review WHERE note_id IN (SELECT id FROM susukgwan.note WHERE tutoring_id = :tutoringId) ORDER BY is_completed, id DESC",
            nativeQuery = true
    )
    List<Review> GetReviewListbyTutoringId(@Param(value="tutoringId") Long tutoringId);
}

/* service */
List<Review> reviewList = reviewRepository.GetReviewListbyTutoringId(1L);
// 1번 수업에 달린 모든 수업일지에 달린 복습항목들을 미완료 우선/최신순 정렬해서 가져옴

이 쿼리메소드를 실행했을 때, 터미널에 직접 작성한 SQL이 그대로 뜨는 것을 볼 수 있다.

cf) 똑같은 쿼리(코드)인데 길이가 이만큼이나 짧아졌따.

2023-04-26 추가
❗여러 줄로 나눠서 쓸때는 각각의 문자열 마지막에 띄어쓰기까지 정확하게 포함해서 문자열 연산을 해줘야 한다.

@Query(
            value = "SELECT t.tutor_id " +
                    "FROM susukgwan.tutoring AS t " +
                    "JOIN susukgwan.note AS n " +
                    "ON t.id = n.tutoring_id " +
                    "JOIN susukgwan.review AS r " +
                    "on n.id = r.note_id " +
                    "WHERE r.id = :reviewId",
            nativeQuery = true
    )
    Long GetTutorIdOfReview(@Param(value = "reviewId") Long reviewId);

❗❗그리고 SQL문 마지막에 ;도 뺄 것!!


파라미터 바인딩

스프링 JPA에서는 쿼리문에 파라미터를 사용할수가 있는데, 위치기반이름기반 파라미터 바인딩 2가지 방법이 있다.

  • 위치기반 파라미터 바딩
    : 파라미터 순서만 맞으면 된다.
    : 쿼리문안에서 파라미터는 순서대로 ?1, ?2, ?3, ... 으로 받는다.
    : 별로 추천되는 방법은 아니다. (당연함.. 명확하지 않고 헷갈리니까)
public interface MemberRepository extends JpaRepository<Member, Long> {
    @Query("select m from Member m where m.username = ?1")
    Member findMembers2(String username);
}
  • 이름기반 파라미터 바인딩
    : @Param을 사용하여 파라미터와 바인딩할 곳 이름을 맞춰준다.
    : 쿼리문안에서 파라미터는 :{paramName}으로 받는다.
    : 추천되는 방법!
public interface MemberRepository extends JpaRepository<Member, Long> {
    @Query("select m from Member m where m.username = :name")
    Member findMembers(@Param("name") String username);
    
    /* 컬렉션 파라미터 바인딩 */
    @Query("select m from Member m where m.username in :names")
	List<Member> findByNames(@Param("names") Collection<String> names);
}

Reference

Spring Data JPA(2) - JpaRepository 쿼리 메소드 기능
JPA 공식문서 Query Methods 부분

profile
일기장같은 공부기록📝

0개의 댓글