어제에 이어서 실습을 마저 해보고, 하는 김에 소스도 좀 같이 파봅니다.
요점
- 성능차이가 있는 API들을 조심하라: deleteAllById 와 deleteAll( 엔티티 목록 ) 등
- QueryExampleMatcher 는 추가된지도 얼마 안 됐지만 잘 사용되지 않으니 어떻게 쓰는지만 알아보고 넘어가기
- 보통 나중에 배울 Query DSL 을 쓴다
(이런 게 있으면서 나중에 가르쳐준다구요?)
- 보통 나중에 배울 Query DSL 을 쓴다
- Spring Data JPA에 편리한 페이징 객체가 있다
- 소스를 좀 파보면 배우는 게 많다 ("요즘 트렌드가 깔끔한 코드이기도 하고")
- save 함수는 ID 조회로 update를 할지 create를 할지 결정한다 (소스 파봄)
오늘은 샘플 위주여서 스크롤이 좀 깁니다. 스크롤만 기니까...
save 후 flush vs saveAndFlush - 지금 알 수 없다
이름만 봐도 대충 감은 오지만, 아직 이 두 개의 차이를 구분하기 위한 선제 학습 내용을 배우지 않았다네요.
SQL을 봤을 때, 특이한 점은 실제 SQL insert 쿼리는 flush 에서 실행된다는 점이겠네요.
참고: 실습 코드는 여기서부터 아래까지 쭉 하나의 메서드입니다.
@Transactional
@Test
open fun crud_2() {
println("SAVE---------------------------------------------")
userRepo.save(User("asdF", "asdF@email.com")) // 6
println("FLUSH---------------------------------------------")
userRepo.flush()
println("SAVEANDFLUSH---------------------------------------------")
userRepo.saveAndFlush(User("asdF2", "asdF2@email.com")) // 7
userRepo.saveAndFlush(User("asdF3", "asdF3@email.com")) // 8
println("AT_THIS_POINT---------------------------------------------")
userRepo.findAll().forEach { println(it) }
SAVE---------------------------------------------
Hibernate:
call next value for hibernate_sequence
FLUSH---------------------------------------------
Hibernate:
insert
into
user
(created_at, email, name, updated_at, id)
values
(?, ?, ?, ?, ?)
SAVEANDFLUSH---------------------------------------------
Hibernate:
call next value for hibernate_sequence
Hibernate:
insert
into
user
(created_at, email, name, updated_at, id)
values
(?, ?, ?, ?, ?)
Hibernate:
call next value for hibernate_sequence
Hibernate:
insert
into
user
(created_at, email, name, updated_at, id)
values
(?, ?, ?, ?, ?)
AT_THIS_POINT---------------------------------------------
Hibernate:
select
user0_.id as id1_0_,
user0_.created_at as created_2_0_,
user0_.email as email3_0_,
user0_.name as name4_0_,
user0_.updated_at as updated_5_0_
from
user user0_
User(id=1, name=martin, email=martin@fastcampus.co.kr, createdAt=2022-02-11T22:16:32.764389, updatedAt=2022-02-11T22:16:32.764389)
User(id=2, name=martin2, email=martin2@fastcampus.co.kr, createdAt=2022-02-11T22:16:32.770391, updatedAt=2022-02-11T22:16:32.770391)
User(id=3, name=martin3, email=martin3@fastcampus.co.kr, createdAt=2022-02-11T22:16:32.770391, updatedAt=2022-02-11T22:16:32.770391)
User(id=4, name=martin4, email=martin4@fastcampus.co.kr, createdAt=2022-02-11T22:16:32.771389, updatedAt=2022-02-11T22:16:32.771389)
User(id=5, name=martin, email=martin@OwO.co.kr, createdAt=2022-02-11T22:16:32.771389, updatedAt=2022-02-11T22:16:32.771389)
User(id=6, name=asdF, email=asdF@email.com, createdAt=null, updatedAt=null)
User(id=7, name=asdF2, email=asdF2@email.com, createdAt=null, updatedAt=null)
User(id=8, name=asdF3, email=asdF3@email.com, createdAt=null, updatedAt=null)
count
코드 디깅을 했었습니다. "count(*)" 가 왜 나오는지 봤었다는 거죠. 그 이전에 저거보다 더 비효율적인 걸 보여주셨던 거 같은데 잘 기억이...
println("COUNT---------------------------------------------")
println("what count: ${userRepo.count()}")
COUNT---------------------------------------------
Hibernate:
select
count(*) as col_0_0_
from
user user0_
what count: 8
delete
실습용 코드를 짜다가 실수를 저질렀습니다. delete( name=asdF ) 라고 했는데 이러면 이름이 asdF 인 객체가 지워질줄 알았습니다. 분명 강의에서 소스 파보면서 ID를 조회해서 지운다고 확인까지 해주셨는데.
println("DELETE (asdF (*ID6), custom created without ID: won't work)-------------")
userRepo.delete(User().also { it.name = "asdF" }) // 6
println("DELETE_BY_ID (ID: 8)------------------------------------------")
userRepo.deleteById(8)
println("AT_THIS_POINT---------------------------------------------")
userRepo.findAll().forEach { println(it) }
DELETE (asdF (*ID6))--------------------------------------------
DELETE_BY_ID (ID: 8)------------------------------------------
AT_THIS_POINT---------------------------------------------
Hibernate:
delete
from
user
where
id=?
Hibernate:
select
user0_.id as id1_0_,
user0_.created_at as created_2_0_,
user0_.email as email3_0_,
user0_.name as name4_0_,
user0_.updated_at as updated_5_0_
from
user user0_
User(id=1, name=martin, email=martin@fastcampus.co.kr, createdAt=2022-02-11T22:16:32.764389, updatedAt=2022-02-11T22:16:32.764389)
User(id=2, name=martin2, email=martin2@fastcampus.co.kr, createdAt=2022-02-11T22:16:32.770391, updatedAt=2022-02-11T22:16:32.770391)
User(id=3, name=martin3, email=martin3@fastcampus.co.kr, createdAt=2022-02-11T22:16:32.770391, updatedAt=2022-02-11T22:16:32.770391)
User(id=4, name=martin4, email=martin4@fastcampus.co.kr, createdAt=2022-02-11T22:16:32.771389, updatedAt=2022-02-11T22:16:32.771389)
User(id=5, name=martin, email=martin@OwO.co.kr, createdAt=2022-02-11T22:16:32.771389, updatedAt=2022-02-11T22:16:32.771389)
User(id=6, name=asdF, email=asdF@email.com, createdAt=null, updatedAt=null)
User(id=7, name=asdF2, email=asdF2@email.com, createdAt=null, updatedAt=null)
Example 쿼리
코드로 조건을 걸 수 있습니다. 간단한 조건부 쿼리를 작성하는데에는 유용하겠죠. Query DSL 보다 얼마나 더 편하냐가 관건이겠는데... 배워봐야 알겠죠?
강의에서는 withMatcher 말고도 필드를 무시하는 withIgnorePaths("name") 도 쓰셨고, GenericPropertiesMatcher 에 있는 endsWith() 을 클래스 이름을 안 적고도 (qualifying 없이) 쓸 수 있게 함수만 딸랑 임포트해서 사용하셨습니다. 어째서인지 kotlin 에서는 안 돼서 왜 import가 안 되지? 그러다가 그냥 패러미터 타입을 보고 적당히 찾아왔다죠.
방법이 있겠죠 머 ㅎㅎ
println("QUERY_BY_EXAMPLE--------------------------------------")
val matcher: ExampleMatcher = ExampleMatcher.matching()
.withMatcher("email", ExampleMatcher.GenericPropertyMatchers.exact())
val example: Example<User> = Example.of(User().also { it.email = "asdF3@email.com" }, matcher)
Assertions.assertEquals(
0,
// userRepo.count(example)
userRepo.findAll(example).size
)
QUERY_BY_EXAMPLE--------------------------------------
Hibernate:
select
user0_.id as id1_0_,
user0_.created_at as created_2_0_,
user0_.email as email3_0_,
user0_.name as name4_0_,
user0_.updated_at as updated_5_0_
from
user user0_
where
user0_.email=?
Update
ID가 있으면 save 함수에서 업데이트로 동작합니다. 코드로 살짝 들어가보면 나오는데... SimpleJpaRepository 입니다.
// 이 코드는 Spring Data JPA 의 일부입니다.
// Spring Data JPA의 저작권을 따릅니다.
생략
package org.springframework.data.jpa.repository.support;
생략
public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T, ID> {
생략
@Transactional @Override
public <S extends T> S save(S entity) {
생략
if (entityInformation.isNew(entity)) { // <- 파들어가면 ID를 확인합니다.
em.persist(entity);
return entity;
} else {
return em.merge(entity);
}
}
생략
println("UPDATE--------------------------------------")
userRepo.saveAndFlush(User().also { it.id = 7; it.name = "AsDF222" })
userRepo.findById(7).also { println(it) }
UPDATE--------------------------------------
Hibernate:
update
user
set
created_at=?,
email=?,
name=?,
updated_at=?
where
id=?
Optional[User(id=7, name=AsDF222, email=null, createdAt=null, updatedAt=null)]
deleteAll vs deleteAllInBatch
"DB에 접근할 때에는 항상 성능 문제를 고려하라. 궁금하면 소스를 보라." 뭐 대충 이런 내용이었습니다.
상식적으로 생각해봐도 delete 할 때에는 ID만으로 하는게 제일 빠르겠죠?
소스를 파보셨는데,
- deleteAll 은 내부에서 foreach 문을 돌아서 n개의 delete 쿼리가 발생하고,.
- deleteAllInBatch 는 그렇지 않다네요.
직접 해보니, flush() 가 없으면 쿼리가 다음에 실행이 필요한 시점까지 밀리므로 공부용으로는 꼭 flush 를 넣어주는 게 좋습니다. (프로덕션 코드에 이런 거 넣으면 당연히 안 되지;) 아래 코드에서 deleteAll() 의 쿼리는 delete 가 두 번 실행되는 걸 확인하실 수 있습니다.
println("DELETE_ALL(FIND_ALL_BY_ID)---------------------------")
userRepo.deleteAll(userRepo.findAllById(arrayListOf(1, 2)))
userRepo.flush()
println("DELETE_ALL_IN_BATCH(FIND_ALL_BY_ID)--------------------------------")
// userRepo.deleteInBatch(arrayListOf(3, 4)) // deprecated, use deleteAllInBatch
userRepo.deleteAllInBatch(userRepo.findAllById(arrayListOf(3, 4)))
userRepo.flush()
DELETE_ALL(FIND_ALL_BY_ID)---------------------------
Hibernate:
select
user0_.id as id1_0_,
user0_.created_at as created_2_0_,
user0_.email as email3_0_,
user0_.name as name4_0_,
user0_.updated_at as updated_5_0_
from
user user0_
where
user0_.id in (
? , ?
)
Hibernate:
delete
from
user
where
id=?
Hibernate:
delete
from
user
where
id=?
DELETE_ALL_IN_BATCH(FIND_ALL_BY_ID)--------------------------------
Hibernate:
select
user0_.id as id1_0_,
user0_.created_at as created_2_0_,
user0_.email as email3_0_,
user0_.name as name4_0_,
user0_.updated_at as updated_5_0_
from
user user0_
where
user0_.id in (
? , ?
)
Hibernate:
delete
from
user
where
id=?
or id=?
Paging
Page 객체가 있습니다. 개발자가 매우 행복한 삶을 살 수 있죠. (아님 말구유~ 몰?루) 패키지 출처는 역시 Spring Data JPA 쪽.
쿼리는 지금 정리하면서 처음으로 자세히 봤는데요, 페이지 객체를 만드는 시점에 한 번 쫙 조회를 하긴 하는 모양이네요.
println("PAGE--------------------------------")
val page: Page<User> = userRepo.findAll(PageRequest.of(1, 3))
println(" - totalPages ${page.totalPages}")
println(" - totalElements ${page.totalElements}")
println(" - numberOfElements ${page.numberOfElements}")
println(" - sort ${page.sort}")
println(" - size ${page.size}")
page.content.forEach {
println(" - content $it")
}
}
PAGE--------------------------------
Hibernate:
select
user0_.id as id1_0_,
user0_.created_at as created_2_0_,
user0_.email as email3_0_,
user0_.name as name4_0_,
user0_.updated_at as updated_5_0_
from
user user0_ limit ? offset ?
Hibernate:
select
count(user0_.id) as col_0_0_
from
user user0_
- totalPages 1
- totalElements 3
- numberOfElements 0
- sort UNSORTED
- size 3
SampleJpaRepository 소스 파보기
- 위에서 설명했던 save가 어떻게 insert 혹은 update 를 고르느냐랑,
- save 함수에 @Transactional 이 달려있어서 개발자가 안 넣었으면 save 안에서 알아서 트랜젝션이 생긴다라던가,
- deleteAll이랑 deleteAllInBatch 가 foreach 로 도는지 아닌지 차이임을 소스로 본다던가
하는 일들이 있었습니다. 잘 만들어진 남의 코드를 보면 많은 걸 배우는 건 당연한 일이고, 요새는 개발하면서 라이브러리를 파들어가보기에도 좋으니까 이런 강의도 좋은 거 같습니다.
본 포스팅은 패스트캠퍼스 환급 챌린지 참여를 위해 작성되었습니다.
패스트캠퍼스: https://bit.ly/37BpXiC
#패스트캠퍼스 #패캠챌린지 #직장인인강 #직장인자기계발 #패스트캠퍼스후기 #한번에끝내는JavaSpring웹개발마스터초격차패키지Online
이거 코드 빼고 공백 빼면 500자 넘을까...? 대충 넘는듯 ㅎㅎ
와 잠깐 8시 30분부터 했다고 OneNote 야??? 지금이 10:49 인데??
'FastCampus - 한번에 끝내는 Java|Spring 웹 개발 > 04 JPA' 카테고리의 다른 글
JPA Ch 3 Query Method 소팅 / 페이징 - 패스트캠퍼스 챌린지 22일차 (0) | 2022.02.14 |
---|---|
JPA Ch 3 Query Method (2~3) - 패스트캠퍼스 챌린지 21일차 (0) | 2022.02.13 |
JPA Ch 3 Query Method (1) - 패스트캠퍼스 챌린지 20일차 (0) | 2022.02.12 |
JPA Ch 2 SprDatJPA 기초 - 패스트캠퍼스 챌린지 18일차 (0) | 2022.02.10 |
JPA Ch 1 인트로 - 패스트캠퍼스 챌린지 17일차 (0) | 2022.02.09 |