빡코

[스프링 데이터 JPA] 예제 도메인, 공통인터페이스?, 쿼리 메서드, 반화타입 본문

Java/JPA

[스프링 데이터 JPA] 예제 도메인, 공통인터페이스?, 쿼리 메서드, 반화타입

chris.djang 2023. 5. 10. 19:38

OverView

package study.datajpa.repository;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.*;
import org.springframework.data.repository.query.Param;
import study.datajpa.dto.MemberDto;
import study.datajpa.entity.Member;

import javax.persistence.Entity;
import javax.persistence.LockModeType;
import javax.persistence.QueryHint;
import java.util.List;
import java.util.Optional;

public interface MemberRepository extends JpaRepository<Member, Long> {
    List<Member> findByUsernameAndAgeGreaterThan(String username, int age);

    //NamedQuery
    @Query(name = "Member.findByUserName")
    List<Member> findByUsername(@Param("username") String username);

    @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("select m.username from Member m")
    List<String> findUserNameList();

    //DTO 조회
    @Query("select new study.datajpa.dto.MemberDto(m.id, m.username, t.name) from Member m join m.team t")
    List<MemberDto> findMemberDto();

    List<Member> findListByUsername(String username); //컬렉션
    Member findMemberByUsername(String Username);//단건
    //Optional<Member> findByUsername2(String username); // 단건 optional

    //페이징
    @Query(value = "select m from Member m left join m.team t",
            countQuery = "select count(m) from Member m")
    Page<Member> findByAge(int age, Pageable pageable);


    //스프링 데이터 JPA 벌크성 업데이트 쿼리
    @Modifying(clearAutomatically = true)
    @Query("update Member m set m.age = m.age + 1 where m.age >= :age")
    int bulkAgeQueryPlus(@Param("age") int age);

    @Query("select m from Member m left join fetch m.team") //member와 연관딘 team을 한번에 끍어 온다.
    List<Member> findMemberFetchJoin();

    //공통메서드오버라이드
    @Override
    @EntityGraph(attributePaths ={"team"})
    List<Member>findAll();

    @EntityGraph(attributePaths = {"team"})
    @Query("select m from Member m")
    List<Member> findMemberEntityGraph();

    //@EntityGraph(attributePaths = {"team"})
    @EntityGraph("Member.all")
    List<Member> findEntityGraphByUsername(@Param("username") String username);

    //readOnly는 변경 감치 체크를 하지 않기 때문에, 업데이트를 진행하지 않는다.
    @QueryHints(value = @QueryHint( name = "org.hibernate.readOnly", value = "true"))
    Member findReadOnlyByUsername(String username);

    //Lock 힌트
    @Lock(LockModeType.PESSIMISTIC_WRITE)
    List<Member> findLockByUsername(String username);

}

 

 

샘플 도메인 설계 

공통 인터페이스 

 

스프링 부트 사용@SpringBootApplication 위치를 지정( 해당패키지와 하위 패키지 인식) 만약 위치가 달라지면@EnableJpaRepositories 필요

@Configuration
@EnableJpaRepositories(basePackages ="jpabook.jpashop.repository")
publicclassAppConfig{
}

 

JpaRepository<Member, Long>   : GenericT: 엔티티타입ID: 식별자타입(PK)

package study.datajpa.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import study.datajpa.entity.Member;

public interface MemberRepository extends JpaRepository<Member, Long> {

}

MemberRepository에는 구현 메서드가 전혀 없는데 어떻게 실행이 되는 걸까? 스프링 데이터 JPA가 구현 클래스 대신 생성 

@Repository 어노테이션 생략 가능 

컴포넌트 스캔을 스프링 데이터 JPA가 자동으로 처리

JPA 예외를 스프링 예외로 변환하는 과정도 자동으로 처리

 

 

주요 메서드

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

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

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

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

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

수 있다.

 

 

공통인터페이스라는 제약 아래서 내가 원하는 특정 쿼리를 만들고 싶다면? 

 

1. 쿼리 메서드 기능 

스프링 데이터 JPA는 메소드 이름을 분석해서 JPQL을 생성하고 실행

 

