Web/JPA

[Data JPA] EP 3. 쿼리 메소드 기능 -2

 

 

 

벌크성 수정쿼리

 

벌크성 수정쿼리는 영속성 컨텍스트를 거치지 않고 바로 DB에 쿼리를 날려 update를 하는 쿼리입니다.

 

예를들어 "어떤 나이 이상인 사람들의 나이를 전부 1살 올려라" 라는 요구사항을 구현할때 굳이 애플리케이션 로직에서 수행하지 않고 DB에 직접 쿼리를 날리는 것이 효율적일 때가 있습니다.

 

그럴때 사용하는 것이 벌크성 수정쿼리입니다.

 

 

@Query에다가 직접 update쿼리를 작성합니다.

보통 벌크성 수정쿼리는 반환값으로 resultList가 아닌 수정된 데이터의 개수를 받습니다.

그럴 때 메서드에@Modifying을 적어주면 getResultList()가 아닌 executeUpdate()를 호출해서 수정된 데이터의 개수를 받을 수 있습니다.

 

    @Modifying
    @Query("update Member m set m.age = m.age + 1 where m.age >= :age")
    int bulkAgePlus(@Param("age") int age);
    @Test
    public void bulkUpdate() {

        //given
        memberRepository.save(new Member("member1", 10));
        memberRepository.save(new Member("member2", 19));
        memberRepository.save(new Member("member3", 20));
        memberRepository.save(new Member("member4", 21));
        memberRepository.save(new Member("member5", 40));

        // when
        int resultCount = memberRepository.bulkAgePlus(20);

        //then
        assertThat(resultCount).isEqualTo(3);
    }

 

 

 

 

 

주의할 점

 

벌크연산을 할 때 꼭 신경써주어야 하는 부분이 있습니다.

벌크연산은 JPA의 영속성 컨텍스트의 관리를 받지 않고 DB에 직접 쿼리를 날리기 때문에 현재 영속성 컨텍스트의 데이터와 DB의 데이터가 다를 수 있습니다.

 

벌크연산을 하고 member5의 나이를 출력해보겠습니다.

벌크연산을 했으므로 member5의 나이는 41로 출력되어야 합니다.

    @Test
    public void bulkUpdate() {

        //given
        memberRepository.save(new Member("member1", 10));
        memberRepository.save(new Member("member2", 19));
        memberRepository.save(new Member("member3", 20));
        memberRepository.save(new Member("member4", 21));
        memberRepository.save(new Member("member5", 40));

        // when
        int resultCount = memberRepository.bulkAgePlus(20);

        List<Member> result = memberRepository.findByUsername("member5");
        Member member5 = result.get(0);
        System.out.println("member5 = " + member5);

        //then
        assertThat(resultCount).isEqualTo(3);
    }

 

 

member5의 나이가 40으로 출력됩니다.

 

 

 

 

그 이유는 JPA에서 영속성 컨텍스트에 값이 있으면 그 값을 캐시로 바로 가져오는데, 벌크 연산은 영속성컨텍스트를 거치지 않고 바로 DB에 업데이트하기 때문에 영속성 컨텍스트의 값과 DB의 값이 다르기 때문입니다.

2021.02.03 - [Web/JPA] - EP2. JPA 기본동작과정과 내부동작방식

 

EP2. JPA 기본동작과정과 내부동작방식

JPA 기본 동작 과정 Entity Manager Factory 에서 Entity Manager 를 만들고 Transaction 안에서 Entity Manager를 이용해 객체와 테이블을 매핑, 수정, 삭제를 할 수 있다. 객체 수정시에 member.setId(1L); 을..

ksabs.tistory.com

2021.02.03 - [Web/JPA] - EP3. 영속성 컨텍스트, 엔티티 매핑

 

EP3. 영속성 컨텍스트, 엔티티 매핑

em.flush(); 를 하면 영속성 컨텍스트의 변경내용을 데이터베이스에 동기화 한다. (sql저장소에 있던 sql문들이 db로 날아간다.) 영속성 컨텍스트를 비우는 것은 아님! 커밋을 실행하면 : flush()를 자동

ksabs.tistory.com

 

 

 

해결방법

벌크연산을 한 후 영속성 컨텍스트를 clear 해줍니다.

그러면 영속성 컨텍스트에 아무 것도 없기 때문에 DB에서 데이터를 가져와 영속성 컨텍스트에 담기때문입니다.

 

    @Test
    public void bulkUpdate() {

        //given
        memberRepository.save(new Member("member1", 10));
        memberRepository.save(new Member("member2", 19));
        memberRepository.save(new Member("member3", 20));
        memberRepository.save(new Member("member4", 21));
        memberRepository.save(new Member("member5", 40));

        // when
        int resultCount = memberRepository.bulkAgePlus(20);
        em.clear();

        List<Member> result = memberRepository.findByUsername("member5");
        Member member5 = result.get(0);
        System.out.println("member5 = " + member5);

        //then
        assertThat(resultCount).isEqualTo(3);
    }

 

 

 

또한,

em.clear()를 직접 해주지않고 modifying에 옵션을 주어도 됩니다.

 

@Modifying(clearAutomatically = true)

이 옵션을 주면 벌크연산이 끝난 후 알아서 영속성 컨텍스트를 비워줍니다.

 

    @Modifying(clearAutomatically = true)
    @Query("update Member m set m.age = m.age + 1 where m.age >= :age")
    int bulkAgePlus(@Param("age") int age);

 

 

 

 

 

