본문 바로가기

Advance I/Spring Data JPA

22.06.30

공통 인터페이스 분석

JpaRepository

JpaRepository 인터페이스: 공통 CRUD 제공(상속) + jpa 특화 (spring-data-jpa 라이브러리)

제네릭은 <엔티티 타입, 식별자 타입> 설정

 

JpaRepository의 부모 인터페이스

PagingAndSortingRepository: 페이징과 정렬은 어떤 데이터베이스이든 비슷한 동작 방식을 가지기 때문에 공통 인터페이스 제공 (spring-data-commons 라이브러리)

CrudRepository: 공통 CRUD 제공

Repository: 최상위 클래스. 기능보다는 정체성 표시의 의미

 

참고: JpaRepository, PagingAndSortingRepository, CrudRepository에 붙은 @NoRepositoryBean 어노테이션은 실제 사용되는 Repository가 아님을 표시하고 빈을 생성하지 않도록 한다. 메서드를 제공하는 중간 인터페이스에 대해 스프링 빈을 생성하고 싶지 않을 때 사용한다.

 

주요 메서드

save(S) : 새로운 엔티티는 저장하고 이미 있는 엔티티는 병합한다.

delete(T) : 엔티티 하나를 삭제한다. 내부에서 EntityManager.remove() 호출

findById(ID) : 엔티티 하나를 조회한다. 내부에서 EntityManager.find() 호출

getOne(ID) : 엔티티를 프록시로 조회한다. 내부에서 EntityManager.getReference() 호출

findAll(…) : 모든 엔티티를 조회한다. 정렬( Sort )이나 페이징( Pageable ) 조건을 파라미터로 제공할 수 있다.

 

참고: JpaRepository 는 대부분의 공통 메서드를 제공한다 -> 공통 메서드가 아닌 경우? (쿼리 메소드, 커스텀 기능)

쿼리 메소드 기능

쿼리 메소드 기능 3가지

메소드 이름으로 쿼리 생성

메소드 이름으로 JPA NamedQuery 호출

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

메소드 이름으로 쿼리 생성

메소드 이름을 분석해서 JPQL 쿼리 실행

ex. findByUsernameAndAgeGreaterThan => 필드명 only(equal), And(and), 필드명+논리연산

 

쿼리 메소드 필터 조건

스프링 데이터 JPA 공식 문서 참고: (https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa.query-methods.query-creation)

 

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

-조회: find…By, read…By, query…By, get…By

  findHelloBy 처럼 ...에 식별하기 위한 내용(설명)이 들어가도 된다.

-COUNT: count…By 반환타입 long

-EXISTS: exists…By 반환타입 boolean

-삭제: delete…By, remove…By 반환타입 long

-DISTINCT: findDistinct, findMemberDistinctBy

-LIMIT: findFirst3, findFirst, findTop, findTop3

 

간단하고 습관적인 쿼리에 잘 사용. 필드가 3개 이상의 복잡한 쿼리는 다른 방법으로...

 

참고: 이 기능은 엔티티의 필드명이 변경되면 인터페이스에 정의한 메서드 이름도 꼭 함께 변경해야 한다. 그렇지 않으면 애플리케이션을 시작하는 시점에 오류가 발생한다. 이렇게 애플리케이션 로딩 시점에 오류를 인지할 수 있는 것이 스프링 데이터 JPA의 매우 큰 장점이다.

NamedQuery

거의 사용 안함

스프링 데이터 JPA는 선언한 "도메인 클래스 + .(점) + 메서드 이름"으로 Named 쿼리를 찾아서 실행

ex)

JpaRepository<Member, Long>

findByUsername(@Param("username" String username)

=> Member.findByUsername라는 이름의 Named 쿼리 실행

 

만약 실행할 Named 쿼리가 없으면 메서드 이름으로 쿼리 생성 전략을 사용한다.

 

NamedQuery는 정적 쿼리로써 애플리케이션 실행 시점에 jpql을 파싱해서 문법 오류를 확인할 수 있다.

사용자가 직접 createQuery 할 경우엔 문법 오류가 있더라도 일단 쿼리가 나갈 수 있다. 즉, 런타임 중 실제로 쿼리가 실행되고 나서야 예외가 발생할 수 있다 -> 위험

 

참고: 스프링 데이터 JPA를 사용하면 실무에서 Named Query를 직접 등록해서 사용하는 일은 드물다. 대신 @Query 를 사용해서 리파지토리 메소드에 쿼리를 직접 정의한다. (@Query)

@Query - 리파지토리 메소드에 쿼리 정의

@org.springframework.data.jpa.repository.Query

public interface MemberRepository extends JpaRepository<Member, Long> {
    @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);
}

메소드 파라미터: @Query 쿼리에 바인딩할 변수 정의

쿼리 생성 전략의 단점(파라미터가 많아지면 메소드 이름이 길어짐)을 보완할 수 있다.

@Query는 이름이 없는 NamedQuery라 할 수 있다.

NamedQuery처럼 애플리케이션 실행 시점에 jpql을 파싱해서 문법 오류를 확인할 수 있다.

 

동적 쿼리? QueryDSL

