Spring Data JPA - 쿼리 만들기 + 실습

Spring Data JPA

백기선님의 강의인 Spring Data JPA 강의를 듣고 공부한 내용을 정리한 글

Spring Data Common에서 쿼리 만드는 방법

스프링 데이터가 지원하는 쿼리 만드는 방법에 대해서 살펴보자.

쿼리 만드는 방법은 크게 두 가지이다.

먼저 첫 번째로, 메소드 이름을 분석해서 쿼리를 만드는 방법이 있다.

interface PostRepository: JpaRepository<Post,Long>{
    fun findPostById(id: Long): Post?
}

메소드의 이름을 JPA가 분석해서 쿼리를 만들 수 있다.

이러한 쿼리 생성 전략을 CREATE 전략이라고 부른다.

두 번째 방법은 미리 정의해둔 쿼리를 찾아 사용하는 방법이 있다.

예시는 아래와 같다.

@RepositoryDefinition(domainClass = Comment::class, idClass = Long::class)
interface CommentRepository: MyRepository<Comment,Long> {

    @Query("SELECT c FROM Comment AS c")
    fun findByTitleContains(keyword: String): List<Comment>
}

USE_DECLARED_QUERY라고도 불리우는 이 방법은 정의하는 방법이 구현체마다 다르다.

우리는 JPA를 사용하고 있기 때문에 HQL을 기본값으로 받아들여서 사용하고 있다.

만약 네이티브 쿼리를 주고 싶은 경우라면 아래와 같이 사용할 수 있다.

@RepositoryDefinition(domainClass = Comment::class, idClass = Long::class)
interface CommentRepository: MyRepository<Comment,Long> {

    @Query("SELECT c FROM Comment AS c", nativeQuery = true)
    fun findByTitleContains(keyword: String): List<Comment>
}

앞서 크게 두 가지 방법이 있다고 했지만 사실 하나가 더 있다.

사실상 위의 두 방법을 합친 것인데, 미리 정의한 쿼리를 찾아보고 없으면 새로 만들기라는 CREATE_IF_NOT_FOUND 전략이 존재한다.

쿼리 애노테이션을 찾아보고, 없는 경우라면 메소드 이름을 분석해서 찾아 사용한다.

이 전략이 기본 전략이다.

이 전랴깅 어디에 적용되어있는지를 보려면 @EnableJpaRepositoriesqueryLookupStrategy라는 속성을 사용해서 설정할 수 있다.

@SpringBootApplication
@EnableJpaRepositories(queryLookupStrategy = QueryLookupStrategy.Key.CREATE_IF_NOT_FOUND)
class JpaApplication

fun main(args: Array<String>) {
    runApplication<JpaApplication>(*args)
}

쿼리를 찾는 방법

메소드 이름으로 쿼리를 표현하기 힘든 경우에 사용한다.

저장소 기술에 따라 다른 선언을 보인다.

JPA의 경우 @Query 외에도 @NamedQuery라는 것을 사용할 수 있다.

우선 순위는 1위가 @Query이고 2위가 @Procedure 3위가 @NamedQuery이다.

만약에 절대 그럴일은 없겠지만 위 세개의 애노테이션이 모두 선언된 경우라면(…) @Query가 가장 먼저 적용된다는 사실을 기억하자.

쿼리를 만드는 방법

자바와 코틀린은 문법이 조금 다르기 때문에 코틀린을 기준으로 하겠다.

우선 코틀린의 경우

fun {접두어}{도입부}By{프로퍼티 표현식}{조건식}[(And|Or){프로퍼티 표현식}{조건식}]{정렬 조건}{매개 변수}

뭔가 조금 복잡해보이지만 예시를 보면 쉽게 알 수 있다.

우선 접두어는 Find,Get,Query,Count 등이 온다.

우리는 일반적으로 조회를 위해 find를 사용하고 있지만 Get,Count 같은 접두어들도 사용할 수 있다.

도입부는 Distict,First(N), Top(N)등이 같은 것들이 올 수 있다.

예를 들어보면 findFirst10의 경우 조회되는 처음 10개를 가져오라는 것이며, findTop10의 경우 정렬 후 가장 우선순위가 높은 10개를 가져오는 명령이 될 것이다.

다음은 Entity에 있는 프로퍼티를 줘야한다.

