-스프링 데이터 JPA를 사용한 벌크성 수정 쿼리

@Modifying
@Query("update Member m set m.age = m.age + 1 where m.age >= :age")
int bulkAgePlus(@Param("age") int age);

 

벌크성 수정, 삭제 쿼리는 @Modifying 어노테이션을 사용해야한다

사용하지 않으면 다음과 같은 오류가 발생한다 

org.hibernate.hql.internal.QueryExecutionRequestException: Not supported for DML operations
 

주의:

벌크 연산은 영속성 컨텍스트를 무시하고 바로 db 값을 업데이트 하기 때문에, 같은 객체에 대해서 디비와 영속성 컨텍스트의 값이 다를 있다

 

해결:

1. 영속성 컨텍스트에 엔티티가 없는 상태에서 벌크 연산을 먼저 실행한다

2. 부득이하게 영속성 컨텍스트에 엔티티가 있으면 벌크 연산 직후 영속성 컨텍스트를 초기화 한다 

 

스냅샷: 영속성 컨텍스트가 생성될 때, 향후 변경 감지를 위해서 원본을 복사해서 만들어두는 객체 

변경 감지가 일어났을 때 1차 캐시에 있는 원본 객체가 중간에 변경되었는지 스냅샷을 통해 확인 

실시간 트래픽이 많은 상황에서는 Lock 되도록 걸면 안된다 

추가로 페이징 공부 참고한 블로그

-페이징, 정렬 파라미터

정렬 기능: org.springframework.data.domain.Sort

페이징 기능(내부에 sort를 포함): org.springframework.data.domain.Pageable

-특별한 반환 타입(Page,Slice,List)

Page: 추가 count 쿼리를 포함하는 페이징이다. 쿼리에 limit을 하기 위한 것이고, count는 반환 타입에서 결정된다 

 

Slice: 추가 count 쿼리 없이 다음 페이지만 확인가능(내부적으로 Limit+1 을 조회한다)

주로 모바일에서 더보기 기능에 사용하고, totalCount를 계산함으로써 성능이 저하돼서 필요없을 때 사용한다 

 

List(자바 컬렉션): 추가 count 쿼리 없이 결과만 반환한다 

 

참고: 

Pageable은 인터페이스이고, 페이지에서 나온 파라미터 정보를 포함한다

Page는 인터페이스이고, 결과 정보를 포함한다 

 

Page<Member> findByUsername(String name, Pageable pageable); //count 쿼리 사용 
Slice<Member> findByUsername(String name, Pageable pageable); //count 쿼리 사용 안함
List<Member> findByUsername(String name, Pageable pageable); //count 쿼리 사용 안함
List<Member> findByUsername(String name, Sort sort);

pageable에 offset과 limit이 있어서 이를 기준으로 페이징을 진행한다.

 

Pageable은 인터페이스라서 이를 구현한 객체인 PageRequest를 이용한다. 참고로 pagerequest에 담는 정보에 정렬정보도 추가할 수 있으며, 페이지 기본 시작은 0페이지에서부터 시작한다. 1페이지부터 시작이 아니다. 

 

작성 방법은 다음과 같다 

PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC,"username"));
Page<Member> page = memberRepository.findByAge(10, pageRequest)

List<Member> content = page.getContent(); //조회된 데이터 
assertThat(content.size()).isEqualTo(3); //조회된 데이터 수, content가 list라서 size만 사용할 수 있다

//page는 slice를 상속받기때문에 다음과 같은 기능을 사용할 수 있다
assertThat(page.getTotalElements()).isEqualTo(5); //전체 데이터 수
assertThat(page.getNumber()).isEqualTo(0); //페이지 번호 
assertThat(page.getTotalPages()).isEqualTo(2); //전체 페이지 번호 
assertThat(page.isFirst()).isTrue(); //첫번째 항목인가? 
assertThat(page.hasNext()).isTrue(); //다음 페이지가 있는가?

-Page 인터페이스

public interface Page<T> extends Slice<T> {
int getTotalPages(); //전체 페이지 수
long getTotalElements(); //전체 데이터 수
<U> Page<U> map(Function<? super T, ? extends U> converter); //변환기
}

 

-Slice 인터페이스 

