공통 인터페이스 설정
- JavaConfig를 설정해야 합니다. 단, Spring Boot 사용시 생략 가능하다.
- Spring Boot를 사용하는 경우 @SpringBootApplication을 사용하여 위치를 지정해야 합니다. (패키지와 하위 패키지를 인식합니다.)
- 위치가 다른 경우 @EnableJpaRepositories를 사용해야 합니다.
@Configuration
@EnableJpaRepositories(basePackages = "practice.datajpa")
public class AppConfig {}
작동 방식
- Spring Data JPA는 구현 클래스를 생성합니다. JpaRepository만 상속하면 됩니다.
필요한 메서드만 인터페이스하고 정의합니다. - org.springframework.data.repositry.Repository를 구현하는 클래스는 스캔 대상입니다.
수입 org.springframework.stereotype.인덱싱됨;
* @보다 크러드 저장소
* @param <티> 저장소가 관리하는 도메인 유형
* @param <ID> 저장소가 관리하는 엔티티의 ID 유형
* @작가 올리버 기르케
*/
@색인
공용 인터페이스 저장소<티, ID> {
}
- 이것이 바로 MemberRepository Interface가 작동하는 이유이며, 실제로 출력해보면 Proxy 객체가 구현 클래스로 출력되는 것을 확인할 수 있습니다.
스프링 데이터 JPA 인터페이스 구현 클래스 = class jdk.proxy2.$Proxy118
- 또한 인터페이스는 @Repository를 생략할 수 있습니다.
- 컴포넌트 스캐닝은 Spring Data JPA에 의해 자동으로 처리됩니다.
- 또한 JPA 예외를 Spring 예외로 변환하는 프로세스를 자동으로 처리합니다.

