[스프링 데이터 JPA] 중요 요약본

sonnng·2023년 11월 20일
0

Spring

목록 보기
31/41
post-thumbnail

@Transactional

이 어노테이션을 활용하게 되면 같은 트랜잭션 안에 있는 동작들에 대해서 엔티티의 동일성을 보장하게 된다. 따라서 테스트 클래스에서 테스트를 진행할 경우에도 find로 엔티티를 찾아와서 assertJ를 활용해 isEqualTo로 동일한지 확인을 하면 true로 동일하다는 것을 알 수 있다.

Jpa에서의 update는 쓰지 않아도 된다.

Jpa에서는 커밋하기 전, 스냅샷으로 처음 찍어둔 영속성 컨텍스트의 내부와 커밋하기 직전의 내용을 비교해서 변경된 부분이 있으면 변경감지(더티체킹)를 해서 해당 내용을 함께 DB에 커밋해준다. 따라서 UPDATE와 관련된 메서드는 리포지토리에서 제외해도 된다.

스프링 데이터 JPA가 구현 클래스를 대신 생성한다.

JpaRepository 인터페이스를 상속한 인터페이스에 대해 Spring Data JPA가 구현 클래스를 생성하게 된다. 이렇게 구현된 클래스는 스캔대상이 되며 xxRepository 인터페이스만으로도 동작하는 것처럼 보이게 되는 것이다. 실제로 XXRepository를 출력해보면 class com.sun.proxy.$ProxyXXX이 출력된다.
JpaRepository를 상속하는 인터페이스는 @Repository를 생략해도 된다. 자동으로 컴포넌트 스캔을 스프링 데이터 JPA가 자동으로 처리하기 때문이다.

쿼리 메서드 기능 3가지

1. 메소드 이름으로 쿼리 생성

ex) findByUsernameAndAge :: username과 age가 where문에 사용된다.
ex) findByUsernameAndAgeGreaterThan :: username과 age를 where문에 사용하되 age는 ~보다 커야 한다는 조건을 갖는다.

(1) JPA에서 제공해주는 쿼리메서드 필터조건은 공식문서 참고 :: [스프링 공식문서](https://docs.spring.io/spring-data/jpa/docs/current/ reference/html/#jpa.query-methods.query-creation)

  • By뒤에는 where문에 들어갈 내용을 적어주면 된다.

(2) 스프링 데이터 JPA가 제공하는 쿼리 메소드 기능

이 기능은 엔티티의 필드명이 변경될 경우 인터페이스에서 정의한 메서드 이름도 꼭 변경해야한다. 그렇지 않으면 애플리케이션 시작 시점에 컴파일러 오류가 발생한다. (큰 강점)

단점 :: 길어지면 답이 없다

    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);

    }

2. 메소드 이름으로 JPA NamedQuery 호출(실무에서 쓸일이 없음)

NamedQuery 기법이 무엇이냐 하면 아래처럼 작성해서 이름을 부여하고 그걸 계속해서 활용하는 방식이라고 한다.

하지만, JpaRepository에서 또 이 NamedQuery를 호출하기 위해 @Query(name = Member.findByUsername) 을 작성해야하고 다음에 소개할 3번을 사용하면 되기 때문에 실무에서는 잘 사용하지 않는다.

3. @Query 어노테이션을 사용해 리파지토리 인터페이스에 쿼리 직접 정의(JPQL)

이 방법은 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);
        }

    }

파라미터 바인딩(위치기반 vs 이름 기반) .결론은 이름기반으로 작성!

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);
}

컬렉션 파라미터 바인딩(in절 지원)

@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 예외 발생한다.

순수 JPA 페이징과 정렬

  • 아래의 조건으로 페이징하고자 한다고 해보자
  1. 검색조건 : 나이가 10살인 멤버
  2. 정렬조건 : 이름으로 내림차순
  3. 페이징 조건 : 첫번재 페이지를 받고싶고, 페이지당 보여줄 데이터는 3건
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);
}


스프링 데이터 JPA페이징과 정렬(공통화)

(1) 페이징과정렬파라미터

  • org.springframework.data.domain.Sort : 정렬기능
  • org.springframework.data.domain.Pageable : 페이징기능 (내부에 Sort 포함)

(2) 특별한반환타입

  • org.springframework.data.domain.Page : 추가 count 쿼리결과를 포함하는페이징
  • org.springframework.data.domain.Slice : 추가 count 쿼리없이 다음페이지만 확인가능(내부적 으로 limit + 1조회)
  • List (자바컬렉션): 추가 count 쿼리없이결과만 반환
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 값이 계속 기다려야하는 문제를 해결할 수 있기 때문에 실무에서 사용하기도 한다.

특히 페이징된 리스트들은 DTO로 변환해서 컨트롤러에 제공

 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을 활용하면 편리하다.

정리

  • Page
  • Slice (count X) :: 추가로 limit + 1을조회한다. 그래서 다음 페이지 여부확인에 사용할 수 있다. (최근모바일리스트 생각해보면 됨)
  • List (count X) :: 카운트쿼리분리(이건 복잡한 sql에서사용, 데이터는 left join, 카운트는 left join 안해도 됨)
    실무에서매우중요!!!

0개의 댓글