스프링 데이터 JPA는 페이징을 쉽게 할 수 있게 도와주는 Pageable
인터페이스를 제공한다.
우리는 PageRequest
구현체를 통해 페이징 설정을 할 수 있다.
인덱스는 0부터 시작한다.
// UserService
int age = 10;
PageRequest page = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "username"));
Page<User> result = userRepository.findAllByAge(age, page);
List<User> users = result.getContent(); // 유저 리스트를 반환
long totalCount = result.getTotalElement(); // 총 개수를 반환
// UserRepository
Page<User> findAllByAge(int age, Pageable pageable);
다음 페이지 여부를 반환한다.
전체 페이지 정보는 갖고 있지 않는다.
limit + 1
을 가져온다.
예제의 경우 4 (3+1)
개를 가져온다!
// UserService
int age = 10;
PageRequest page = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "username"));
Slice<User> result = userRepository.findAllByAge(age, page); // Slice
List<User> users = result.getContent(); // 유저 리스트를 반환
// 해당 기능 없음. long totalCount = result.getTotalElement();
// UserRepository
Slice<User> findAllByAge(int age, Pageable pageable); // Slice
이름기반과 위치기반이 있다. 보통 이름기반을 많이 사용한다.
컬렉션 파라미터 바인딩 + 이름기반으로 쿼리도 사용할 수 있다.
@Query("select m from Member m where m.username in :names")
List<Member> findAllByNames(@Param("names") List<String> names);
값을 찾을 수 없다면, 순수 JPA는 NoResultException
예외를 발생시킨다.
스프링 데이터 JPA는 내부적으로 try-catch로 감싸서 null
로 반환한다.
자바8 이후부터는 Optional
을 사용한다!
User user = userRepository.findById(id);
System.out.println("findUser = " + user);
// Spring Data JPA : findUser = null (없다면)
// 순수 JPA : Exception (없다면)
회원의 나이를 윤석열 나이로 일괄 변경해야 한다고 가정해보자.
// userService
int bulkAgeUpdateCount = bulkUpdateAgeByYoon(1); // 1살 이상인 경우에만 윤석열 나이 적용
// userRepository
@Modifying(clearAutomatically = true) // ✅
@Query("update User u set u.age = u.age - 1 where u.age >= :age")
int bulkUpdateAgeByYoon(@Param("age") int age);
update쿼리에는 @Modifying
어노테이션을 넣어주어야 한다.
💡 주의해야할 점은
이러한 벌크 쿼리는 영속성 컨텍스트를 무시하고 직접 DB에 반영된다.
즉, 아직 flush되지 않은 영속 상태의 엔티티는 이 변경 내용이 반영되지 않는다.이를 해결하려면
@Modifying(clearAutomatically = true)
옵션을 활용해야 한다.
이 옵션을 설정하면 쿼리 실행 이후 clear()가 자동 호출되어 영속성 컨텍스트가 초기화되고, 이후 조회 시 DB에서 다시 최신 상태의 데이터를 불러올 수 있다.bulkUpdateAgeByYoon() 메서드 실행: DB에 직접 UPDATE 쿼리 실행 ↓ JPQL 기반 쿼리이므로 flush() 자동 호출 ↓ clearAutomatically = true: 영속성 컨텍스트 clear() ↓ 향후 조회 시 DB에서 변경된 엔티티를 새로 로딩
N+1문제를 해결하는데 도와주는 어노테이션
class User {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name ="team_id")
Team team = new ArrayList<>();
}
List<User> users = userRepository.findAll();
for(User user : users) {
// N + 1 문제 발생
System.out.println("team 이름: " + user.getTeam().getName());
}
User(N):Team(1)
관계에서 fetch = LAZY로 설정되어 있다고 가정해보자.
처음에 User를 조회하면, 1번의 select 쿼리로 N개의 결과(User)가 나타난다.
이때 team은 지연 로딩(Lazy)으로 설정되어 있어, 각 User의 team 필드는 프록시 객체로 채워진다.
그런데 user.getTeam().getName()
처럼 실제 Team 정보에 접근하면,
각각의 User에 대해 별도의 쿼리가 추가로 발생하게 된다.
결국 처음 1번의 조회 쿼리 + N번의 팀 조회 쿼리가 실행된다.
이러한 문제를 N+1문제라고 할 수 있다.
// UserRepository
@Query("select u from User u join fetch u.team")
List<User> findAll();
FetchJoin은 JPA가 지원하는 기능이다.
위 코드는 User와 관련있는 Team 객체도 한꺼번에 조회하는 쿼리이다.
❓ JOIN FETCH vs LEFT JOIN FETCH ?
- team이 무조건 있는 경우 → JOIN FETCH
- team이 없을 수도 있는 경우 → LEFT JOIN FETCH
@EntityGraph
로도 N+1 문제를 해결해보자public class UserRepository extends JpaRepository<User, Long> {
...
@Override
@EntityGraph(attributePaths = {"team"}
List<User> findAll();
}
내부적으로 left fetch join
을 사용한다.
간편한 쿼리의 경우 해당 어노테이션을 사용하면 되고,
쿼리가 복잡해지면 JPQL 내에서 fetch join
을 사용하면 된다.