이 경우 조건식을 줄 수 있다. IgnoreCase 또는 어떤 값의 범위를 줄 수도 있다.

아래는 예시이다.

fun findByLikeGreaterThanAndPost(likeCount: Int, post:Post, pageable: Pageable): Page<Comment>

주의해야할 점은 페이징을 하고자 할 때 pageable을 매개변수로 줘야한다는 점이다.

반환 값을 보면 Page 인터페이스를 사용하고 있는데 호기심 많은 개발자라면,

List로 사용해도 될 것 같은데? 왜 굳이? 라는 의문이 들 수 있겠다.

우선 Page를 사용하면 페이징에 필요한 정보들 (현재가 몇 페이지인지, 총 페이지가 몇 페이지이며)등을 얻을 수 있다는 장점이 있다.

조건식 다음에는 정렬조건이 올 수 있으므로 OrderBy{프로퍼티} 이후 Asc or Desc로 정렬 조건을 명시할 수 있다.

다른 방법으로는 우리가 페이징할 때 매개변수로 받았던 pageable에다가 소팅에 관련된 정보를 같이 넣어줄 수도 있다.

참고로 페이징을 안하고 소팅만 하겠다고 하면 pageable을 빼고 Sort 인터페이스만 넘겨주면 된다. 다만 이 경우라면 페이징이 안되니까 List로 받아야한다.

권장하는 방법은 pageable을 넘겨서 Page로 받는 것이 일반적이라고 한다.

실제로 쿼리를 만드는 경우

가장 기본적인 전략은 먼저, 메소드 이름으로 구현이 가능한지 확인해봐야한다.

이를 테스트하기 위해선 테스트코드를 작성해서 해당 쿼리가 내가 의도한대로 동작하는지 확인해보면 된다.

참고로 메소드 이름을 잘못 정의한 경우라면 애초에 빈 생성 자체가 안되서 오류가 뜬다.

테스트 코드 작성

@Test
fun crud() {
  val comment = Comment()
  comment.comment = "spring Data JPA"
  commentRepository.save(comment)

  val comments = commentRepository.findByCommentContains("Spring")
  assertThat(comments.size).isEqualTo(1)
}

강의에서는 대소문자를 구분 못해서 에러가 나는데, 하이버네이트가 똑똑해졌는지 이제는 대소문자를 구분 없이 찾아준다 (!!)

원래라면 IgnoreCase를 사용했어야 했지만 하이버네이트가 똑똑해져서 이제는 알아서 처리해준다.

@Test
fun crud() {
  val comment = Comment()
  comment.comment = "spring Data JPA"
  comment.likeCount = 6
  commentRepository.save(comment)

  val comments = commentRepository.findByCommentContainsAndLikeCountGreaterThan("Spring",10)
  assertThat(comments.size).isEqualTo(1)
}

And 조건을 사용해서 추가적으로 likeCount 프로퍼티 값이 특정 값이 이상인 경우를 Entity에서 찾으라고 할 수도 있다.

그리고 OrderBy를 사용해 정렬을 하는 것 또한 가능하다.

아래는 예시.

@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class CommentRepositoryTest {

    @Autowired
    lateinit var commentRepository: CommentRepository

    @Test
    fun crud() {
        this.makeComment(100,"spring data Jpa")
        this.makeComment(55,"HIBERNATE SPRING")

        val comments = commentRepository.findByCommentContainsOrderByLikeCountDesc("Spring")
        assertThat(comments.size).isEqualTo(2)
        assertThat(comments).first().hasFieldOrPropertyWithValue("likeCount",100)
    }

    private fun makeComment(likeCount:Int, commentContent: String) {
        val comment = Comment()
        comment.comment = commentContent
        comment.likeCount = likeCount
        commentRepository.save(comment)
    }

}

이 예제코드에서 재밌는 점은 hasFieldOrPropertyWithValue 메소드를 사용해서 첫 번째로 반환되는 프로퍼티의 값이 얼마인지를 테스트 조건으로 명시할 수 있다는 것이다.

또한 자바8에 추가된 Stream을 사용할 수 있다.

이 스트림을 사용하는 경우라면 tryWithResource를 사용해야한다.

그리고 비동기 쿼리를 사용할 수 있다.

이 기능의 자세한 내용은 다음 시간에 살펴보도록 하자.

Reference

인프런 백기선님의 스프링 Data JPA



© 2022. by minkuk

Powered by minkuk