본문 바로가기

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

JPA Ch 2 SprDatJPA 기초 (2) - 패스트캠퍼스 챌린지 19일차

어제에 이어서 실습을 마저 해보고, 하는 김에 소스도 좀 같이 파봅니다.

요점

  • 성능차이가 있는 API들을 조심하라: deleteAllById 와 deleteAll( 엔티티 목록 ) 등
  • QueryExampleMatcher 는 추가된지도 얼마 안 됐지만 잘 사용되지 않으니 어떻게 쓰는지만 알아보고 넘어가기
    • 보통 나중에 배울 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 인데??