공통 인터페이스 적용
Spring Data JPA에서 제공하는 공통 인터페이스를 사용합니다. 그러면 기존 순수 JPA 리포지토리에서 사용하던 코드를 그대로 사용해도 동일하게 동작하는 것을 확인할 수 있다.
공통 인터페이스 분석
공용 인터페이스 Jpa리포지토리<티, ID> 연장하다 목록CrudRepository<티, ID>, ListPagingAndSortingRepository<티, ID>, QueryByExampleExecutor<티>
- JpaRepository 인터페이스는 공통 CRUD를 제공합니다.
- 제네릭은 다음으로 설정됩니다.
. - T: 엔티티
- ID: 엔티티의 식별자 유형
- S: 엔터티 및 하위 유형
- 주요 공통 방법
- save(S): 새 항목을 저장하고 기존 항목을 병합합니다.
- delete(T): 엔티티를 삭제합니다.
- 내부적으로 EntityManager.remove() 호출
- findById(ID): 하나의 엔티티를 검색합니다.
- 내부적으로 EntityManager.find() 호출
- getOne(ID) : 엔터티를 프록시로 검색합니다.
- 내부적으로 EntityManager.getReference() 호출
- findAll(…) : 모든 엔터티를 검색합니다.
- 정렬 또는 페이징 가능한 조건을 매개변수로 제공할 수 있습니다.
쿼리 메서드 기능
3개의 쿼리 메서드 함수
- 메서드 이름으로 쿼리를 만들 수 있습니다.
- 메소드 이름으로 JPA NamedQuery를 호출할 수 있습니다.
- 쿼리는 @Query 주석을 사용하여 Repository Interface에서 직접 정의할 수 있습니다.
메서드 이름으로 쿼리 생성
- 예를 들어 find … By의 By 뒤에 where를 입력하는 조건을 지정할 수 있습니다. 기준 뒤에 조건이 추가되지 않으면 전체 쿼리가 적용됩니다.
- findHelloBy와 마찬가지로 …는 식별을 위한 내용과 설명을 포함할 수 있습니다.
- 장점
- 엔터티의 필드 이름이 변경되면 인터페이스에 정의된 메서드 이름도 변경되어야 합니다. 그렇지 않으면 응용 프로그램 시작 시 오류가 발생했습니다.하다.
- 애플리케이션 로드 시 오류 인식할수있다.
키워드 설명
| 찾기…로, 읽다… 얻다… 쿼리…별로, 검색…으로, 스트림… |
일반적으로 리포지토리 유형, Collection 또는 Streamable 하위 유형 또는 Page, GeoResults 또는 기타 매장별 결과 래퍼와 같은 결과 래퍼를 반환하는 일반 쿼리 메서드입니다. findBy…, findMyDomainTypeBy…로 사용하거나 추가 키워드와 함께 사용할 수 있습니다. |
| 존재한다… | 프로젝션이 존재하며 일반적으로 부울 결과를 반환합니다. |
| 카운트… | 숫자 결과를 반환하는 카운트 프로젝션입니다. |
| 삭제… 제거… |
결과가 없거나(void) 삭제 횟수를 반환하는 삭제 쿼리 메서드입니다. |
| …첫 번째 …맨 위 |
쿼리 결과를 첫 번째로 제한 |
| …별개의… | 고유한 결과만 반환하려면 고유 쿼리를 사용하십시오. 해당 기능이 지원되는지 여부는 상점별 문서를 참조하십시오. 이 키워드는 find(및 다른 키워드)와 by 사이의 주제 위치에 나타날 수 있습니다. |
논리적 키워드 키워드 표현
| 그리고 | 그리고 |
| 또는 | 또는 |
| 후에 | 후에, IsAfter |
| 전에 | 전에, IsBefore |
| 함유 | 함유, IsContaining, 포함 |
| 사이 | 사이, 사이에 |
| ENDING_WITH | 엔딩, IsEndingWith, 로 끝나다 |
| 존재한다 | 존재한다 |
| 거짓 | 거짓, IsFalse |
| 보다 큰 | 보다 큰, 보다 큼 |
| GREATER_THAN_EQUALS | 보다 큼, 보다 큼 |
| 안에 | 안에, IsIn |
| 이다 | 이다, 같음(또는 키워드 없음) |
| 비었다 | 비었다, 비어 있는 |
| IS_NOT_EMPTY | 비어 있지 않음, 비어 있지 않음 |
| IS_NOT_NULL | 널이 아님, IsNotNull |
| IS_NULL | 없는, IsNull |
| LESS_THAN | 보다 작음, IsLessThan |
| LESS_THAN_EQUAL | LessThanEqual, IsLessThanEqual |
| 좋다 | 좋다, 처럼 |
| 가까운 | 가까운, 근처에있다 |
| 아니다 | 아니다, 아니다 |
| NOT_IN | 노인, IsNotIn |
| 같지 않은 | 같지 않은, 같지 않음 |
| 정규식 | 정규식, 경기 정규식, 성냥 |
| 로 시작 | 로 시작, IsStartingWith, 다음으로 시작 |
| 진실 | 진실, 사실이다 |
| 이내에 | 이내에, IsWithin |
키워드 설명
| 케이스 무시, IgnoringCase | 대소문자를 구분하지 않는 비교를 위해 술어 키워드와 함께 사용됩니다. |
| 모두무시, 모든 무시 사례 | 모든 적합한 속성에 대해 대소문자를 무시합니다. 쿼리 메서드 조건자의 어딘가에 사용됩니다. |
| 주문… | 정적 정렬 순서와 속성 경로 및 방향을 지정합니다(예: OrderByFirstnameAscLastnameDesc). |
public interface MemberRepository extends JpaRepository<Member, Long> {
List<Member> findByUsernameAndAgeGreaterThan(String username, int age);
}
- 쿼리 메서드 필터 조건(스프링 데이터 JPA 공식 문서)
https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa.query-methods.query-creation
스프링 데이터 JPA – 참조 문서
예 119. 쿼리 메서드에서 @Transactional 사용 @Transactional(readOnly = true) interface UserRepository extends JpaRepository { List findByLastname(String lastname); @Modifying @Transactional @Query(“u.active = false인 사용자 u에서 삭제”) void del
docs.spring.io
- Spring Data JPA에서 제공하는 쿼리 메서드 기능
- Lookup: find … By, read … By, query … By, get … By ( … 식별을 위한 설명을 포함할 수 있음. )
- COUNT ( long ) : 카운트 … 기준
- EXISTS ( 부울 ) : 존재합니다 … By
- 삭제 ( long ) : 삭제 … 의해, 제거 … 의해
- DISTINCT : findDistinct, findMemberDistinctBy
- 제한: findFirst3, findFirst, findTop, findTop3
User findFirstByOrderByLastnameAsc();
User findTopByOrderByAgeDesc();
Page<User> queryFirst10ByLastname(String lastname, Pageable pageable);
Slice<User> findTop3ByLastname(String lastname, Pageable pageable);
List<User> findFirst10ByLastname(String lastname, Sort sort);
List<User> findTop10ByLastname(String lastname, Pageable pageable);
JPA NamedQuery
- Spring Data JPA에서 NamedQuery 사용
/**
* Spring Data JPA에서 NamedQuery 사용
* @param 사용자 이름
* @반품 목록<회원>
*/
@질문(이름 = “Member.findByUsername”) // @Query를 생략하고 메서드 이름만으로 명명된 쿼리를 호출할 수 있습니다.
목록<회원> findByUsername(@파람(“사용자 이름”) 끈 사용자 이름);
- “선언된 도메인 클래스(구성원) + .(점) + 메서드 이름”으로 쿼리 생성 전략을 사용합니다. (1위 – NamedQuery)
- 실행할 명명된 쿼리가 없으면 메서드 이름으로 쿼리를 만드는 전략이 사용됩니다. (우선 순위 2 – 메서드 이름으로 쿼리 생성)
- 장점
- 애플리케이션 로딩 시간쿼리가 구문 분석되고 구문 오류가 발견되어 통지됩니다. (정적 쿼리이기 때문에 jpql은 sql로 미리 만들어 둘 수 있습니다.
@Query, 리포지토리 메서드에서 쿼리 정의
- 실행할 메서드에 정적 쿼리를 직접 작성하므로 이름이 없는 명명된 쿼리라고 할 수 있습니다.
- 장점
- 구문 오류는 JPA 명명된 쿼리와 같이 애플리케이션 실행(로딩) 시간에 찾을 수 있습니다.
* 직접 조회 기능 정의
* @param 사용자 이름
* @param 나이
* @반품
*/
@질문(“멤버 m에서 m을 선택하십시오. 여기서 m.username = :username 및 m.age = :age”)
목록<회원> 사용자 찾기(@파람(“사용자 이름”) 끈 사용자 이름, @파람(“나이”) 정수 나이);
매개변수 바인딩
위치 기반 방법과 이름 기반 방법이 있지만 코드 가독성과 유지 관리성을 위해 이름 기반 매개 변수 바인딩을 사용합니다.
- 컬렉션 매개변수 바인딩(컬렉션 유형에서 지원하는 절에서)
* 바인딩 컬렉션 매개변수
* @param 이름
* @반품 목록<회원>
*/
@질문(“m.username이 :names에 있는 구성원 m에서 m을 선택하십시오.”)
목록<회원> findByNames(@파람(“이름”) 목록<끈> 이름);
반환 유형
Spring Data JPA는 다음과 같이 유연한 반환 유형을 지원합니다.
- 수집
- 검색 결과가 없으면 빈 컬렉션이 반환됩니다.
- 하나의
- 검색 결과가 없으면 null이 반환됩니다.
- 단일 항목으로 지정된 메소드가 호출되면 Spring Data JPA는 내부적으로 JPQL의 Query.getSingleResult() 메소드를 호출합니다. 이때 Spring Data JPA는 NoResultException 예외가 발생하면 무시하고 대신 null을 반환합니다.
- 조회 결과가 세 개 이상인 경우 javax.persistence.NonUniqueResultException 예외가 발생합니다.
- 검색 결과가 없으면 null이 반환됩니다.
- 단품 선택사항
페이징 및 정렬
순수한 JPA에서
- 검색 조건: 연령 – 10세
- 정렬 기준: 이름 내림차순
- 페이징 조건: 첫 페이지, 페이지당 3개의 데이터 표시
* 순수 JPA 페이징 코드
*/
공공의 목록<회원> findByPage(정수 나이, 정수 오프셋, 정수 한계) {
반품 여자 이름.createQuery(
“멤버 m에서 m 선택” +
” 여기서 m.age = :age” +
” m.username desc로 주문”, 회원.수업)
.setParameter(“나이”, 나이)
.setFirstResult(오프셋)
.setMaxResults(한계)
.getResultList();
}
공개 긴 총 합계(정수 나이) {
반품 여자 이름.createQuery(
“멤버 m에서 count(m) 선택” +
” 여기서 m.age = :age”, 긴.수업)
.setParameter(“나이”, 나이)
.getSingleResult();
}
봄 데이터 jpa에서
- 페이징 및 정렬 매개변수
- org.springframework.data.domain.Sort : 정렬 기능
- org.springframework.data.domain.Pageable : 페이징 함수(Sort 내부 포함)
- 특수 반환 유형
- org.springframework.data.domain.Page : 추가 카운트 쿼리 결과를 포함하는 페이징
- org.springframework.data.domain.Slice : 추가 카운트 쿼리 없이 다음 페이지만 확인 가능(제한 + 1 쿼리)
- List(Java 컬렉션): 추가 카운트 쿼리 없이 결과만 반환
Page<Member> findPageByAge(int age, Pageable pageable);
페이지<회원> 결과 = 구성원 저장소.findPageByAge(10pageRequest);
목록<회원> 내용 = 결과.getContent(); // 검색된 데이터
주장하다(콘텐츠.크기()).동일하다(삼); // 검색된 데이터 수
주장하다(결과.getTotalElements()).동일하다(5); // 총 데이터 수
주장하다(결과.getNumber()).동일하다(0); // 페이지 번호
주장하다(결과.getTotalPages()).동일하다(2); // 총 페이지 수
주장하다(결과.isFirst()).사실이다(); // 이것이 첫 번째 항목입니까?
주장하다(결과.hasNext()).사실이다(); // 다음 페이지가 있습니까?
- Pageable은 org.springframework.data.domain.PageRequest 객체를 인터페이스 구현으로 사용합니다.
- PageRequest 생성자
- 첫 번째 매개변수: 현재 페이지(페이지는 0부터 시작)
- 2번째 파라미터: 조회할 데이터 개수
- 3번째 파라미터: 얼라인먼트 정보
- 일부분
- 세지 않고 추가 쿼리 제한 + 1. 따라서 다음 페이지가 있는지 여부를 확인할 수 있습니다.
PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "username"));
Slice<Member> result = memberRepository.findSliceByAge(10, pageRequest);
select
m1_0.member_id,
m1_0.age,
m1_0.team_id,
m1_0.username
from
member m1_0
where
m1_0.age=?
order by
m1_0.username desc offset 0 rows fetch first 4 rows only
- 맨 위, 첫 번째
select
m1_0.member_id,
m1_0.age,
m1_0.team_id,
m1_0.username
from
member m1_0 fetch first 2 rows only
select
m1_0.member_id,
m1_0.age,
m1_0.team_id,
m1_0.username
from
member m1_0 fetch first 3 rows only
- 카운트 쿼리 분리(복잡한 SQL에서 사용)
- 데이터는 반드시 왼쪽으로 연결되어야 하며, 왼쪽 조인이 필요하지 않은 상황에서는 카운트가 분리됩니다.
@질문(값 = “멤버 m에서 m 선택 왼쪽 m.team t 가입”, countQuery = “멤버 m에서 count(m) 선택”)
페이지<회원> findMemberAllCountByAge(정수 나이, 페이징 가능 페이징 가능);
대량 수정 쿼리
- 일괄 수정 및 삭제 쿼리의 경우 @Modifying 주석을 사용합니다.
- 사용하지 않으면 예외를 throw합니다.
org.springframework.dao.InvalidDataAccessApiUsageException: Expecting a SELECT query : `update Member m set m.age = m.age + 1 where m.age >= :age`
- 대량 쿼리를 실행한 후 지속성 컨텍스트를 초기화해야 합니다.
- @Modifying(clearAutomatically = true)
- 대량 작업은 지속성 컨텍스트를 무시하고 실행합니다. 따라서 지속성 컨텍스트의 엔터티 상태와 DB의 엔터티 상태는 다를 수 있습니다.
- 권장 조치
- 대량 작업은 지속성 경쟁에서 엔터티 없이 먼저 실행됩니다.
- 필연적으로 퍼시스턴스 컨텍스트에 엔터티가 있는 경우 JPQL 쿼리 실행 시 자동으로 플러시를 호출하여 DB의 값을 동기화하고 대량 작업을 DB에서 직접 수행한다. 따라서 지속성 컨텍스트는 대량 작업 직후에 초기화되어야 합니다.
// 벌크성 수정 쿼리
@Modifying
@Query("update Member m set m.age = m.age + 1 where m.age >= :age")
int bulkAgePlus(@Param("age") int age);
JPQL 쿼리 실행 시 플러시가 자동으로 호출되는 이유
JPA의 플러시 개념 및 이를 호출하는 세 가지 방법
플러시 란 무엇입니까? – Flush()는 지속성 컨텍스트의 변경 사항을 데이터베이스에 반영합니다. 플러시를 실행하면 구체적으로 다음이 수행됩니다. 변경 감지가 작동 중이고 지속성 컨텍스트에 있습니다.
gwonbookcase.tistory.com
@EntityGraph
하나의 SQL로 관련 엔터티를 쿼리하는 방법. Fetch Join이라고 생각하시면 쉽습니다. 즉, 이 기능을 사용하면 JPQL 없이 가져오기 조인을 사용할 수 있습니다. (JPQL + 엔티티 그래프도 가능)
- 정리하다
- 페치 조인의 사실상 속기 버전
- LEFT OUTER JOIN 사용
1. 일반적인 방법 재정의
/**
* 공통 메서드 오버라이딩
*/
@Override
@EntityGraph(attributePaths = {"team"})
List<Member> findAll();
select
m1_0.member_id,
m1_0.age,
t1_0.team_id,
t1_0.team_name,
m1_0.username
from
member m1_0
left join
team t1_0
on t1_0.team_id=m1_0.team_id
member.getUsername() = userA
member.getTeam().getClass() = class study.datajpa.entity.Team
member.getTeam().getTeamName() = teamA
member.getUsername() = userA
member.getTeam().getClass() = class study.datajpa.entity.Team
member.getTeam().getTeamName() = teamB
2. JPQL + 엔티티 그래프
/**
* JPQL + 엔티티 그래프
*/
@EntityGraph(attributePaths = {"team"})
@Query(value = "select m from Member m")
List<Member> findMemberEntityGraph1();
3. 메소드명 + 엔티티 그래프로 쿼리
// 메서드 이름으로 쿼리 + 엔티티 그래프
@EntityGraph(attributePaths = {"team"})
List<Member> findEntityGraphByUsername(String username);
4. NamedEntityGraph
@NamedEntityGraph(name = "Member.all", attributeNodes = @NamedAttributeNode("team"))
@EntityGraph("Member.all")
@Query("select m from Member m")
List<Member> findMemberEntityGraph2();
JPA 힌트 및 잠금
JPA 힌트란 무엇입니까?
이것은 SQL 힌트가 아니라 JPA 구현에 제공되는 힌트인 JPA 쿼리 힌트입니다.
예를 들어, org.hibernate.readOnly 옵션이 true로 설정되면 Update Query가 실행되지 않는다.
org.springframework.data.jpa.repository.QueryHints 주석을 사용합니다.
@QueryHints(value = @QueryHint(name = "org.hibernate.readOnly", value = "true"))
Member findReadOnlyByUsername(String username);
- forCounting : 리턴 타입으로 페이지 인터페이스를 적용하면 추가 호출되는 페이징 카운트 쿼리에도 쿼리 힌트가 적용된다. (기본값: 참)
@QueryHints(value = {@QueryHint(name = "org.hibernate.readOnly", value = "true")}, forCounting = true)
Page<Member> findForCountingByUsername(String username, Pageable pageable);
잠그다
// Lock
@Lock(LockModeType.PESSIMISTIC_WRITE)
List<Member> findLockByUsername(String username);
확대
사용자 지정 저장소 구현
Spring Data JPA 인터페이스의 메소드를 직접 구현하고 싶을 때
- JPA를 직접 사용하는 상황(EntityManager)
- Spring JDBC Template을 사용하는 경우
- 마이바티스를 사용해야 하는 상황
- DB Connection을 직접 사용하는 경우
- QueryDSL을 사용하는 상황
커스텀 클래스 구현(이전)
- 규칙: 리포지토리 인터페이스 이름 + Impl
- Spring Data JPA는 이를 인식하여 Spring bean으로 등록합니다.
public interface MemberRepositoryCustom {
List<Member> findMemberCustom();
}
@RequiredArgsConstructor
public class MemberRepositoryImpl implements MemberRepositoryCustom{
private final EntityManager em;
@Override
public List<Member> findMemberCustom() {
return em.createQuery(
"select m from Member m", Member.class)
.getResultList();
}
}
public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom{
// Lock
@Lock(LockModeType.PESSIMISTIC_WRITE)
List<Member> findLockByUsername(String username);
- Impl이 아닌 다른 이름으로 바꾸고 싶을 때 - XML 구성
<repositories base-package="study.datajpa.repository" repository-impl-postfix="Impl" />
- JavaConfig 설정
@EnableJpaRepositories(basePackages = "study.datajpa.repository", repositoryImplementationPostfix = "Impl")
사용자 지정 리포지토리를 구현하는 새로운 방법( new )
- 규칙: 사용자 정의 인터페이스 이름 + Impl
- 이 메서드는 기존 메서드와 유사한 사용자 정의 인터페이스 이름과 구현 클래스 이름을 가지므로 더 직관적오전.
- 또한 여러 인터페이스를 개별적으로 구현하는 것도 가능하므로 새로 변경된 이 방식을 더 권장합니다.
※참고: 임의의 리포지토리를 생성하여 사용할 수 있습니다. 예를 들어 MemberQueryRepository를 인터페이스가 아닌 클래스로 만들어 스프링 빈으로 등록하고 직접 가져올 수 있습니다.
감사
엔터티를 만들거나 변경할 때, 누가 언제 변경했는지 추적하려는 경우에 사용됩니다.
- 등록 날짜
- 수정 날짜
- 등록자
- 수식어
순수 JPA에서 사용하는 경우
- JPA 주요 이벤트 주석
- @PrePersist, @PostPersist
- @사전업데이트, @사후업데이트
@MappedSuperclass
@Getter
public class JpaBaseEntity {
@Column(updatable = false)
private LocalDateTime createdDate;
private LocalDateTime updatedDate;
@PrePersist
public void prePersist() {
LocalDateTime now = LocalDateTime.now();
createdDate = now;
updatedDate = now;
}
@PreUpdate
public void preUpdate() {
updatedDate = LocalDateTime.now();
}
}
@Test
@DisplayName("Auditing - 순수 JPA")
void auditing() throws InterruptedException {
Member userA = new Member("userA");
jpaRepository.save(userA); // @PrePersist
Thread.sleep(200);
userA.setUsername("userB");
em.flush(); // @PreUpdate
em.clear();
Member foundUserA = jpaRepository.findById(userA.getId()).get();
System.out.println("foundUserA.getCreatedDate() = " + foundUserA.getCreatedDate());
System.out.println("foundUserA.getUpdatedDate() = " + foundUserA.getUpdatedDate());
}
스프링 데이터 JPA를 사용하는 경우
- 환경
- @EnableJpaAuditing : 스프링 부트 구성 클래스에 적용
- 저장 시 저장된 데이터만 입력하고 싶은 경우(update 열에 null 값 포함) @EnableJpaAuditing(modifyOnCreate = false)
- @EntityListeners(AuditingEntityListener.class) : 엔티티에 적용
- @EnableJpaAuditing : 스프링 부트 구성 클래스에 적용
@EnableJpaAuditing
@SpringBootApplication
public class DataJpaApplication {
public static void main(String() args) {
SpringApplication.run(DataJpaApplication.class, args);
}
@Bean
public AuditorAware<String> auditorProvider() {
return new AuditorAware<String>() {
@Override
public Optional<String> getCurrentAuditor() {
return Optional.of(UUID.randomUUID().toString());
}
};
}
}
- 주석 사용
- @CreatedDate
- @LastModifiedDate
- @CreatedBy
- @LastModifiedBy
@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
@Getter
public class BaseEntity extends BaseTimeEntity {
@CreatedBy
@Column(updatable = false)
private String createdBy;
@LastModifiedBy
private String lastModifiedBy;
}
@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
@Getter
public class BaseTimeEntity {
@CreatedDate
@Column(updatable = false)
private LocalDateTime createdDate;
@LastModifiedDate
private LocalDateTime lastModifiedDate;
}
웹 확장 - 도메인 클래스 변환기
HTTP 매개 변수로 전달된 엔터티의 ID로 엔터티 개체를 찾아 바인딩합니다.
- 도메인 클래스 변환기를 사용한 후 HTTP 요청은 구성원 ID를 수신하지만 도메인 클래스 변환기는 중간에서 작동하며 구성원 엔터티 개체를 반환합니다.하다.
- 도메인 클래스 변환기 결국 저장소를 사용하여 엔터티를 찾습니다.
- 엔터티가 도메인 클래스 변환기에 대한 매개 변수로 수신되면 이 엔터티는 단순 조회에만 사용해야한다.
- 논트랜잭션 범위에서 엔터티를 조회하였기 때문에 엔터티가 변경되더라도 DB에 반영되지 않는다.
// 도메인 클래스 컨버터 사용 전
@GetMapping("/members/before/{id}")
public String findMember1(@PathVariable("id") Long id) {
Member member = memberRepository.findById(id).get();
return member.getUsername();
}
// 도메인 클래스 컨버터 사용 후
@GetMapping("/members/after/{id}")
public String findMember2(@PathVariable("id") Member member) {
return member.getUsername();
}
웹 확장 - 페이징 및 정렬
- Pageable을 매개변수로 받을 수 있습니다.
- 매개변수
- 페이지 : 현재 페이지. (0부터 시작)
- 크기: 한 페이지에 노출될 데이터의 개수
- sort : 정렬 조건. (ASC/내림차순)
spring.data.web:
pageable:
default-page-size: 10 # 기본 페이지 사이즈
max-page-size: 2000
// 페이징과 정렬 - 개별 설정 (@PageableDefault)
@GetMapping("/members_page")
public Page<Member> list2(@PageableDefault(size = 5, sort = "username", direction = Sort.Direction.DESC) Pageable pageable) {
return memberRepository.findAll(pageable);
}
- 접두사
- 페이징 정보가 둘 이상인 경우 접두사로 구분됩니다.
- @Qualifier에 접두사를 추가합니다.
- 기본값 : /members?member_page=0&order_page=1
- 페이지 콘텐츠를 DTO로 변환
- 페이지는 DTO로 변환하기 위해 map()을 지원합니다.
- 1부터 페이지를 시작하는 방법
- Pageable 및 Page를 매개변수 및 응답 값으로 사용하는 대신 클래스를 직접 생성하고 처리합니다. 즉, PageRequest가 직접 생성되어 저장소로 전달되고, Page 대신 응답 값도 생성되어 제공됩니다.
- spring.data.web.pageable.one-indexed-parameters를 true로 설정합니다. 하지만 이 방식은 웹에서 매개변수 -1만 처리하기 때문에 응답 값인 모든 페이지에 대해 페이지 인덱스를 0으로 사용하는 데에는 한계가 있습니다.
사양(명세)
Domain Driven Design이라는 책에서는 사양의 개념을 소개합니다. 따라서 Spring Data JPA는 JPA Criteria를 활용하여 이 개념의 사용을 지원합니다.
술어 ( 술어 )
package org.springframework.data.jpa.domain;
public interface Specification<T> extends Serializable {
- 참 또는 거짓으로 평가합니다.
- AND, OR 등의 연산자를 조합하여 다양한 검색 조건을 손쉽게 생성할 수 있습니다. (복합 패턴)
사양 기능 사용 방법
- JpaSpecificationExecutor 인터페이스를 상속합니다.
- 사양은 매개변수로 받아 검색 조건으로 사용됩니다.
- 사양은 사양을 구현하여 조립할 수 있습니다. -> where(), and(), or(), not() 제공
public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom, JpaSpecificationExecutor<Member> {
- 사양을 정의하려면 사양 인터페이스를 구현해야 합니다.
- 사양을 정의할 때 toPredicate 메서드만 구현하면 됩니다. 이때 JPA Criteria의 Root, CriteriaQuery, CriteriaBuilder 클래스를 파라미터로 제공한다.
public class MemberSpec {
public static Specification<Member> teamName(final String teamName) {
return (root, query, builder) -> {
if (StringUtils.isEmpty(teamName)) {
return null;
}
Join<Member, Team> t = root.join("team", JoinType.INNER);// 회원과 조인
return builder.equal(t.get("teamName"), teamName);
};
}
public static Specification<Member> username(final String username) {
return (root, query, builder) -> builder.equal(root.get("username"), username);
}
}
@Test
@DisplayName("명세 기능 사용")
void specification() {
Team teamA = new Team("teamA");
em.persist(teamA);
Member user1 = new Member("user1", 10, teamA);
Member user2 = new Member("user2", 20, teamA);
em.persist(user1);
em.persist(user2);
em.flush();
em.clear();
Specification<Member> spec = MemberSpec.username("user1").and(MemberSpec.teamName("teamA"));
List<Member> result = memberRepository.findAll(spec);
assertThat(result.size()).isEqualTo(1);
assertThat(result.get(0).getUsername()).isEqualTo("user1");
}
select m1_0.member_id,m1_0.age,m1_0.created_by,m1_0.created_date,m1_0.last_modified_by,m1_0.last_modified_date,m1_0.team_id,m1_0.username
from member m1_0
join team t1_0
on t1_0.team_id=m1_0.team_id
where m1_0.username="user1"
and t1_0.team_name="teamA";
예를 통한 쿼리(예를 사용한 쿼리.)
- 엔터티 자체가 검색 조건이 됩니다.
@Test
@DisplayName("Query By Example")
void queryByExample() {
Team teamA = new Team("teamA");
em.persist(teamA);
Member user1 = new Member("user1", 10, teamA);
Member user2 = new Member("user2", 20, teamA);
em.persist(user1);
em.persist(user2);
em.flush();
em.clear();
// Probe 생성
Member member = new Member("user1");
Team team = new Team("teamA"); // 내부조인으로 teamA 가능
member.setTeam(team);
// ExampleMatcher 생성
// age property 는 무시
ExampleMatcher matcher = ExampleMatcher.matching()
.withIgnorePaths("age");
Example<Member> example = Example.of(member, matcher);
List<Member> result = memberRepository.findAll(example);
assertThat(result.size()).isEqualTo(1);
assertThat(result.get(0).getUsername()).isEqualTo("user1");
}
- 프로브: 필드에 데이터가 있는 실제 도메인 개체
- ExampleMatcher: 일치하는 특정 필드에 대한 자세한 정보를 제공합니다. (재사용 가능)
- 예 : 쿼리를 생성하는 데 사용됩니다. (Probe와 ExampleMatcher로 구성)
장점
- 동적 쿼리를 편리하게 처리할 수 있습니다.
- 도메인 개체를 있는 그대로 사용합니다.
- 데이터 저장소가 RDB에서 NoSQL로 변경되더라도 코드 변경이 없도록 추상화됩니다.
- Spring Data JPA - JpaRepository Interface에 이미 포함되어 있습니다.
불리
- 조인은 가능하지만 외부 조인이 아닌 내부 조인만 가능합니다.
- 중복 제약이 없습니다.
- 이름 = ?0 또는 (이름 = ?1 및 성 = ?2)
- 매칭 조건은 매우 간단합니다.
- 메시지
- 시작하다
- 포함
- 끝
- 정규식
- 다른 속성은 일치만 지원합니다.
- 메시지
- 따라서 매칭 조건이 실무에서 사용하기에는 너무 단순하고, LEFT 조인이 불가능하다는 단점이 있다.
예측
엔터티 대신 DTO를 편리하게 쿼리하는 데 사용됩니다.
쿼리의 select 절에 있는 필드
- 프로젝션의 의미
- 조회할 엔터티의 필드를 getter 형식으로 지정하면 해당 필드만 선택하여 조회할 수 있습니다.
- Spring Data JPA에서 사용시 메소드 이름은 자유이며 반환형은 인식한다.
인터페이스 기반 Closed Projections
- 속성 유형(getter) 인터페이스가 제공되면 Spring Data JPA에서 구현을 제공합니다.
public interface UsernameOnly {
String getUsername();
}
select
m1_0.username
from
member m1_0
where
m1_0.username="user1";
인터페이스 기반 오픈 프로젝션
- SpEL 구문이 지원됩니다.
- 단, SpEL 구문을 사용하는 경우에는 DB에서 모든 엔터티 필드를 조회한 후 계산한다. 따라서 JPQL SELECT 절의 최적화가 불가능합니다.
public interface UsernameOnly {
@Value("#{target.username + ' ' + target.age}")
String getUsername();
}
select
m1_0.member_id,
m1_0.age,
m1_0.created_by,
m1_0.created_date,
m1_0.last_modified_by,
m1_0.last_modified_date,
m1_0.team_id,
m1_0.username
from
member m1_0
where
m1_0.username="user1";
클래스 기반 프로젝션
- 비 인터페이스 콘크리트 DTO 유형
- 생성자 매개변수 이름으로 일치
동적 프로젝션
Generic 유형이 제공되면 프로젝션 데이터를 동적으로 변경할 수 있습니다.
<T> List<T> findProjectionTByUsername(String username, Class<T> type);
List<UsernameOnly> result3 = memberRepository.findProjectionTByUsername("user1", UsernameOnly.class);
중첩 구조 처리
public interface NestedClosedProjection {
String getUsername();
Teaminfo getTeam();
interface Teaminfo {
String getTeamName();
}
}
select
m1_0.username,
t1_0.team_id,
t1_0.team_name
from
member m1_0
left join
team t1_0
on t1_0.team_id=m1_0.team_id
where
m1_0.username="user1"
- 프로젝션 대상이 루트 엔터티(여기서는 멤버)이면 JPQL SELECT 절 최적화가 가능합니다.
- 프로젝션 대상이 루트가 아닌 경우
- 왼쪽 외부 조인을 처리해야 합니다.
- 모든 필드가 선택되고 엔터티로 검색된 다음 계산됩니다.
- 정리하다
- 프로젝션 대상이 루트 엔터티인 경우에 유용합니다.
- 프로젝션 대상이 루트 엔터티를 벗어나면 JPQL 선택 최적화가 실패합니다.
- 실제로 복잡한 경우 QueryDSL을 사용하십시오.
네이티브 쿼리
Spring Data JPA 기반 네이티브 쿼리
- 페이징을 지원합니다.
- 반환 유형
- 물체()
- 튜플
- DTO(스프링 데이터 인터페이스 프로젝션 지원)
- 제한
- Sort 매개변수를 통한 정렬이 정상적으로 작동하지 않을 수 있습니다.
- JPQL과 마찬가지로 애플리케이션 로딩 시 문법 검사가 불가능하다.
- 동적 쿼리는 불가능합니다.
- NativeSQL을 DTO로 쿼리할 때 JdbcTemplate 또는 MyBatis를 사용합니다.
- 프로젝션 활용(정적 쿼리의 경우 유용할 것 같음)
긴 getId();
끈 getUsername();
끈 getTeamName();
}
"멤버 m이 팀 t에 합류했습니다.",
countQuery = "카운트 선택회원으로부터"
, nativeQuery =진실
)페이지<멤버 프로젝션 >findByNativeProjection( 페이징 가능페이징 가능
);