@Query로 값, DTO로 조회하기

1. 단순히 값만 조회  

    @Query("select m.username from Member m") 

    List<String> findUsernameList();    *username:String

JPA 값 타입( @Embedded )도 이 방식으로 조회할 수 있다.

 

2. DTO로 직접 조회

    @Query("select new study.datajpa.dto.MemberDto(m.id, m.username, t.name) " + "from Member m join m.team t")

    List findMemberDto();

DTO로 직접 조회 하려면 JPA의 new 명령어를 사용해야 한다. 생성자가 맞는 DTO가 필요하다.

DTO의 패키지 경로까지 전부 기재

파라미터 바인딩

select m from Member m where m.username = ?0 //위치 기반

select m from Member m where m.username = :name //이름 기반

코드 가독성과 유지보수를 위해 이름 기반만 사용할 것

컬렉션 파라미터 바인딩

Collection 타입으로 in절 지원

@Query("select m from Member m where m.username in :names")

List findByNames(@Param("names") List names);

반환 타입

스프링 데이터 JPA는 유연한 반환 타입 지원

 

List findByUsername(String name); //컬렉션

Member findByUsername(String name); //단건 Entity

Optional findByUsername(String name); //단건 Optional

...

https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#repository-query-return-types

조회 결과가 많거나 없으면?

컬렉션

결과 없음: 빈 컬렉션 반환 (null이 아닌데 조회한 데이터가 없음)

단건 조회

결과 없음: null (예외 발생X)

반환 결과가 2건 이상: javax.persistence.NonUniqueResultException 예외 발생

           ->  스프링 데이터 JPA가 org.springframework.dao.IncorrectResultSizeDataAccessException 으로 변환

                (리포지토리가 어떤 프레임워크를 쓰던 간에 스프링 프레임워크 안에서 예외를 동일하게 처리할 수 있게)

 데이터가 있을지 없을지 모르면 그냥 Optional로 반환하여 개발자가 처리

 

참고: 단건으로 지정한 메서드를 호출하면 스프링 데이터 JPA는 내부에서 JPQL의 Query.getSingleResult() 메서드를 호출한다. 이 메서드를 호출했을 때 조회 결과가 없으면 javax.persistence.NoResultException 예외가 발생하는데 개발자 입장에서 다루기가 상당히 불편하다. 스프링 데이터 JPA는 단건을 조회할 때 이 예외가 발생하면 예외를 무시하고 대신에 null 을 반환한다

페이징과 정렬

순수 JPA 페이징과 정렬

검색 조건: 나이가 10살

정렬 조건: 이름으로 내림차순

페이징 조건: 첫 번째 페이지, 페이지당 보여줄 데이터는 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();
}

정렬(내림차): order by 컬럼명(필드명) desc

페이징: (0 index) offset 번째 부터 limit 개 만큼 조회 

 

public long totalCount(int age) {
    return em.createQuery("select count(m) from Member m where m.age = :age",
                    Long.class)
            .setParameter("age", age)
            .getSingleResult();
}

조건에 부합하는 Member 수(row 수) -> count 함수

결과값 1개이므로 getSingleResult()

스프링 데이터 JPA 페이징과 정렬

페이징과 정렬 파라미터

org.springframework.data.domain.Sort : 정렬 기능

org.springframework.data.domain.Pageable : 페이징 기능 (내부에 Sort 포함) - 구현체: PageRequest

=> 어떤 DB를 사용하던 간에 공통적으로 적용

 

특별한 반환 타입

org.springframework.data.domain.Page : 추가 count 쿼리 결과를 포함 (컨텐츠 조회 쿼리 + total count 쿼리)

org.springframework.data.domain.Slice : 추가 count 쿼리 없이 다음 페이지 유무만 확인 가능 (컨텐츠 limit+1 조회 쿼리)

 (최근 모바일 리스트, 더보기)

List (자바 컬렉션): 추가 count 쿼리 없이 결과만 반환 (컨텐츠 조회 쿼리만)

 

*Page = [ Slice(상속) + total count 메서드(추가 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 index(Pageable)는 0부터 시작이다.

 

참고:

①데이터가 많아질수록

②데이터 조회를 위한 join이 total count 쿼리에도 적용되는 경우

total count 쿼리에 의한 성능 저하가 일어날 수 있다.

* total count 쿼리 분리하기 *

@Query(value = “select m from Member m”,
        countQuery = “select count(m.username) from Member m”)
Page<Member> findMemberAllCountBy(Pageable pageable);

 

페이지를 유지하면서 엔티티를 DTO로 변환하기

Page<Member> page = memberRepository.findByAge(10, pageRequest);
Page<MemberDto> dtoPage = page.map(m -> new MemberDto());

엔티티가 아닌 DTO로 변환하여 API로 반환할 것

 

 

 

*이전 코드 이동: ctrl+E+Enter

'Advance I > Spring Data JPA' 카테고리의 다른 글

22.07.06  (0) 2022.07.06
22.07.04  (0) 2022.07.04
22.07.02  (0) 2022.07.02
22.07.01  (0) 2022.07.01
22.06.28 :: 실전! 스프링 데이터 JPA  (0) 2022.06.30