ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • #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 쿼리가 없으면 메서드 이름으로 쿼리 생성 전략을 사용한다.

    · 필요하면 전략을 변경할 수 있지만 권장하지 않는다.

     

    참고:

    https://docs.spring.io/spring-data/jpa/docs/current/reference/html/ #repositories.query-methods.query-lookup-strategies

    참고: 스프링 데이터 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건

    댓글

Designed by Tistory.