이 어노테이션을 활용하게 되면 같은 트랜잭션 안에 있는 동작들에 대해서 엔티티의 동일성을 보장하게 된다. 따라서 테스트 클래스에서 테스트를 진행할 경우에도 find로 엔티티를 찾아와서 assertJ를 활용해 isEqualTo로 동일한지 확인을 하면 true로 동일하다는 것을 알 수 있다.
Jpa에서는 커밋하기 전, 스냅샷으로 처음 찍어둔 영속성 컨텍스트의 내부와 커밋하기 직전의 내용을 비교해서 변경된 부분이 있으면 변경감지(더티체킹)를 해서 해당 내용을 함께 DB에 커밋해준다. 따라서 UPDATE와 관련된 메서드는 리포지토리에서 제외해도 된다.
JpaRepository 인터페이스를 상속한 인터페이스에 대해 Spring Data JPA가 구현 클래스를 생성하게 된다. 이렇게 구현된 클래스는 스캔대상이 되며 xxRepository 인터페이스만으로도 동작하는 것처럼 보이게 되는 것이다. 실제로 XXRepository를 출력해보면 class com.sun.proxy.$ProxyXXX
이 출력된다.
JpaRepository를 상속하는 인터페이스는 @Repository를 생략해도 된다. 자동으로 컴포넌트 스캔을 스프링 데이터 JPA가 자동으로 처리하기 때문이다.
ex) findByUsernameAndAge :: username과 age가 where문에 사용된다.
ex) findByUsernameAndAgeGreaterThan :: username과 age를 where문에 사용하되 age는 ~보다 커야 한다는 조건을 갖는다.
조회 :: find...By, read...By, query...By, get...By
예:) findHelloBy 처럼 ...에식별하기위한내용(설명)이 들어가도된다.
[공식문서](https://docs.spring.io/spring-data/jpa/docs/current/reference/html/ #repositories.query-methods.query-creation)
COUNT :: count...By 반환타입 long
EXISTS :: exists...By 반환타입은 boolean
삭제 :: delete...By, remove...By 반환타입은 long
DISTINCT :: findDistinct, findMemberDistinct, ...
LIMIT :: findFirst3, findFirst, findTop ...
[공식문서](https://docs.spring.io/spring-data/jpa/docs/current/reference/html/ #repositories.limit-query-result)
이 기능은 엔티티의 필드명이 변경될 경우 인터페이스에서 정의한 메서드 이름도 꼭 변경해야한다. 그렇지 않으면 애플리케이션 시작 시점에 컴파일러 오류가 발생한다. (큰 강점)
단점 :: 길어지면 답이 없다
List<Member> findByUsernameAndAgeGreaterThan(String username, int age);
@Test
public void findByUsernameAndAgeGreaterThan(){
Member m1 = new Member("AAA", 10);
Member m2 = new Member("BBB", 20);
memberRepository.save(m1);
memberRepository.save(m2);
List<Member> result = memberRepository.findByUsernameAndAgeGreaterThan("BBB", 15);
assertThat(result.get(0).getUsername()).isEqualTo("BBB");
assertThat(result.get(0).getAge()).isEqualTo(20);
assertThat(result.size()).isEqualTo(1);
}
NamedQuery 기법이 무엇이냐 하면 아래처럼 작성해서 이름을 부여하고 그걸 계속해서 활용하는 방식이라고 한다.
하지만, JpaRepository에서 또 이 NamedQuery를 호출하기 위해 @Query(name = Member.findByUsername) 을 작성해야하고 다음에 소개할 3번을 사용하면 되기 때문에 실무에서는 잘 사용하지 않는다.
이 방법은 NamedQuery의 장점(애플리케이션 구동 시점에 잘못 작성한 오타 검수 가능)와 리포지토리 인터페이스에 직접 쿼리를 작성할 수 있다는 장점을 가진 방법으로, 많이 사용된다.
@Query("select m from Member m where m.username = :username and m.age = :age")
List<Member> findUser(@Param("username") String username, @Param("age") int age);
@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);
assertThat(result.get(0)).isEqualTo(m1);
}
+++++추가적으로 @Query를 이용해서 JPQL을 작성하는 경우에 대해 예시를 한두개 살펴보면 다음과 같다.
@Query("select m.username from Member m")
List<String> findUsernameList();
@Query("select new study.datajpa.dto.MemberDto(m.id, m.username, t.name) from Member m join m.team t")
List<MemberDto> findMemberDto();
이렇게 원하는 String으로 반환하게 할 수도 있고, DTO로 직접 조회해서 new 연산자를 사용해 DTO에 딱 맞게 구할 수도 있다. 관련된 테스트 내용은 아래와 같이 작성한다.
@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);
memberRepository.save(m1);
m1.setTeam(team);
List<MemberDto> memberDto = memberRepository.findMemberDto();
for (MemberDto s : memberDto) {
System.out.println("dto = "+s);
}
}
import org.springframework.data.repository.query.Param
public interface MemberRepository extends JpaRepository<Member, Long> {
@Query("select m from Member m where m.username = :name")
Member findMembers(@Param("name") String username);
}
@Query("select m from Member m where m.username in :names")
List<Member> findByNames(@Param("names") List<String> names);
이렇게 사용할 경우 Collection 타입으로 이 컬렉션에 해당하는 것만 추출되어 반환하게 된다.
컬렉션의 경우
결과가 없으므로 빈 컬렉션을 반환한다. 따로 null로 저장되진 않는다.
단건 조회의 경우
결과가 없으면 :: null반환
결과가 2건 이상이면 :: javax.persistence.NonUniqueResultException
예외 발생한다.
public List<Member> findByPage(int age, int offset, int limit) {
return em.createQuery("select m from Member m where m.age = :age order by m.username desc")
.setParameter("age", age)
.setFirstResult(offset).setMaxResults(limit)
.getResultList();
}
public long totalCount(int age) {
return em.createQuery("select count(m) from Member m where m.age = :age", Long.class)
.setParameter("age", age)
.getSingleResult();
}
@Test
public void paging() throws Exception {
//given
memberJpaRepository.save(new Member("member1", 10));
memberJpaRepository.save(new Member("member2", 10));
memberJpaRepository.save(new Member("member3", 10));
memberJpaRepository.save(new Member("member4", 10));
memberJpaRepository.save(new Member("member5", 10));
int age = 10;
int offset = 0;
int limit = 3;
//when
List<Member> members = memberJpaRepository.findByPage(age, offset, limit);
long totalCount = memberJpaRepository.totalCount(age);
//페이지 계산 공식 적용...
// totalPage = totalCount / size ...
// 마지막 페이지 ...
// 최초 페이지 ..
//then
assertThat(members.size()).isEqualTo(3);
assertThat(totalCount).isEqualTo(5);
}
org.springframework.data.domain.Sort
: 정렬기능org.springframework.data.domain.Pageable
: 페이징기능 (내부에 Sort 포함) org.springframework.data.domain.Page
: 추가 count 쿼리결과를 포함하는페이징org.springframework.data.domain.Slice
: 추가 count 쿼리없이 다음페이지만 확인가능(내부적 으로 limit + 1조회)Page<Member> findByUsername(String name, Pageable pageable); //count 쿼리 사용
Slice<Member> findByUsername(String name, Pageable pageable); //count 쿼리 사용 안
함
List<Member> findByUsername(String name, Pageable pageable); //count 쿼리 사용 안
함
List<Member> findByUsername(String name, Sort sort);
Page<Member> findByAge(int age, Pageable pageable);
여기에서 두번째 파라미터로 받은 Pageable은 인터페이스로, 실제 사용시 해당 인터페이스를 구현한 PageRequest 객체를 사용해 넣어주면 된다.
//페이징 조건과 정렬 조건 설정
@Test
public void page() throws Exception {
//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));
//when
PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC,
"username"));
Page<Member> page = memberRepository.findByAge(10, pageRequest);
//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(); //다음 페이지가 있는가?
}
PageRequest 생성자의첫번째 파라미터에는 현재 페이지를, 두번째 파라미터에는 조회할 데이터 수를 입력한다. 여기에 추가로 정렬 정보 도 파라미터로 사용할 수 있다. 참고로 페이지는 0부터시작한다.
count쿼리는 CountQuery를 사용해서 따로 분리해 사용할 수 있다. countQuery를 사용하게 되면, left join이나 right join으로 인해 count 값이 계속 기다려야하는 문제를 해결할 수 있기 때문에 실무에서 사용하기도 한다.
int age = 10;
PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "username"));
Page<Member> page = memberRepository.findByAge(age, pageRequest);
Page<MemberDto> dtoPage = page.map(m -> new MemberDto(m.getId(), m.getUsername(), m.getTeam().getName()));
page(엔티티)를 바로 컨트롤러에 제공하면 엔티티가 노출되는 위험이 있기 때문에 DTO로 변환해서 제공하도록 한다. 여기서는 map을 활용하면 편리하다.