JPA는 엔티티를 가져와서 데이터를 변경하면 변경감지를 해서 엔티티를 하나하나 조회해서 값을 바꾸면
transaction commit 시점에 얘가 바꼇네 하고 update query로 db에 날라간다.(이거는 한건 한건씩 날라가는거)
스프링 데이터 JPA는 한번에 update query를 날릴 수가 있다.
예를 들어 모든 직원의 연봉을 10% 인상하라는 내용은
하나씩 update 하는거 보다는 db에다가 update query를 +10%해서 한번에 commit하는게 효율적이다.
먼저 순수 JPA로 회원의 나이를 변경하는 쿼리를 짜보자.
이 부분에서 inline이 되지 않는다. 해결중(20:20)
(20:22분)
MemberJpaRepository
resultCount에다가 커서를 두고 Inline을 해줘야 한다.
public int bulkAgePlus(int age) {
int resultCount = em.createQuery("update Member m set m.age = m.age + 1" +
"where m.age >= :age")
.setParameter("age", age)
.executeUpdate();
return resultCount;
}
=> Inline Refactoring
public int bulkAgePlus(int age) {
return em.createQuery("update Member m set m.age = m.age + 1" +
"where m.age >= :age")
.setParameter("age", age)
.executeUpdate();
}
MemberJpaRepositoryTest를 실행 시켜보면 update 쿼리가 나간것을 볼 수 있다.
@Test
public void bulkUpdate() {
//given
memberJpaRepository.save(new Member("member1", 10));
memberJpaRepository.save(new Member("member1", 19));
memberJpaRepository.save(new Member("member1", 20));
memberJpaRepository.save(new Member("member1", 21));
memberJpaRepository.save(new Member("member1", 40));
// 각각 +1이 될 것이다.
// when // resultCount는 3개가 나올 것이다.
int resultCount = memberJpaRepository.bulkAgePlus(20);
// then
assertThat(resultCount).isEqualTo(3);
}
update
member
set
age=age+1
where
age>=?
2022-04-17 20:28:23.722 INFO 38987 --- [ main] p6spy : #1650194903722 | took 0ms | statement | connection 3| url jdbc:h2:tcp://localhost/~/datajpa
update member set age=age+1 where age>=?
update member set age=age+1 where age>=20;
h2 db에서 확인을 하면 모두 +1씩 된 것을 확인할 수 있다.
그러면 스프링 데이터 JPA에서는 어떻게 할까
MemberRepositoy
@Modifying
@Query("update Member m set m.age = m.age + 1 where m.age >= :age")
int bulkAgePlus(@Param("age") int age);
MemberRepositoyTest
@Test
public void bulkUpdate() {
//given
memberRepository.save(new Member("member1", 10));
memberRepository.save(new Member("member1", 19));
memberRepository.save(new Member("member1", 20));
memberRepository.save(new Member("member1", 21));
memberRepository.save(new Member("member1", 40));
// when
int resultCount = memberRepository.bulkAgePlus(20);
// then
assertThat(resultCount).isEqualTo(3);
}
실행시키면 아래와 같이 나가게 된다.
update member set age=age+1 where age>=?
update member set age=age+1 where age>=20;
MemberRepository interface에서
여기서 modifying을 빼면 에러가 나야하는데 정상적으로 동작한다... 해결중.
// @Modifying
@Query("update Member m set m.age = m.age + 1 where m.age >= :age")
int bulkAgePlus(@Param("age") int age);
}
MemberRepositoryTest에서 아래 코드를 실행시켰을 대 member5는 40살 41살중에 몇살인지 확인해보면 40인 것을 확인해야 하는데 쿼리 실행을 할 때 age = 40이 나오지 않는다. 해결중... (findListByUsername => findByUsernamed으로 변경)
그래도 아직 미해결....
@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.findListByUsername("member5");
Member member5 = result.get(0);
System.out.println("member5 = " + member5);
// then
assertThat(resultCount).isEqualTo(3);
}
JPA라는 것은 영속성 컨텍스트라는 개념이 있어서 즉 1차캐시에 뭔가 있기 때문에 데이터가 변경된것을 자동으로 찾아서 업데이트가 되는데 스프링 데이터 JPA에서 벌크 연산을 하게되면 db에 한번에 넣어버리고 영속성 컨텍스트는 그걸 모르게 된다.
그래서 벌크연산 이후에는 영속성 컨텍스트를 다 날려버려야 한다.
그래서 MemberRepositoryTest에서 @PersistenceContext를 해주고 flush와 clear를 해주면 age = 41이 나오게 된다.
(아직 해결중..)
// MEemberRepositoryTest
package study.datajpa.repository;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Slice;
import org.springframework.data.domain.Sort;
import org.springframework.test.annotation.Rollback;
import org.springframework.transaction.annotation.Transactional;
import study.datajpa.dto.MemberDto;
import study.datajpa.entity.Member;
import study.datajpa.entity.Team;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
@Transactional
@Rollback(false)
class MemberRepositoryTest {
@Autowired MemberRepository memberRepository;
@Autowired TeamRepository teamRepository;
@PersistenceContext
EntityManager em;
@Test
public void testMember() {
System.out.println("memberRepository = " + memberRepository.getClass());
Member member = new Member("memberA");
Member savedMember = memberRepository.save(member);
Member findMember = memberRepository.findById(savedMember.getId()).get();
assertThat(findMember.getId()).isEqualTo(member.getId());
assertThat(findMember.getUsername()).isEqualTo(member.getUsername());
assertThat(findMember).isEqualTo(member);
}
@Test
public void basicCRUD() {
Member member1 = new Member("member1");
Member member2 = new Member("member2");
memberRepository.save(member1);
memberRepository.save(member2);
// 단건 조회 검증
Member findMember1 = memberRepository.findById(member1.getId()).get();
Member findMember2 = memberRepository.findById(member2.getId()).get();
assertThat(findMember1).isEqualTo(member1);
assertThat(findMember2).isEqualTo(member2);
findMember1.setUsername("member!!!!!!!");
// 리스트 조회 검증
List<Member> all = memberRepository.findAll();
assertThat(all.size()).isEqualTo(2);
// 카운트 검증
long count = memberRepository.count();
assertThat(count).isEqualTo(2);
// 삭제 검증
memberRepository.delete(member1);
memberRepository.delete(member2);
long deleteCount = memberRepository.count();
assertThat(deleteCount).isEqualTo(0);
}
@Test
public void findByUsernameAndAgeGreaterThen() {
Member m1 = new Member("AAA", 10);
Member m2 = new Member("AAA", 20);
memberRepository.save(m1);
memberRepository.save(m2);
List<Member> result = memberRepository.findByUsernameAndAgeGreaterThan("AAA", 15);
assertThat(result.get(0).getUsername()).isEqualTo("AAA");
assertThat(result.get(0).getAge()).isEqualTo(20);
assertThat(result.size()).isEqualTo(1);
}
@Test
public void findHelloBy() {
List<Member> helloBy = memberRepository.findTop3HelloBy();
}
@Test
public void testNamedQuery() {
Member m1 = new Member("AAA", 10);
Member m2 = new Member("BBB", 20);
memberRepository.save(m1);
memberRepository.save(m2);
List<Member> result = memberRepository.findByUsername("AAA");
Member findMember = result.get(0);
assertThat(findMember).isEqualTo(m1);
}
@Test
public void testQuery() {
Member m1 = new Member("AAA", 10);
Member m2 = new Member("BBB", 20);
memberRepository.save(m1);
memberRepository.save(m2);
List<Member> result = memberRepository.findUser("AAA", 10);
Member findMember = result.get(0);
assertThat(findMember).isEqualTo(m1);
}
@Test
public void findUsernameList() {
Member m1 = new Member("AAA", 10);
Member m2 = new Member("BBB", 20);
memberRepository.save(m1);
memberRepository.save(m2);
List<String> usernameList = memberRepository.findUsernameList();
for (String s : usernameList) {
System.out.println("s = " + s);
}
}
@Test
public void findMemberDto() {
Team team = new Team("teamA");
teamRepository.save(team);
Member m1 = new Member("AAA", 10);
m1.setTeam(team);
memberRepository.save(m1);
List<MemberDto> memberDto = memberRepository.findMemberDto();
for (MemberDto dto : memberDto) {
System.out.println("dto = " + dto);
}
}
@Test
public void findByNames() {
Member m1 = new Member("AAA", 10);
Member m2 = new Member("BBB", 20);
memberRepository.save(m1);
memberRepository.save(m2);
List<Member> result = memberRepository.findByNames(Arrays.asList("AAA", "BBB"));
for (Member member : result) {
System.out.println("member = " + member);
}
}
@Test
public void returnType() {
Member m1 = new Member("AAA", 10);
Member m2 = new Member("BBB", 20);
memberRepository.save(m1);
memberRepository.save(m2);
Optional<Member> findMember = memberRepository.findOptionalByUsername("asdasfasDFADSF");
System.out.println("findMember = " + findMember);
}
@Test
public void paging() {
memberRepository.save(new Member("member1", 10));
memberRepository.save(new Member("member2", 10));
memberRepository.save(new Member("member3", 10));
memberRepository.save(new Member("member4", 10));
memberRepository.save(new Member("member5", 10));
int age = 10;
PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "username"));
// when
Page<Member> page = memberRepository.findByAge(age, pageRequest);
Page<MemberDto> toMap = page.map(m -> new MemberDto(m.getId(), m.getUsername(), null));
// then
List<Member> content = page.getContent();
assertThat(content.size()).isEqualTo(3);
assertThat(page.getTotalElements()).isEqualTo(5);
assertThat(page.getNumber()).isEqualTo(0);
assertThat(page.getTotalPages()).isEqualTo(2);
assertThat(page.isFirst()).isTrue();
assertThat(page.hasNext()).isTrue();
}
@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.flush();
em.clear();
List<Member> result = memberRepository.findByUsername("member5");
Member member5 = result.get(0);
System.out.println("member5 = " + member5);
// then
assertThat(resultCount).isEqualTo(3);
}
}
쿼리가 나가고 난 다음에 clear를 자동으로 해주는 게 스프링 데이터 JPA에 존재한다. 그래서 em.clear()를 없애고 memberRepository interface에서 아래와 같이 연산을 넣어주면 똑같이 동작한다.
@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);
JPA와 JDBC template, 순수한 JDBC, Mybatis를 섞어쓸때도 이러한 부분을 조심해야한다.
bulk 연산을 쏜 거랑 Mybatis나 JDBC template처럼 JDBC를 직접날리는거는 JPA가 인식을 못한다. 즉 영속성 컨텍스트에 있는 거랑 안맞기 때문에 이 경우에 flush나 clear 작업을 해줘야 한다.
Mybatis에 query를 날리기 전에 flush를 해주고 많은 데이터가 바뀌면 clear를 해주는 작업이 필요하다.
<출처 김영한: 실전! 스프링 데이터 JPA >
실전! 스프링 데이터 JPA - 인프런 | 강의
스프링 데이터 JPA는 기존의 한계를 넘어 마치 마법처럼 리포지토리에 구현 클래스 없이 인터페이스만으로 개발을 완료할 수 있습니다. 그리고 반복 개발해온 기본 CRUD 기능도 모두 제공합니다.
www.inflearn.com
'Spring > SpringDataJPA' 카테고리의 다른 글
JPA Hint & Lock (0) | 2022.04.17 |
---|---|
@EntityGraph (0) | 2022.04.17 |
스프링 데이터 JPA 페이징과 정렬 (0) | 2022.04.17 |
순수 JPA 페이징과 정렬 (0) | 2022.04.17 |
반환 타입 (0) | 2022.04.17 |