This commit is contained in:
haerong22
2021-09-02 14:19:40 +09:00
5 changed files with 189 additions and 2 deletions

View File

@@ -12,6 +12,7 @@ import javax.persistence.*;
name="Member.findByUsername",
query="select m from Member m where m.username = :username"
)
@NamedEntityGraph(name = "Member.all", attributeNodes = @NamedAttributeNode("team"))
public class Member {
@Id @GeneratedValue

View File

@@ -68,4 +68,10 @@ public class MemberJpaRepository {
.setParameter("age", age)
.getSingleResult();
}
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();
}
}

View File

@@ -2,10 +2,14 @@ package com.example.springdatajpa.repository;
import com.example.springdatajpa.dto.MemberDto;
import com.example.springdatajpa.entity.Member;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.data.jpa.repository.*;
import org.springframework.data.repository.query.Param;
import javax.persistence.LockModeType;
import javax.persistence.QueryHint;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
@@ -32,4 +36,36 @@ public interface MemberRepository extends JpaRepository<Member, Long> {
List<Member> findListByUsername(String username); // 컬렉션
Member findMemberByUsername(String username); // 단건
Optional<Member> findOptionalByUsername(String username); // Optional
@Query(value = "select m from Member m left join m.team t",
countQuery = "select count(m) from Member m")
Page<Member> findPageByAge(int age, Pageable pageable);
Slice<Member> findSliceByAge(int age, Pageable pageable);
// @Modifying(clearAutomatically = true)
@Modifying
@Query("update Member m set m.age = m.age + 1 where m.age >= :age")
int bulkAgePlus(@Param("age") int age);
@Query("select m from Member m left join fetch m.team")
List<Member> findMemberFetchJoin();
@Override
// @EntityGraph(attributePaths = {"team"})
List<Member> findAll();
@EntityGraph(attributePaths = {"team"})
@Query("select m from Member m")
List<Member> findMemberEntityGraph();
// @EntityGraph(attributePaths = {"team"})
@EntityGraph("Member.all")
List<Member> findEntityGraphByUsername(@Param("username") String username);
@QueryHints(value = @QueryHint(name = "org.hibernate.readOnly", value = "true"))
Member findReadOnlyByUsername(String username);
@Lock(LockModeType.PESSIMISTIC_WRITE)
Member findLockByUsername(String username);
}

View File

@@ -106,4 +106,20 @@ class MemberJpaRepositoryTest {
assertEquals(3, members.size());
assertEquals(5, totalCount);
}
@Test
void bulkUpdate() {
// given
memberJpaRepository.save(new Member("member1", 10));
memberJpaRepository.save(new Member("member2", 19));
memberJpaRepository.save(new Member("member3", 21));
memberJpaRepository.save(new Member("member4", 33));
memberJpaRepository.save(new Member("member5", 40));
// when
int resultCount = memberJpaRepository.bulkAgePlus(20);
// then
assertEquals(3, resultCount);
}
}

View File

@@ -7,9 +7,15 @@ import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.dao.IncorrectResultSizeDataAccessException;
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 javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
@@ -25,6 +31,8 @@ public class MemberRepositoryTest {
MemberRepository memberRepository;
@Autowired
TeamRepository teamRepository;
@PersistenceContext
EntityManager em;
@Test
void testMember() {
@@ -178,5 +186,125 @@ public class MemberRepositoryTest {
assertThrows(IncorrectResultSizeDataAccessException.class,
() -> memberRepository.findOptionalByUsername("ddd")); // 반환 타입은 단건 인데 여러 건이 조회 되었을 경우 예외 발생
}
@Test
void paging() {
// given
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.findPageByAge(age, pageRequest);
Page<MemberDto> toMap = page.map(member -> new MemberDto(member.getId(), member.getUsername(), null));
// then
List<Member> members = page.getContent();
assertEquals(3, members.size());
assertEquals(5, page.getTotalElements());
assertEquals(0, page.getNumber());
assertEquals(2, page.getTotalPages());
assertTrue(page.isFirst());
assertTrue(page.hasNext());
Slice<Member> slice = memberRepository.findSliceByAge(age, pageRequest);
List<Member> content = page.getContent();
assertEquals(3, content.size());
assertEquals(0, slice.getNumber());
assertTrue(slice.isFirst());
assertTrue(slice.hasNext());
}
@Test
void bulkUpdate() {
// given
memberRepository.save(new Member("member1", 10));
memberRepository.save(new Member("member2", 19));
memberRepository.save(new Member("member3", 21));
memberRepository.save(new Member("member4", 33));
memberRepository.save(new Member("member5", 40));
// when
int resultCount = memberRepository.bulkAgePlus(20); // 벌크 연산에서는 영속성 컨텍스트에 관계없이 DB에 저장된다.
Member member5 = memberRepository.findMemberByUsername("member5");
assertEquals(40, member5.getAge()); // age 가 41이 아닌 40 이다. 영속성 컨텍스트에 아직 member5의 데이터가 남아있기 때문
em.flush(); // 커밋
em.clear(); // 영속성 컨텍스트를 직접 삭제 하거나 @Modifying(clearAutomatically = true) 옵션을 준다.
member5 = memberRepository.findMemberByUsername("member5");
assertEquals(41, member5.getAge()); // 영속성 컨텍스트에 캐시가 남아있지 않으므로 DB에서 가져온다.
// then
assertEquals(3, resultCount);
}
@Test
void findMemberLazy() {
// 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
List<Member> members = memberRepository.findAll();
members.forEach(member -> {
System.out.println("member.getUsername() = " + member.getUsername());
System.out.println("member.getTeam().getClass() = " + member.getTeam().getClass()); // Lazy 로딩 이므로 프록시 객체
System.out.println("member.getTeam().getName() = " + member.getTeam().getName()); // 이때 실제로 team 데이터 조회 ( N + 1 문제 발생 )
});
// fetch join 으로 해결 -> 한 번에 team 데이터도 모두 조회
// @EntityGraph(attributePaths = {"team"}) 사용 가능
List<Member> memberFetchJoin = memberRepository.findMemberFetchJoin();
memberFetchJoin.forEach(member -> {
System.out.println("member.getUsername() = " + member.getUsername());
System.out.println("member.getTeam().getClass() = " + member.getTeam().getClass());
System.out.println("member.getTeam().getName() = " + member.getTeam().getName()); // fetch join
});
}
@Test
void queryHint() {
// given
Member member1 = new Member("member1", 10);
memberRepository.save(member1);
em.flush();
em.clear();
// when
Member findMember = memberRepository.findReadOnlyByUsername("member1");
findMember.setUsername("member2");// read only 옵션으로 변경 감지 X
em.flush();
}
@Test
void lock() {
// given
Member member1 = new Member("member1", 10);
memberRepository.save(member1);
em.flush();
em.clear();
// when
Member findMember = memberRepository.findLockByUsername("member1");
}
}