다행히도 어제 배운 내용을 까먹지는 않았습니다. 아니, 적어도 다음에 배울 게 뭐였는지는 안 까먹었습니다. 그래서 오늘의 주제는 다음 두 가지입니다.
- @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시 반에 시작했는데 지금이 몇 시야...
'FastCampus - 한번에 끝내는 Java|Spring 웹 개발 > 04 JPA' 카테고리의 다른 글
JPA Ch 10 @Embeddable - 패스트캠퍼스 챌린지 41일차 (0) | 2022.03.05 |
---|---|
JPA Ch 9 @Converter - 패스트캠퍼스 챌린지 40일차 (0) | 2022.03.04 |
JPA Ch 9 @Query (1, 2) - 패스트캠퍼스 챌린지 38일차 (0) | 2022.03.02 |
JPA Ch 8 remove cascade / soft delete - 패스트캠퍼스 챌린지 37일차 (0) | 2022.03.01 |
JPA Ch 8 영속성 전이 (Cascade) - 패스트캠퍼스 챌린지 36일차 (0) | 2022.02.28 |