공통 인터페이스 분석
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 |