성능최적화 페치조인

 

@ManyToOne 관계에서 N+1문제를 해결하기 위해 fetch join을 사용합니다.

 

페치조인 성능최적화에 대한 자세한 설명은 아래 링크에 정리되어있습니다.

2021.04.06 - [Web/JPA] - EP2. 지연 로딩과 조회 성능 최적화

 

EP2. 지연 로딩과 조회 성능 최적화

주문을 조회하는 API를 설계해보았다. package jpabook.jpashop.api; import jpabook.jpashop.domain.Order; import jpabook.jpashop.repository.OrderRepository; import jpabook.jpashop.repository.OrderSearch..

ksabs.tistory.com

 

 

 

@Query 이용

@Query에 직접 fetch join jpql을 적어도 된다.

    @Query("select m from Member m left join fetch m.team")
    List<Member> findMemberFetchJoin();
    @Test
    public void findMemberLazy() throws Exception {
        //given
        //member1 -> teamA
        //member2 -> teamB

        Team teamA = new Team("teamA");
        Team teamB = new Team("teamB");
        teamRepository.save(teamA);
        teamRepository.save(teamB);
        Member member1 = new Member("member1", 10, teamA);
        Member member2 = new Member("member2", 10, teamB);
        memberRepository.save(member1);
        memberRepository.save(member2);

        em.flush();
        em.clear();

        //when

        //select Member
        List<Member> members = memberRepository.findMemberFetchJoin();

        for (Member member : members) {
            System.out.println("member = " + member.getUsername());
            System.out.println("member.getTeam().getClass() = " + member.getTeam().getClass());
            System.out.println("member.team = " + member.getTeam().getName());
        }
    }

 

프록시 객체를 가져오지 않고 바로 member와 연관된 team을 가져오는 것을 볼 수 있다.

 

 

 

@EntityGraph(attributePaths = {"team"})

 

1. findAll() override

Data JPA의 findAll을 오버라이딩 받아서 @EntityGraph에서 attributePaths = {"team"}을 해주면 알아서 team에 fetch join을 걸어줍니다.

 

    @Override
    @EntityGraph(attributePaths = {"team"})
    List<Member> findAll();

 

 

 

2. @Query jpql

@Query에서는 Member를 가져오는 jpql만 적고 @EntityGraph을 이용해 fetch join으로 team을 가져오는 방법도 있습니다.

    @EntityGraph(attributePaths = {"team"})
    @Query("select m from Member m")
    List<Member> findMemberEntityGraph();

 

 

 

사용팁

  • 굳이 직접 jpql을 치지 않아도 되는 간단한것들은 EntityGraph을 이용하고,
  • 복잡한 쿼리를 직접 작성해야 하는 것들은 @Query에서 jpql로 fetch join을 쓰면됩니다.

 

 

 

 

 

 

JPA hint

 

변경감지 기능을 사용할 때의 메모리 비용이 많이 드는 단점이 있습니다.

  1. 변경감지를 해야하므로 원본을 가지고 있어야 해서 데이터를 2개 가지고 있어야 한다.
  2. 변경체크기능을 수행하야한다.

 

그런데 JPA는 변경을 위해 엔티티를 가져오지 않았더라도, 변경감지 기능을 위해 데이터를 두개(원본포함)을 가지고 있습니다.

 

그래서 조회용으로 가져오기 위해 JPA가 아닌 Hibernate에서 최적화 기능을 제공합니다.

이것이 JPA Hint 입니다

 

 

@QueryHints 안에 value 값으로 @QueryHint를 넣어줍니다.

@QueryHint 안에는 readOnly 옵션을 넣어줍니다.

    @QueryHints(value = @QueryHint(name = "org.hibernate.readOnly", value = "true"))
    Member findReadOnlyByUsername(String username);

 

 

readOnly로 가져오면 변경감지를 사용하지 못합니다.

    @Test
    public void queryHint() throws Exception {
        //given
        Member member1 = memberRepository.save(new Member("member1", 10));
        em.flush();
        em.clear();

        //when
        Member findMember = memberRepository.findReadOnlyByUsername("member1");
        findMember.setUsername("member2");

        em.flush();

        //then
    }

 

setUsername으로 member1 -> member2 로 바꿨는데도 update문이 동작하지 않습니다.

 

 

 

사실 실무에서 트래픽은 readOnly로 최적화할 수 있는건 몇프로 안됩니다.

실제 트래픽을 많이 먹는것을 복잡한 query가 동작할 때 입니다.

그래서 이 기능은 성능 테스트를 해보고 유의미한 성능 최적화를 얻을 수 있을때 사용하면 됩니다.

 

 

 

 

 

Lock

JPA에서 제공하는 DB에서 select할때  다른데서 손 못대도록 미리 lock걸어 가져오는 방법입니다.

 

    @Lock(LockModeType.PESSIMISTIC_WRITE)
    List<Member> findLockByUsername(String name);

 

 

쿼리가 for update 로 가져오는 것을 볼 수 있습니다.

    @Test
    public void lock() throws Exception {
        //given
        Member member1 = memberRepository.save(new Member("member1", 10));
        em.flush();
        em.clear();

        //when
        List<Member> result = memberRepository.findLockByUsername("member1");

        //then
    }