@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 pagealbe);

이전에 만들었던 interface Repository에 username으로 조회하는 기능을 추가하고 싶을 때는 어떻게해야 될까??

interface이기 때문에 새로운 기능을 추가 하기 어렵다. 그렇다고 하나의 기능을 추가하기 위해 새로운 클래스를 만들고 interface를 상속받으면 아래와 같이 모든 메소드를 Override 해야하고 기능을 구현해야 한다. (이렇게 되는 경우 Spring Data 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();
    }

위와 같이 파라미터로 넘어온 username이 같고, age보다 높은 컬럼들을 반환하는 기능을 JPA로 구현한 코드입니다.

 

이를 Spring Data JPA에 추가하려면 아래와 같이 하면됩니다.

List<Member> findByUsernameAndAgeGreaterThan(String username, int age);

메소드 이름으로 쿼리를 작성해주는 Spring Data JPA의 강력한 기능 때문에 위의 코드 한줄로 같은 기능을 제공해줍니다.

username뒤에는 아무런 기능이 없기 때문에 equal(=).  And 후
age뒤에는 GreaterThan이 있습니다. 즉, 파라미터인 age보다 크면 (>)
반환해주는 쿼리문을 Spring Data JPA에서 작성해줍니다.
주의 
메소드로 쿼리문을 작성하기 때문에 오타가 나면 안됩니다!! 

 

쿼리 메소드 필터 조건

 

Spring Data JPA - Reference Documentation

Example 109. Using @Transactional at query methods @Transactional(readOnly = true) interface UserRepository extends JpaRepository { List findByLastname(String lastname); @Modifying @Transactional @Query("delete from User u where u.active = false") void del

docs.spring.io

확인해보니 대부분의 기능들을 제공해줍니다. 하지만, 2개가 넘어가는 경우 가독성이 떨어지는 우려가 생기고 오타가 발생할 수 있는 문제점이 있을 것 같습니다. 

 

 

스프링 데이터 JPA가 제공하는 쿼리 메소드 기능

 

 

 

메소드 이름으로 쿼리 생성 정리

  • 장점
    • 애플리케이션 로딩 시점에 오류를 인지할 수 있다
  • 단점
    • 엔티티의 필드명이 변경되는 경우 메서드 이름도 함께 변경해야한다. 즉, 유지보수성이 뛰어나지는 않다.

 

 

 

 

@Query, 리포지토리 메소드에 쿼리 정의하기


메소드 위에 @Query("쿼리문")을 통해 사용할 수 있다.

    @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);

이전에 사용했던 

    List<Member> findByUsernameAndAgeGreaterThan(String username, int age);

메소드 이름으로 작성했던 쿼리문이랑 같은 기능을 한다. 

 

 

 

@Query 정리

  • 장점
    • 애플리케이션 로딩 시점에 정적 쿼리이기때문에 SQL로 파싱을 해놓는다고 합니다. 그렇기때문에 애플리케이션 로딩시점에 문법 오류를 알 수 있습니다.
    • DTO로 반환을 할 수 있다.
  • 단점
    • 간단한 쿼리문인 경우 메소드 이름으로 작성하는 것보다 많은 코드를 써야 한다. (간단한 쿼리는 메소드 이름 사용하면 편리할듯? 합니다.)
    • 동적 쿼리를 작성할 수 없다. 동적 쿼리는 QueryDSL가 좋다고 한다.

 

 

 

@Query, 값, DTO 조회하기


기존 JPQL에서 dto로 반환했을 때처럼 사용하면됩니다.

MemberRepository 코드 추가

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

위와 같이 코드를 작성하면 @Query를 통해 Dto로 반환할 수 있습니다.

 

 

 

 

파라미터 바인딩


위치 기반, 이름 기반 파라미터 바인딩은 지금까지 해왔던 바인딩이다. 파라미터 바인딩에는 컬렉션 파라미터 바인딩이라는 것도 있다.

 

컬렉션 파라미터 바인딩

Collection 타입으로 in절 지원

    @Query("select m from Member m where m.username in :names")
    List<Member> findByNames(@Param("names") Collection<String> names);