public interface Slice<T> extends Streamable<T> {
 int getNumber();
int getSize();
int getNumberOfElements();
List<T> getContent();
boolean hasContent();
Sort getSort();
boolean isFirst();
boolean isLast();
boolean hasNext();
boolean hasPrevious();
Pageable getPageable();
Pageable nextPageable();
Pageable previousPageable();//이전 페이지 객체
<U> Slice<U> map(Function<? super T, ? extends U> converter); //변환기
}

 

-count쿼리를 다음과 같이 분리할 수 있다. 복잡한 sql에서 사용해야하며, 데이터는 Left join을 하고, 카운트는 Left join 안 해도 된다. 참고로 전체 count 쿼리는 매우 무겁다

@Query(value = “select m from Member m”,
       countQuery = “select count(m.username) from Member m”)
Page<Member> findMemberAllCountBy(Pageable pageable);

- 파라미터 바인딩에는 위치기반 바인딩과 이름 기반 바인딩이 있다

코드 가독성과 유지보수를 위해 이름 기반 파라미터 바인딩을 사용하자. 위치기반은 실수로 위치가 바뀔 경우 큰 문제가 발생할 수 있다. 

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<Member> findByNames(@Param("names") List<String> names);

 

-반환 타입

스프링 데이터 JPA는 유연한 반환 타입을 지원한다 

List<Member> findByUsername(String name); //컬렉션 
Member findByUsername(String name); //단건 
Optional<Member> findByUsername(String name); //단건 Optional

 

List는 찾으려는 객체가 없더라도 빈 객체를 반환하므로 컬렉션은 어떤 상황에서 사용해도 괜찮다 

만약 데이터를 조회했는데 데이터가 있을 수도 없을 수도 있을 Optional 사용하면 된다 

 

참고: 단건으로 지정한 메서드를 호출하면 jpql의 Query.getSingleResult() 메서드를 호출하는데, 이때 결과가 없으면 

javax.persistance.NoResultException 예외가 발생하는데, 이는 개발자 입장에서 다루기가 상당히 불편하다

스프링 데이터 JPA는 단건을 조회할 때 위 예외가 발생해도 예외를 무시하고 대신 null을 반환한다 

 

 

ItemRepository 인터페이스가 JpaRepository를 구현하면 Spring Data Jpa가 ItemRepository 구현 클래스를 대신 생성한다.

이때 ItemRepository는 프록시 클래스라서 작동한 것이다 

@Repository를 생략가능한 이유는 스프링 데이터 JPA가 컴포넌트 스캔을 자동으로 처리해준다

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

 

-JpaRepository에서 제공하는 공통 메서드 

-스프링 데이터 JPA에서 제공하는 3가지 기능

1. 메소드 이름으로 쿼리 생성: find....byxxxx(xxxx를 기반으로 쿼리를 실행한다)

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

1) 조회: find..By, read...By, query...By, get...By

2) count: count...By, 반환타입은 long이다 

3) exists: exits...By, 반환타입은 boolean이다 

4) delete: delete...By, remove...By, 반환타입은 long이다

5) distinct: findDistinct, findMemberDistincBy, 중복제거해서 객체 조회?

6) limit: findFirst3, findFirst, findTop, findTop3

 

참고: 엔티티의 필드명이 변경되면 인터페이스에 정의된 메서드 이름도 꼭 같이 변경해줘야 한다. 그렇지 않으면 애플리케이션을 실행하는 시점에 오류가 발생할 수 있다. 애플리케이션 로딩 시점에 오류를 인지할 수 있는 것이 스프링 데이터 JPA의 큰 장점이다 

2. 메소드 이름으로 JPA NamedQuery 호출

실무에서 사용하는 일은 드물기 때문에 밑의 3번 방법을 사용하는 것을 권장한다.

그래도 애플리케이션 실행 시점에 문법 오류를 발견할 수 있는 매우 큰 장점이 있다. 

@Entity
    @NamedQuery(      
           name="Member.findByUsername",
          query="select m from Member m where m.username = :username")
  public class Member {
... }
@Query(name = "Member.findByUsername")
  List<Member> findByUsername(@Param("username") String username);

 

3. @Query 어노테이션을 사용해서 Repository interface에 쿼리를 직접 정의함 

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") int age);
    
}

실행할 메서드에 정적 쿼리를 직접 작성하므로 이름 없는 Named 쿼리라 할 수 있다 

JPA Named 쿼리처럼 애플리케이션 실행 시점에 문법 오류를 발견할 수 있는 매우 큰 장점이 있다. 

+ Recent posts