-
#Spring Data JPA - 2 공통 인터페이스Spring Data JPA 2021. 3. 23. 11:41
공통 인터페이스 기능
· 순수 JPA 기반 리포지토리 만들기
· 스프링 데이터 JPA 공통 인터페이스 소개
· 스프링 데이터 JPA 공통 인터페이스 활용
· 순수 JPA 기반 리포지토리 만들기
· 순수한 JPA 기반 리포지토리를 만들자
· 기본 CRUD
· 저장
· 변경 → 변경감지 사용
· 삭제
· 전체 조회
· 단건 조회
· 카운트
참고: JPA에서 수정은 변경감지 기능을 사용하면 된다.
트랜잭션 안에서 엔티티를 조회한 다음에 데이터를 변경하면, 트랜잭션 종료 시점에 변경감지 기능이 작동해서 변경된 엔티티를 감지하고 UPDATE SQL을 실행한다.
공통 인터페이스 설정
JavaConfig 설정- 스프링 부트 사용시 생략 가능
@Configuration @EnableJpaRepositories(basePackages = "jpabook.jpashop.repository") public class AppConfig {}
· 스프링 부트 사용시 @SpringBootApplication 위치를 지정(해당 패키지와 하위 패키지 인식)
· 만약 위치가 달라지면 @EnableJpaRepositories 필요
구현체가 없고 인터페이스 뿐이 없는데 어떻게 memberRepository를 인젝션 받은게 동작을 하냐?
스프링이 이 인터페이스를 보고 SPRING DATA JPA가 구현 클래스를 만들어서 넣어준것이다. 아래 그림이 이것이다.
스프링 데이터 JPA가 구현 클래스 대신 생성
· org.springframework.data.repository.Repository 를 구현한 클래스는 스캔 대상
· MemberRepository 인터페이스가 동작한 이유
· 실제 출력해보기(Proxy)
· 결과 : memberRepository.getClass() → class com.sun.proxy.$ProxyXXX
· @Repository 애노테이션 생략 가능
· 컴포넌트 스캔을 스프링 데이터 JPA가 자동으로 처리
· JPA 예외를 스프링 예외로 변환하는 과정도 자동으로 처리
공통 인터페이스 적용
순수 JPA로 구현한 MemberJpaRepository 대신에 스프링 데이터 JPA가 제공하는 공통 인터페이스 사용
스프링 데이터 JPA 기반 MemberRepository
public interface MemberRepository extends JpaRepository<Member, Long> { }
MemberRepository 테스트
package study.datajpa.repository; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.transaction.annotation.Transactional; import study.datajpa.entity.Member; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; @SpringBootTest @Transactional public class MemberRepositoryTest { @Autowired MemberRepository memberRepository; @Test public void testMember() { Member member = new Member("memberA"); Member savedMember = memberRepository.save(member); Member findMember = memberRepository.findById(savedMember.getId()).get(); Assertions.assertThat(findMember.getId()).isEqualTo(member.getId()); Assertions.assertThat(findMember.getUsername()).isEqualTo(member.getUsername()); Assertions.assertThat(findMember).isEqualTo(member); //JPA 엔티티 동일성 보장 } @Test public void basicCRUD() { Member member1 = new Member("member1"); Member member2 = new Member("member2"); memberRepository.save(member1); memberRepository.save(member2); //단건 조회 검증 Member findMember1 = memberRepository.findById(member1.getId()).get(); Member findMember2 = memberRepository.findById(member2.getId()).get(); assertThat(findMember1).isEqualTo(member1); assertThat(findMember2).isEqualTo(member2); //리스트 조회 검증 List<Member> all = memberRepository.findAll(); assertThat(all.size()).isEqualTo(2); //카운트 검증 long count = memberRepository.count(); assertThat(count).isEqualTo(2); //삭제 검증 memberRepository.delete(member1); memberRepository.delete(member2); long deletedCount = memberRepository.count(); assertThat(deletedCount).isEqualTo(0); } }
· TeamRepository는 테스트 생략
· Generic
· T: 엔티티 타입
· ID: 식별자 타입(PK)
공통 인터페이스 분석
· JpaRepository 인터페이스: 공통 CRUD 제공
· 제네릭은 <엔티티 타입, 식별자 타입> 설정
* JpaRepository 공통 기능 인터페이스*
public interface JpaRepository<T, ID extends Serializable> extends PagingAndSortingRepository<T, ID> { ... }
* JpaRepository 를 사용하는 인터페이스*
public interface MemberRepository extends JpaRepository<Member, Long> { }
공통 인터페이스 구성
주의
· T findOne(ID) → Optional findById(ID) 변경
제네릭 타입
· T : 엔티티
· ID : 엔티티의 식별자 타입
· S : 엔티티와 그 자식 타입
주요 메서드
save(S) : 새로운 엔티티는 저장하고 이미 있는 엔티티는 병합한다.
delete(T) : 엔티티 하나를 삭제한다. 내부에서 EntityManager.remove() 호출
findById(ID) : 엔티티 하나를 조회한다. 내부에서 EntityManager.find() 호출
getOne(ID) : 엔티티를 프록시로 조회한다. 내부에서 EntityManager.getReference() 호출
findAll(…) : 모든 엔티티를 조회한다. 정렬( Sort )이나 페이징( Pageable ) 조건을 파라미터로 제공할 수 있다.
참고: JpaRepository 는 대부분의 공통 메서드를 제공한다
쿼리 메소드 기능
package study.datajpa.repository; import org.springframework.data.jpa.repository.JpaRepository; import study.datajpa.entity.Member; import java.util.List; public interface MemberRepository extends JpaRepository<Member,Long> { //JpaRepository<type,id(pk type)> List<Member> findByUsername(String username); //username같은것은 공통으로 하기 애매해서 안만들어져 있다. //impl로 상속받아 구현할려고하면 interface라서 나머지도 다 구현해야한다고 나온다. //그럼 어떻게 findByUsername을 해결해야할까? //쿼리메소드로 해결! }
username과 같은것은 도메인으로 특화되있는 것이기 때문에 공통으로 하기 애매해서 안만들어져 있다.
impl로 상속받아 구현할려고하면 내가 하고싶은것은 username만 재정의 해서 쓰는것인데interface라서 나머지도 다 구현해야한다고 나온다.
그럼 어떻게 findByUsername을 해결해야할까? -> 쿼리메소드로 해결!· 메소드 이름으로 쿼리 생성
· NamedQuery
· @Query - 리파지토리 메소드에 쿼리 정의
· 파라미터 바인딩
· 반환 타입
· 페이징과 정렬
· 벌크성 수정 쿼리
· @EntityGraph
스프링 데이터 JPA가 제공하는 마법 같은 기능
쿼리 메소드 기능 3가지
· 메소드 이름으로 쿼리 생성
· 메소드 이름으로 JPA NamedQuery 호출
· @Query 어노테이션을 사용해서 리파지토리 인터페이스에 쿼리 직접 정의
메소드 이름으로 쿼리 생성
메소드 이름을 분석해서 JPQL 쿼리 실행
이름과 나이를 기준으로 회원을 조회하려면?
순수 JPA 리포지토리
public List<Member> findByUsernameAndAgeGreaterThan(String username, int age) { return em.createQuery("select m from Member m where m.username = :username and m.age > :age") .setParameter("username", username) .setParameter("age", age) .getResultList(); }
순수 JPA 테스트 코드
@Test public void findByUsernameAndAgeGreaterThan() { Member m1 = new Member("AAA", 10); Member m2 = new Member("AAA", 20); memberJpaRepository.save(m1); memberJpaRepository.save(m2); List<Member> result =memberJpaRepository.findByUsernameAndAgeGreaterThan("AAA", 15); assertThat(result.get(0).getUsername()).isEqualTo("AAA"); assertThat(result.get(0).getAge()).isEqualTo(20); assertThat(result.size()).isEqualTo(1); }
스프링 데이터 JPA
public interface MemberRepository extends JpaRepository<Member, Long> { List<Member> findByUsernameAndAgeGreaterThan(String username, int age); }
· 스프링 데이터 JPA는 메소드 이름을 분석해서 JPQL을 생성하고 실행
하지만 이것은 이름이 길어지면 보기 힘들기 떄문에 두개 이상 넘어가게 될 경우 다른 방법으로 해결하자
쿼리 메소드 필터 조건 스프링 데이터 JPA 공식 문서 참고:
https://docs.spring.io/spring-data/jpa/docs/2.4.6/reference/html/#jpa.query-methods
Spring Data JPA - Reference Documentation
Example 109. Using @Transactional at query methods @Transactional(readOnly = true) public interface UserRepository extends JpaRepository { List findByLastname(String lastname); @Modifying @Transactional @Query("delete from User u where u.active = false") v
docs.spring.io
스프링 데이터 JPA가 제공하는 쿼리 메소드 기능
· 조회: find…By , read…By , query…By get…By, (문서 4.4.2. Query Creation)
· 예:) findHelloBy 처럼 ...에 식별하기 위한 내용(설명)이 들어가도 된다.
· COUNT: count…By 반환타입 long
· EXISTS: exists…By 반환타입 boolean
· 삭제: delete…By, remove…By 반환타입 long
· DISTINCT: findDistinct, findMemberDistinctBy
· LIMIT: findFirst3, findFirst, findTop, findTop3 (문서 4.4.5. Limiting Query Results)
참고: 이 기능은 엔티티의 필드명이 변경되면 인터페이스에 정의한 메서드 이름도 꼭 함께 변경해야 한다. 그렇지 않으면 애플리케이션을 시작하는 시점에 오류가 발생한다.
이렇게 애플리케이션 로딩 시점에 오류를 인지할 수 있는 것이 스프링 데이터 JPA의 매우 큰 장점이다.
JPA NamedQuery(실무에서 쓸일이 거의 없음)
JPA의 NamedQuery를 호출할 수 있음
* @NamedQuery 어노테이션으로 Named 쿼리 정의*
@Entity @NamedQuery( name="Member.findByUsername", query="select m from Member m where m.username = :username") public class Member { ... }
JPA를 직접 사용해서 Named 쿼리 호출
public class MemberRepository { public List<Member> findByUsername(String username) { ... List<Member> resultList = em.createNamedQuery("Member.findByUsername", Member.class) .setParameter("username", username) .getResultList(); } }
스프링 데이터 JPA로 NamedQuery 사용
@Query(name = "Member.findByUsername") List<Member> findByUsername(@Param("username") String username);
@Query 를 생략하고 메서드 이름만으로 Named 쿼리를 호출할 수 있다.
스프링 데이터 JPA로 Named 쿼리 호출
public interface MemberRepository extends JpaRepository<Member, Long> { //** 여기 선언한 Member 도메인 클래스 List<Member> findByUsername(@Param("username") String username); }
· 스프링 데이터 JPA는 선언한 "도메인 클래스 + .(점) + 메서드 이름"으로 Named 쿼리를 찾아서 실행
· 만약 실행할 Named 쿼리가 없으면 메서드 이름으로 쿼리 생성 전략을 사용한다.
· 필요하면 전략을 변경할 수 있지만 권장하지 않는다.
참고:
참고: 스프링 데이터 JPA를 사용하면 실무에서 Named Query를 직접 등록해서 사용하는 일은 드물다.
대신 @Query 를 사용해서 리파지토리 메소드에 쿼리를 직접 정의한다.
@Query, 리포지토리 메소드에 쿼리 정의하기(실무에서 많이 사용)
메서드에 JPQL 쿼리 작성 (복잡한 jpql을 해결)
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") intage); }
· @org.springframework.data.jpa.repository.Query 어노테이션을 사용
· 실행할 메서드에 정적 쿼리를 직접 작성하므로 이름 없는 Named 쿼리라 할 수 있음
· JPA Named 쿼리처럼 애플리케이션 실행 시점에 문법 오류를 발견할 수 있음(매우 큰 장점!)
참고: 실무에서는 메소드 이름으로 쿼리 생성 기능은 파라미터가 증가하면 메서드 이름이 매우 지저분해진다.
따라서 @Query 기능을 자주 사용하게 된다.
하지만 이것은 정적 쿼리이기때문에 동적쿼리를 사용하려면 QueryDSL을 공부해야한다.
@Query, 값, DTO 조회하기
단순히 값 하나를 조회
@Query("select m.username from Member m") List<String> findUsernameList();
JPA 값 타입( @Embedded )도 이 방식으로 조회할 수 있다.
DTO로 직접 조회
@Query("select new study.datajpa.repository.MemberDto(m.id, m.username, t.name) " + "from Member m join m.team t") List<MemberDto> findMemberDto();
주의! DTO로 직접 조회 하려면 JPA의 new 명령어(new Operation으로 생성)를 사용해야 한다.
그리고 다음과 같이 생성자가 맞는 DTO가 필요하다. (JPA와 사용방식이 동일하다.)
package study.datajpa.repository; import lombok.Data; @Data public class MemberDto { private Long id; private String username; private String teamName; public MemberDto(Long id, String username, String teamName) { this.id = id; this.username = username; this.teamName = teamName; } }
파라미터 바인딩
· 위치 기반(사용 X)
· 이름 기반(사용 O)
select m from Member m where m.username = ?0 //위치 기반 select m from Member m where m.username = :name //이름 기반
파라미터 바인딩
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); }
참고: 코드 가독성과 유지보수를 위해 이름 기반 파라미터 바인딩을 사용하자 (위치기반은 순서 실수가 바꾸면…)
컬렉션 파라미터 바인딩(실무에서 많이 씀)
Collection 타입으로 in절 지원
@Query("select m from Member m where m.username in :names") List findByNames(@Param("names") List names); //실제 작성 @Query("select m from Member m where m.username in :names") List<Member> findByNames(@Param("names") List<String> names);
반환 타입
스프링 데이터 JPA는 유연한 반환 타입 지원
List<Member> findByUsername(String name); //컬렉션 Member findByUsername(String name); //단건 Optional<Member> findByUsername(String name); //단건 Optional
스프링 데이터 JPA 공식 문서:
https://docs.spring.io/spring-data/jpa/docs/current/reference/ html/#repository-query-return-types
조회 결과가 많거나 없으면?
· 컬렉션
· 결과 없음: 빈 컬렉션 반환
· 단건 조회
· 결과 없음: null 반환
· 결과가 2건 이상: javax.persistence.NonUniqueResultException 예외 발생
참고: 단건으로 지정한 메서드를 호출하면 스프링 데이터 JPA는 내부에서 JPQL의 Query.getSingleResult() 메서드를 호출한다.
이 메서드를 호출했을 때 조회 결과가 없으면 javax.persistence.NoResultException 예외가 발생하는데 개발자 입장에서 다루기가 상당히 불편하다. 스프링 데이터 JPA는 단건을 조회할 때 이 예외가 발생하면 예외를 무시하고 대신에 null 을 반환한다.
순수 JPA 페이징과 정렬
JPA에서 페이징을 어떻게 할 것인가?
다음 조건으로 페이징과 정렬을 사용하는 예제 코드를 보자.
· 검색 조건: 나이가 10살
· 정렬 조건: 이름으로 내림차순
· 페이징 조건: 첫 번째 페이지, 페이지당 보여줄 데이터는 3건
'Spring Data JPA' 카테고리의 다른 글
#Spring Data JPA - 6 쿼리메소드 - 페이징과 정렬 (0) 2021.03.24 #Spring Data JPA - 5 쿼리메소드 - 반환타입 (0) 2021.03.23 #Spring Data JPA - 4 쿼리 메소드 -파라미터 바인딩 (0) 2021.03.23 #Spring Data JPA - 3 쿼리 메소드 기능 (0) 2021.03.23 #Spring Data JPA - 1 프로젝트 생성 (0) 2021.03.12