쿼리 메소드 필터 조건

스프링 데이터 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,

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

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

COUNT: count…By 반환타입 long

EXISTS: exists…By 반환타입 boolean

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

DISTINCT: findDistinct, findMemberDistinctBy LIMIT: findFirst3, findFirst, findTop, findTop3

   https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#repositories.limit-query-result

 

 

순수 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();
}


@Test
public void findByUsernameAndAgeGreaterThan() throws Exception {

    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);
}
select member0_.member_id as member_i1_0_, member0_.age as age2_0_, member0_.team_id as team_id4_0_, member0_.username as username3_0_ 
from member member0_ 
where member0_.username=? and member0_.age>?

select member0_.member_id as member_i1_0_, member0_.age as age2_0_, member0_.team_id as team_id4_0_, member0_.username as username3_0_ 
from member member0_ 
where member0_.username='AAA' and member0_.age>15;

 

2. JPA NamedQuery (실무 사용x)

-쿼리의 이름을 부여하고 호출함 

//Meber Entity 
@Entity
@NamedQuery(
        name = "Member.findByUsername",
        query = "select m from Member m where m.username =:username")

public class Member {
}

//순수 JPA Repository
//JPA Namedquery
public List<Member> findByUsername(String username) {

    return em.createNamedQuery("Member.findByUsername", Member.class)
        .setParameter("username", username)
        .getResultList();

}

//스프링 데이터 JPA로 NamedQuery 호출 
public interface MemberRepository extends JpaRepository<Member, Long> {

    //NamedQuery
    @Query(name = "Member.findByUserName")
    List<Member> findByUsername(@Param("username") String username);
}

//TEST 코드 
@Test
public void testNamedQuery() throws Exception {

    Member m1  = new Member("AAA", 10);
    Member m2 = new Member("AAA", 20);
    memberRepository.save(m1);
    memberRepository.save(m2);

    List<Member> result = memberRepository.findByUsername("AAA");
    Member findMember = result.get(0);
    assertThat(findMember).isEqualTo(findMember);

}

//실행 쿼리
    select
        member0_.member_id as member_i1_0_,
        member0_.age as age2_0_,
        member0_.team_id as team_id4_0_,
        member0_.username as username3_0_ 
    from
        member member0_ 
    where
        member0_.username=?;

@Query생략하고 메서드 이름만으로 Named 쿼리를 호출할 있다.

 

3. @Query, 리포지토리 메소드에 쿼리 정의하기 (실무에서 활용)

-동적 쿼리의 경우 querydsl 권장

public interface MemberRepository extends JpaRepository<Member, Long> {
   
    @Query("select m from Member m where m.username = :username and m.age =:age")
    List<Member> findUserName(@Param("username") String username, @Param("age") int age);
}

실행쿼리 

    select
        member0_.member_id as member_i1_0_,
        member0_.age as age2_0_,
        member0_.team_id as team_id4_0_,
        member0_.username as username3_0_ 
    from
        member member0_ 
    where
        member0_.username=? 
        and member0_.age=?

 

4. @Query, , DTO 조회하기

public interface MemberRepository extends JpaRepository<Member, Long> {

    @Query("select m.username from Member m")
    List<String> findUserNameList();

    //DTO 조회
    @Query("select new study.datajpa.dto.MemberDto(m.id, m.username, t.name) from Member m join m.team t")
    List<MemberDto> findMemberDto();
    
}

 

package study.datajpa.dto;

import lombok.Data;

@Data
public class MemberDto {

    private Long id;
    private String username;
    private String name;

    public MemberDto(Long id, String username, String name) {
        this.id = id;
        this.username = username;
        this.name = name;
    }
}

 

5. 파라미터 바인딩

public interface MemberRepository extends JpaRepository <Member, Long> {
    @Query("select m from Member m where m.username = :name")    
    Member findMembers(@Param("name") String username);
}

 

 

반환 타입

List<Member> findListByUsername(String username); //컬렉션
Member findMemberByUsername(String Username);//단건
Optional<Member> findByUsername2(String username); // 단건 optional

 

Supported Query Return Types

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을 반환한다.