스프링 데이터 JPA란?
스프링 프레임워크에서 JPA를 편리하게 사용할 수 있도록 지원하는 프로젝트. 데이터 접근 계층을 개발할 때 지루하게 반복되는 CRUD 문제를 더 깔끔한 방법으로 해결한다. CRUD를 처리하기 위한 공통 인터페이스를 제공하고 리포지토리를 개발할 때 인터페이스만 작성하면, 실행 시점에 스프링 데이터 JPA가 구현 객체를 동적으로 생성하여 주입한다.
데이터 접근 계층을 개발할 때 구현 클래스 없이 인터페이스만 작성하여 사용이 가능하다.
- 스프링 데이터 JPA 라이브러리 추가
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
// ...
}
공통 인터페이스
- 스프링 데이터 JPA 적용
package com.ys.test.lab.api.repository;
import com.ys.test.lab.api.entity.UserEntity;
import org.springframework.data.jpa.repository.JpaRepository;
// JPA Repository 준비끝!
public interface UserRepository extends JpaRepository<UserEntity, Long> {
UserEntity findByNname(String name);
}
JpaRepository 부분을 보면 제네릭에 UserEntity, 식별자 타입을 지정했다. JpaRepository 인터페이스가 제공하는 다양한 기능을 이용할 수 있다. 아래의 그림은 JpaRepository 인터페이스의 계층구조이다.
윗부분에 스프링 데이터 모듈이 있고 CrudRepository, PagingAndSortingRepository가 있는데, 스프링 데이터 프로젝트가 공통으로 사용하는 인터페이스다. 여기에 JpaRepository 인터페이스는 추가로 JPA에 특화된 기능을 더 제공해준다.
- JpaRepository의 주요메소드
메소드 | 설명 |
save([Entity or ChildEntity]) | 새로운 엔티티는 저장하고 이미 있는 엔티티는 수정 (엔티티에 식별자 값이 없으면 새로운 엔티티로 인식하여 EntityManager.persist() / 식별자가 있으면 EntityManager.merge()) |
delete(Entity) | 엔티티 하나를 삭제. 내부에서 EntityManager.remove()가 호출됨 |
findOne(ID) | 엔티티 하나를 조회. 내부에서 EntityManager.find()가 호출됨 |
getOne(ID) | 엔티티를 프록시로 조회. 내부에서 EntityManager.getReference()가 호출됨 |
findAll() | 모든 엔티티를 조회. 정렬(Sort)이나 페이징(Pagable) 조건을 파라미터로 제공이 가능 |
=> JpaRepository 인터페이스는 일반적인 CRUD를 해결해준다.
쿼리 메소드
- 메소드 이름으로 쿼리 생성
: 위의 예제에서 인터페이스에 findByName() 메소드를 정의하였다. 메소드를 실행하면 JPA는 메소드 이름을 분석해서 JPQL을 생성하고 실행한다. 이 방법은 이름을 마음대로 지어서 쿼리가 만들어지는게 아니라, 일정한 정해진 규칙이 있다.
스프링 데이터 JPA 쿼리 생성 기능 공식문서 (SpringBoot-Data-JPA 2.5.5 ver 기준)
- 메소드 이름으로 JPA NamedQuery 호출
@Entity
@NamedQuery(
name = "UserEntity.findByName",
query = "SELECT u FROM User u WHERE u.name = :name"
)
@Table(name = "user")
public class UserEntity {
// ...
}
// ---------------------------------------
public interface UserRepository extends JpaRepository<UserEntity, Long> {
// 정의해둔 NamedQuery를 호출
List<UserEntity> findByName(@Param("name") String name);
}
- JPA는 [ 도메인 클래스 + . + 메소드 이름 ] 으로 Named쿼리를 찾아서 실행한다. (이것도 또한 NamedQuery를 사용하기 위한 규칙이다) 만약 Named쿼리가 없으면 메소드 이름으로 쿼리 생성전략을 사용한다 (전략을 바꾸는 것도 가능)
- @Query 어노테이션을 사용해서 리포지토리 인터페이스에 쿼리 직접 정의
public interface UserRepository extends JpaRepository<UserEntity, Long> {
// JPQL Query
@Query("SELECT u FROM User u WHERE u.name = ?1")
UserEntity findByName(String name);
// Native Query
@Query("SELECT * FROM User WHERE name = ?0", nativeQuery = true)
UserEntity findByName2(String name);
}
네이티브 SQL도 작성할 수 있도록 지원한다. 이때는 nativeQuery 옵션을 true로 한다. JPQL 문법과 다른부분은 위치기반 파라미터의 시작이 0이라는 점이다. (가급적이면 이름 기반 파라미터를 사용하는게 가독성에 좋을듯)
- 벌크성 수정 쿼리
@Modifying(clearAutomatically = true)
@Query("UPDATE Product p SET p.price = p.price * 1.1 WHERE p.stockAmount < :stockAmount")
int bulkPriceUp(@Param("stockAmount") String stockAmount);
=> 스프링 데이터 JPA에서 벌크성 수정, 삭제 쿼리는 @Modifying 어노테이션을 사용하면 된다. 벌크성 쿼리를 실행하고나서 영속성 컨텍스트를 초기화하고 싶으면 clearAutomatically옵션을 true로 설정한다. (default: false)
반환타입
SELECT 결과가 단건이면 객체를, 여러건이라면 컬렉션 인터페이스를 사용하는 등 유연하게 반환해준다.
- List<UserEntity> findByName(String name); // 여러건. 결과데이터가 없는경우 빈컬렉션 리턴
- UserEntity findById(Long id); // 단건. 결과데이터가 없는경우 null 리턴
페이징과 정렬
스프링 데이터JPA는 쿼리 메소드에 페이징과 정렬 기능을 사용할 수 있도록 파라미터를 제공한다.
1] Sort: 정렬 기능
2] Pageable: 페이징 기능(Sort 포함)
// count 쿼리 사용
Page<UserEntity> findByName(String name, Pageable pageable);
// count 쿼리 사용안함
List<UserEntity> findByName(String name, Pageable pageable);
List<UserEntity> findByName(String name, Sort sort);
- Page 사용 예제 코드
public interface UserRepository extends JpaRepository<UserEntity, Long> {
Page<UserEntity> findByNameStartingWith(String name, Pageable pageable);
}
public class UserService {
private final UserRepository userRepository;
@Autowired
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public void test() {
// Pagable 인터페이스의 구현체인 PageRequest를 사용 (현재패이지, 한페이지내 조회수, 정렬정보)
// 페이지는 0부터 시작함.
PageRequest pageRequest = new PageRequest(0, 10, new Sort(Direction.DESC, "name"));
Page<UserEntity> result = userRepository.findByNameStartingWith("장", pageRequest);
List<UserEntity> users = result.getContent(); // 조회된 데이터
int totalPages = result.getTotalPages(); // 전체 페이지 수
boolean hasNextPage = result.hasNextPage(); // 다음 페이지 존재 여부
}
}
- Page 인터페이스
: 이 인터페이스를 사용하여 JPA 조회 데이터에 페이징, 정렬기능을 손쉽게 사용할 수 있다.
public interface Page<T> extends Iterable<T> {
int getNumber(); // 현재 페이지
int getSize(); // 페이지 크기
int getTotalPages(); // 전체 페이지 수
int getNumberOfElements(); // 현재 페이지에 나올 데이터 수
long getTotalElements(); // 전체 데이터 수
boolean hasPreviousPage(); // 이전 페이지 여부
boolean isFirstPage(); // 현재 페이지가 첫 페이지 인지 여부
boolean hasNextPage(); // 다음 페이지 여부
boolean isLastPage(); // 현재 페이지가 마지막 페이지 인지 여부
Pageable nextPageable(); // 다음 페이지 객체, 다음 페이지가 없으면 null 반환
Pageable previousPageable(); // 다음 페이지 객체, 이전 페이지가 없으면 null 반환
List<T> getContent(); // 조회된 데이터
boolean hasContent(); // 조회된 데이터 존재 여부
Sort getSort(); // 정렬 정보
}
- 페이징과 정렬 기능 - 스프링 MVC
스프링 데이터가 제공하는 페이징과 정렬 기능을 스프링 MVC에서 편리하게 사용할 수 있도록 HandlerMethodArgumentResolver를 제공.
- 페이징 기능: PageableHandlerMethodArgumentResolver
- 정렬 기능: SortHandlerMethodArgumentResolver
- Pageable 객체는 Request시 page, size, sort 요청 파라미터를받아서 처리할 수 있다.
// ex) http://localhost:8080/users?page=0&size=20&sort=name,desc&sort=address.city
@RestController
public clsss UserController {
private final UserService userService;
@Autowired
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping(value = "/users")
public List<UserEntity> userList(Pageable pageable) {
Page<UserEntity> page = userService.findUsers(pageable);
return page.getContent();
}
}
(!) 페이지를 1부터 시작하고 싶으면 두가지 방법으로 설정을 변경해준다.
1] yml, yaml 파일에 외부설정 추가
data.web.pageable.one-indexed-parameters = true
2] implements WebMvcConfigurer 하여 Configuration 어노테이션으로 설정값 Bean에 등록
설정방법 링크: http://devstory.ibksplatform.com/2020/03/spring-boot-pageable.html
- 접두사
: 사용해야 할 페이징 정보가 둘 이상이면 접두사를 사용해서 구분할 수 있다. @Qualifier 사용 {접두사명}_ 로 구분한다.
public String list {
@Qualifier("user") Pageable userPageable,
@Qualifier("order") Pageable orderPageable
}
// ex) /users?user_page=0&order_page=1
- @PageableDefault
PageableDefault어노테이션을 사용하여 기본값 설정 변경이 가능하다. (default는 page=1, size=20)
- 송성: page, size, sort, direction
명세(Specification)
: 명세를 이해하기 위한 핵심 단어는 술어(Predicate)인데 이것은 단순히 참과 거짓으로 평가된다. 그리고 이것은 AND, OR 같은 연산자로 조합할 수 있다. Specification은 컬포지트 패턴으로 구성되어 있어서 여런 Specification을 조합할 수 있으므로 다양한 검색조건을 조립해서 새로운 검색조건을 만들 수 있다. 아래의 참고 외부링크로 예시를 확인해보자.
예시 참고링크: https://groti.tistory.com/49
사용자 정의 리포지토리 구현
JPA 리포지토리 외에 직접 메소드를 구현할 수 있다.
- 사용방법
1] 사용자 정의 인터페이스(interface) 생성
public interface UserRepositoryCustom {
public List<UserEntity> findUserCustom();
}
2] 사용자 정의 구현 클래스 생성
- 클래스명 생성 규칙이 존재한다. [ 리포지토리인터페이스명(UserRepository) + "Impl" ] 이어야 한다. 이렇게 이름이 지어져야 JPA가 사용자 정의 구현 클래스로 인식한다
public class UserRepositoryImpl implements UserRepositoryCustom {
@Override
public List<UserEntity> findUserCoutom() {
// ... 사용자정의
}
}
// 사용자 정의 인터페이스 상속
public interface UserRepository extends JpaRepository<UserEntity, Long>, UserRepositoryCustom {
}
스프링 데이터 JPA와 QueryDSL 통합
- https://data-make.tistory.com/626
참고자료
- 서적 - 자바 ORM 표준 JPA 프로그래밍 제 12장 - 김영한 지음
- https://groti.tistory.com/49 - 명세(Specification)
- http://devstory.ibksplatform.com/2020/03/spring-boot-pageable.html - Pageable 설정관련
- https://data-make.tistory.com/626 - 스프링 데이터 JPA + QueryDSL
'SpringFramework > JPA' 카테고리의 다른 글
JPA - 트랜잭션과 락 (+격리수준) (0) | 2021.11.29 |
---|---|
JPA - N+1 문제 (0) | 2021.11.29 |
JPA - QueryDSL (0) | 2021.11.19 |
JPA - Criteria (0) | 2021.11.19 |
JPA - 객체지향 쿼리 언어, JPQL (0) | 2021.11.17 |