위처럼 in 절을 사용해서 컬렉션 파라미터 바인딩을 지원해줍니다.

 

 

 

반환 타입


단일 건의 조회일 때, null 값이 넘어올 가능성이 있다면 Optional을 사용하자! (NullPointException 방지!) 

 

 

 

 

 

순수 JPA 페이징과 정렬


페이징과 정렬 파라미터

  • 정렬
    • org.springframework.data.domain.Sort
  • 페이징 기능 (내부에 Sort 포함)
    • org.springframework.data.domain.pageable

 

특별한 반환 타입

  • 추가 Count 쿼리 결과를 포함하는 페이징
    • org.springframework.data.domain.Page
  • 추가 Count 쿼리 없이 다음 페이지만 확인 가능(내부적으로 Limit +1 조회)
    • org.springframework.data.domain.Slice

 

Spring Data JPA 페이지 사용

    Page<Member> findByAge(int age, Pageable pagealbe);

이전에 배웠던 메소드 이름으로 쿼리를 생성해주면서 페이징을 해준다. (파라미터로 Pageable을 추가해주면 사용이 가능하다.)

반환값을 Page로 하는 경우 (추가 count 쿼리 등을 할 수 있다.)

 

Test

        //given
        int age = 10;
        PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "username"));

        //when
        Page<Member> page = memberRepository.findByAge(age, pageRequest);

        //then
        List<Member> content = page.getContent();

Pageable, Page<T> 사용방법

Page<T>의 경우 getTotalElements(), getNumber(), getTotalPages() 등등의 기능이 있으니 공부해서 사용해보면 좋을 것 같다.

참고로, Slice<T> 반환의 경우 getTotalElements(), getTotalPages()와 같은 totalCount와 관련된 기능만 제외(Pgae<T> 에서)됩니다.

 

반환값에 따른 쿼리문 변화

Page<Member> (왼쪽)                 /              List<Member>, Slice<Member>  (오른쪽) 

 

Page<T> 정리

  • 장점
    • 반환 값을 Page<T>만 바꿔주어도 total count 쿼리문을 보낼 수 있다.
  • 단점
    • Total count 쿼리문이 나가기때문에 성능상 좋지 않다. (컬럼 수가 많을 때 특히 비효율적)

 

Page<T>로 반환 할 때, join을 하는경우 count 쿼리문도 조인을 하기 때문에 countQuery문을 따로 작성하는 것이 성능에 좋다. (최선)

CountQuery X

@Query(value = "select m from Member m left join m.team t")
Page<Member> findByAge(int age, Pageable pagealbe);

발생한 쿼리문

CountQuery O

@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 pagealbe);

발생한 쿼리문

조인을하며 쿼리가 복잡해지는 경우 성능을 높이기 위해 CountQuery문을 따로 작성하는 방법도 좋을 것 같다.

 

 

Page일 때, Dto로 변환하는 방법

        Page<Member> page = memberRepository.findByAge(age, pageRequest);
        Page<MemberDto> pageDto = page.map(member -> new MemberDto(member.getId(), 
        					member.getUsername(), 
       						member.getTeam().getName()));

위의 방식으로 쉽게 Dto로 변환이 가능하다.

 

주의
page는 1부터 시작이 아닌 0부터 시작이다. (배열 Index와 같음)

 

 

 

벌크성 수정 쿼리


벌크성 수정 쿼리를 사용할 때는 한 번에 많은 컬럼을 수정해야 하는 경우 사용한다. (예를들면, 물가 상승에 따른 모든 물건 값이 10%씩 올랐을 때 한번에 모든 물건의 가격을 10%올리는 쿼리문을 보내야할 때)

 

순수 JPA

    public int bulkAgePlus(int age){
        return em.createQuery("update Member m set m.age = m.age+1 where m.age >= : age")
                .setParameter("age", age)
                .executeUpdate();
    }

파라미터 보다 이상이면 age를 +1하는 쿼리문을 순수한 JPA로 작성했다. 이를 Spring Data JPA로 변형해보자.

 

Spring Data JPA

    @Modifying  //executeUpdate() 와 같다고 보면 됌 
    @Query("update Member m set m.age = m.age + 1 where m.age >= :age")
    int bulkAgePlus(@Param("age") int age);

 

