본문 바로가기

FastCampus - 한번에 끝내는 Java|Spring 웹 개발/04 JPA

JPA Ch 9 @Query (3), NativeQuery - 패스트캠퍼스 챌린지 39일차

오늘은 세로로도 찍어봤습니다. 세로로 길어서... 물론 필기는 가로로 했지만. 강의가 두 개인데 주제가 확 달라서 그냥 새 페이지를 열었다능

다행히도 어제 배운 내용을 까먹지는 않았습니다. 아니, 적어도 다음에 배울 게 뭐였는지는 안 까먹었습니다. 그래서 오늘의 주제는 다음 두 가지입니다.

  • @Query (3강): 생뚱맞은 객체를 만들어 반환하기 (DTO 같은 거) 등
  • 다음 강의 NativeQuery 써보기

@Query 에서 DTO

이전편에서 @Query(value="JPQL 쿼리문") 으로 존재하지 않는 DTO인 BookNameAndCategory 를 만들어서 반환하고자 했습니다. 방법은 다음 두 가지입니다.

  • BookNameAndCategory 를 인터페이스로 바꾸기 (getter, setter 로 바꿔야 합니다.)
    • interface IBookNameAndCateogory 를 만들었습니다.
    • "select b.name, b.category from Book b" 면 됩니다.
  • JPQL로 직접 객체 생성하기
    • new 객체() 로 쿼리 안에서 진짜로 객체를 생성할 수 있습니다.
    • 전체 패키지 이름을 넣어야 합니다.
      "select new kr.co.fastcampus.jpasandbox.book.BookNameAndCategory(...생략"

프로젝션이라고 부르는 모양입니다. (명칭 정도는 가르쳐주시지...)
"자주 보이는 사이트": https://www.baeldung.com/spring-data-jpa-projections
오피셜: https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#projections

바로 둘 다 해보죠. 아래는 BookRepo 의 아래부분에 추가한 메소드입니다. 인터페이스로 쿼리를 할 때에는 꼭 프로퍼티 이름이랑 매칭을 시켜줍시다. 아니면 그냥 JPQL에서 객체를 통으로 반환하던가요. (안 그러고 싶으시죠?)

    //                       빼먹었다가 낭패봄
    //                            vvvvvvv
    @Query(value = "select b.name as name, b.category as category from Book b")
    List<IBookNameAndCategory> findAllBookNameAndCategory();

    @Query(value =
            "select" +
            " new kr.co.fastcampus.jpasandbox.book.BookNameAndCategory(" +
            "   b.name, b.category" +
            " )" +
            " from Book b")
    List<BookNameAndCategory> findAllBookNameAndCategoryJPQL();
}

인터페이스를 짜는 게 귀찮아서 kotlin으로 갈아치웠습니다. 그리고 삽질이 이어지게 되는데...

data class 로 반환하는 케이스의 경우, kotlin 으로는 처음에는 오류가 나서 이런저런 가설을 세워봤는데 (setter를 쓰나? 기본값이 있으면 상관없나?) 결국 다 상관없고 nullable 이

interface IBookNameAndCategory {
    val name: String?
    val category: String?
}

data class BookNameAndCategory(
    var name: String?,
    var category: String?
)//                     ^
//       nullable 이 아니면 오류가 납니다.
//          ("could not instanciate tuple? 이었나?") 
//     https://stackoverflow.com/a/40960220

테스트는 임시로 객체를 두 개 만들어서.

@Test
fun makingDtoTest() {
    val book1 = putBookWithName("book1")
    val book2 = putBookWithName("book2")
    book2.isDeleted = true
    bookRepo.save(book2)


    val interfaceDtoList: MutableList<IBookNameAndCategory> = bookRepo.findAllBookNameAndCategory()
    interfaceDtoList.forEach { println("${it.name}, ${it.category}") }
    println("--------------------")
    val dtoList: MutableList<BookNameAndCategory> = bookRepo.findAllBookNameAndCategoryJPQL()
    dtoList.forEach { println("${it.name}, ${it.category}") }
}

테스트 결과를 봅시다. 예상대로 동작. 클래스의 머리에 붙어있는 @Where(clause = "deleted = false") 가 동작하는 게 인상적이네요.