하지만, 벌크 연산에는 문제점이 있다. 벌크 연산하는 경우 영속성 컨텍스트를 거쳐서 update를 하는 것이 아닌 바로 DB에 쿼리문을 날린다고 한다.

 

여기서 발생하는 문제점은 하나의 트랜잭션에서 insert한 엔티티를 벌크연산을 해버린 뒤, insert한 엔티티를 select하여 조회하면 벌크연산이 되지 않는다는 것이다. 

 

설명을 하자면, Insert하면 영속성 컨텍스트와 DB에 저장이 된다. 벌크 연산하면 DB에만 저장이 된다. select하면 DB를 조회하기 전에 영속성 컨텍스트에 찾고자하는 엔티티가 있는지 확인한다. 이때, 있으면 영속성 컨텍스트에서 가져오고 없다면 DB를 조회해서 갖고온다. 하지만 현재 영속성 컨텍스트에 있기때문에 DB를 거치지 않고 반환을 하게 된다. 이렇게 되는 경우 벌크연산이 되지 않은 엔티티가 반환이 되버리고 만다. 이것이 문제가 되는 것이다. (이해가 되지 않는다면, 영속성 컨텍스트에 대한 내용을 공부하는 것을 권장)

 

해결 방법은 벌크 연산 후, em.clear()를 하면 된다. 영속성 컨텍스트가 비어지기때문에 DB에 접근해서 데이터를 갖고오기때문에 위와 같은 문제가 일어나지 않게 된다. 혹은, @Modifying(clearAutomatically = true) 옵션을 추가하면 된다. 쿼리가 나고 바로 영속성 컨텍스트를 clear 시켜주는 옵션이다.

 

 

 

@EntityGraph


Spring Data JPA에서 fetch join을 사용하는 방법

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

//JPQL + 엔티티 그래프 
@EntityGraph(attributePaths = {"team"}) 
@Query("select m from Member m") 
List<Member> findMemberEntityGraph();

//메서드 이름으로 쿼리에서 특히 편리하다. 
@EntityGraph(attributePaths = {"team"}) 
List<Member> findByUsername(String username)

@EntityGraph를 통해 fetch join을 사용할 수 있다.

 

 

EntityGraph 정리

  • fetch join의 간편 버전이다.
  • left outer join 사용
  • 보통은 JPQL로 사용한다. 간단간단한 경우 EntityGraph를 사용

 

 

 

JPA Hint & Lock


JPA Hint

JPA 쿼리 힌트 (SQL 힌트가 아니라 JPA 구현체에게 제공하는 힌트이다.)

 

쿼리 힌트 사용

    @QueryHints(value = @QueryHint(name = "org.hibernate.readOnly", value = "true"))
    Member findReadOnlyByUsername(String username);

일반 select를 하게되면, 더티 체킹을 하기위해 1차캐시에 엔티티의 정보를 저장해놓는다. 이것이 성능상 크지는 않지만 작게나마 영향을 줄 수 있다. 그때 QueryHint를 통해 readOnly를 사용하면 1차캐시에 저장하지 않기 때문에 조회만 할때 성능이 좋아질 수 있다. (단, 정말 조회만 할 때 사용해야 한다. 더티체킹을 통한 update 쿼리문이 날라가지 않기 때문이다.)

 

 

Lock

    @Lock(LockModeType.PESSIMISTIC_WRITE)
    List<Member> findByUsername(String name);
JPA가 제공하는 락은 JPA 책 16.1 트랜잭션과 락 절 참고.

깊은 내용이기 때문에 책을 참고하며 공부해야 될 것 같다.

 

 

 

 

 

출처

 

실전! 스프링 데이터 JPA - 인프런 | 강의

스프링 데이터 JPA는 기존의 한계를 넘어 마치 마법처럼 리포지토리에 구현 클래스 없이 인터페이스만으로 개발을 완료할 수 있습니다. 그리고 반복 개발해온 기본 CRUD 기능도 모두 제공합니다.

www.inflearn.com

 

+ Recent posts