====================
Hibernate: 
    select
        book0_.name as col_0_0_,
        book0_.category as col_1_0_ 
    from
        book book0_ 
    where
        (
            book0_.deleted = false
        )
book1, null
--------------------
Hibernate: 
    select
        book0_.name as col_0_0_,
        book0_.category as col_1_0_ 
    from
        book book0_ 
    where
        (
            book0_.deleted = false
        )
book1, null

 

페이징 & 잡설

페이징은 이전 강의 어딘가에서 배웠던 거처럼 그냥 패러미터로 (Pageable pageable) 을 넣으면 됩니다. 쿼리를 할 때에는 PageRequest.of(...) 로 넣으시면 되구요. @Query(value="JPQL")을 써도 오버로딩이 평범하게 되거든요. 코드 생략.

이어지는 강사님의 당부: 적절한 도구를 적절한 용도로. 잘 모르겠으니까 쿼리메소드만 쓰는 일이 잦다고 하시네요. 그... 저희 Query DSL은...

NativeQuery

바로 이어지는 NativeQuery 는 이전에도 간혹 썼던 기능으로, 이름에서 유추하실 수 있듯이, JPQL 이 아닌 실제 DB의 쿼리를 날리게 되는 옵션입니다. 이전에 모르고 썼던 걸 들고와보죠.

@Modifying
@Query(value = "update book set category='none'", nativeQuery = true)
void update();

보시다시피 JPQL에서 보였던 Book book 같은 구문이 보이지 않습니다. 네이티브 DB가 @Entity 가 붙어있는 JVM 객체의 유무를 알 리가 없으니 당연하겠죠.

그 외에도 테이블 이름 등도 JPA 가 실제 쿼리로 내놓는 거랑 맞춰줘야 합니다. createdAt 이라고 쓰고싶으시겠지만, 지금 제 설정에서는 실제 쿼리는 created_at 이라고 나오므로 제대로 snake_case 로 맞춰서 써야 합니다.

NativeQuery あるある

그리고, @Where() 로 제시되었던 조건을 무시하게 되므로 주의합시다. 쿼리문을 특정 DB용으로 커스텀으로 만드는거니까 당연하겠죠; 개발자가 만든 특정 DB 전용 쿼리를 어떻게/왜 JPA가 분석해요; 할 이유도 없고 해서도 안 됩니다. (강의에서는 예시로 확인함, 저는 실습 생략합니다)

물론... 특정 DB에 의존적인 구문이 되므로, 아무리 대상 DB를 잘 바꾸지 않는다고 해도 (H2랑은 테스트에서 교체하는 일이 많기도 하고) 왠만해선 피하는 게 좋겠습니다.

NativeQuery 는 언제 쓸까? - 예시를 곁들여서

그럼에도 네이티브 쿼리를 써야 할 일로 두 가지 예시를 들어주셨습니다.

  • 성능상의 문제
  • JPA만으로는 실행할 수 없는 쿼리문

NativeQuery 사용사례 : 성능 문제 해결

한참 전에 배웠던 내용 중에, JPA의 메소드 두 종의 차이점에 대한 내용이 있습니다.

  • deleteAllInBatch() -> 조건 없이 바로 SQL로 몽땅 지웁니다. 빠릅니다.
  • deleteInBatch() -> forEach 와 동일합니다. 느립니다.

아래쪽의 deleteInBatch() 의 경우, JPA 쿼리 계획 과정에서 특정한 ID들을 쿼리하고 그것들만 지우고자 할 때 손쉽게 발생할 수 있습니다. 예시 코드...

@Test
fun findAllSlowTest() {
    putBookWithName("book 1")
    putBookWithName("book 2")
    bookRepo.flush()

    println("-----------------")
    val books = bookRepo.findAll()
    books.forEach { it.category = "myCategory" }
    bookRepo.saveAll(books)
    bookRepo.flush()
    println("=================")

    books.forEach { println(it) }
}

... 를 보면, saveAll() 을 할 때 "where deleted = false" 를 조건으로 하는 모든 객체에 대한 update구문이 나올 것 같이 생겼습니다.

핫하, JPA에게 속으셨군요. 거기까지 최적화해주진 않았습니다. 결과를 보시겠습니다. (미어캣은 또 속았습니다 짤)

Hibernate: 삽입구문 2개
-----------------
Hibernate: 
    select 생략
    from
        book book0_ 
    생략
    where
        book0_.id=? 
        and ( book0_.deleted = false ) 
Hibernate: 
    select 생략
    from
        book book0_ 
    생략
    where
        book0_.id=? 
        and ( book0_.deleted = false ) 
Hibernate: 
    update
        book 
    set
        생략
    where
        id=?
Hibernate: 
    update
        book 
    set
        생략
    where
        id=?
=================
Book(생략)
Book(생략)

id로 조회하고 설정하는 걸 두 번씩 하고있습니다. 전혀 이럴 필요가 없는데 말이죠. 이게 마이그레이션 같은 거에서 1000개가 된다면...

이럴 때 NativeQuery 를 씁니다.

    @Modifying // 없으면 int 반환에서 오류가 납니다. 수정할 때에는 꼭 잊지 맙시다.
    @Query(value = "update book set category = ?1", nativeQuery = true)
    int updateCategoryAll(String category);
//  ^^^
//  업데이트된 row 의 숫자를 반환합니다. aka "applied Rows"

덧붙이자면 @Modifying 은 쿼리의 동작 방식을 바꾼다네요. 문서에서 나와있는 대로 UPDATE, DELETE, INSERT, DDL 에서 꼭 잊지 맙시다.

검색해서 링크 걸기가 귀찮았어요... 시간을 벌써 많이 써서;;;

강의에서 한 트러블슈팅은 테스트 메소드에 @Transactional 이 빠져있는 거였는데, update 시에는 필요하다고 합니다. 꼭 넣어줍시다.

그리고 저는 positional parameter 를 적용을 해봤는데요, 여기서 또 트랜젝션 때문에 트러블슈팅을 했습니다.
https://stackoverflow.com/a/28829942 - 일단 named parameter 는 안 된다고 하구요,
암만 네이티브 쿼리를 날려도 @Transactional 안에 있어서 반영이 되지 않으므로... 외부 서비스 쪽에 넣어줬습니다. 이전에 만들었던 BookService 있죠?

@Service
@RequiredArgsConstructor
public class BookService {
생략
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void prepareTwoBooks() {
        Book book1 = new Book();
        book1.setName("book 1");
        Book book2 = new Book();
        book2.setName("book 2");
        book2.setDeleted(true);
        bookRepo.save(book1);
        bookRepo.save(book2);
        bookRepo.flush();
    }
}

테스트는 위의 내용을 사용합니다. 2번 책은 지운 걸로 표시하는 게 포인트네요.

@Transactional
@Test
open fun updateCategoryAllTest() {
    bookService.prepareTwoBooks()

    println("-----------------")

    bookRepo.updateCategoryAll("my category")
    bookRepo.flush()

    println("=================")

    val books = bookRepo.findAllNativeQuery() // deleted = true 인게 안 나와서 추가
    books.forEach { println(it) }
}

update 쿼리가 한 번 되는거야 이쯤되면 다들 아실테고 결과나 확인해봅시다.

Book(id=1, name=book 1, category=my category, 많이 생략, deleted=false)
Book(id=2, name=book 2, category=my category, 많이 생략, deleted=true)

@Where 의 내용이 무시되는 것도 delete 부분으로 확인할 수 있었습니다.

NativeQuery 사용사례 : 특수한 쿼리

시간이 늦어서 좀 많이 생략합니다.

  • show tables; 같은 JPA로는 안 되는 쿼리를 때릴 때
    @Query(value = "show tables;", nativeQuery = true)
    List<String> showTables();
  • @Enumerated 에 실수로 EnumType.STRING 을 안 넣어줬을 때
    (강사님 말씀으로는 치명적인 문제라서 발견 즉시 마이그레이션을 하므로 사실 이런 용도로는 잘... 이라고 하시네요.)
본 포스팅은 패스트캠퍼스 환급 챌린지 참여를 위해 작성되었습니다.
패스트캠퍼스: https://bit.ly/37BpXiC

#패스트캠퍼스 #패캠챌린지 #직장인인강 #직장인자기계발 #패스트캠퍼스후기 #한번에끝내는JavaSpring웹개발마스터초격차패키지Online

아직 12시 안 넘겼어... 오늘 8시 반에 시작했는데 지금이 몇 시야...