Spring Data JPA는 자기 혼자 돌아가는 기술이 아니라, JPA를 사용할때 많이 도와주는 기술이다. 정말 다양한 기능을 제공한다.
다만 JPA에 대한 이해없이 Spring Data JPA만 익혀 사용한다면 문제가 생길 수 있다. 탄탄한 JPA기반 지식위에 Spring Data JPA라는 도구를 사용하는 것이 맞을 것이다.
의존성 추가
//JPA, 스프링 데이터 JPA 추가
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
//p6spy 라이브러리(query 보기 쉽게 해주는거)
implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.9.0'
Spring Data JPA는 결국 JPA를 쉽게 쓰기위한 기술.. 꼭 JPA를 익혀라
JPA를 사용하다보면 기본적인 CRUD 기능들을 중복해서 만들게 된다.
findAll(), save(), delete(), findById()등 아주 기본적인 쿼리문들을 반복해서 짜게 되는 상황이 의외로 많다.
Spring Data JPA를 사용하기위해선 @EnableJpaRepositories로 설정해야된다.
@Configuration
@EnableJpaRepositories(basePackages = "jpabook.jpashop.repository")
public class AppConfig {}
@SpringBootApplication
위치를 지정(해당 패키지와 하위 패키지 인식)@EnableJpaRepositories
필요Repository 만들어 보기
public interface MemberRepository extends JpaRepository<Member, Long> {
}
Spring Data JPA가 proxy로 구현체를 만들어서 대신 넣어준다. 엔티티와 Id의 타입을 보고 프록시 구현체를 만들어 넣어줌. 또한 코드들을 살펴보면 CRUDRepository등 여러가지에서 이미 여러가지 쿼리 기능들을 만들어 넣어놓음.
그리고 위에 Repository를 보면 @Repository
가 생략 되어 있는데 컴포넌트 스캔을 스프링 데이터 JPA가 자동으로 처리, JPA 예외를 스프링 예외로 변환하는 과정도 자동으로 처리해준다.
실제 우리가 그전에 짜보았던 순수 JPA 코드들이 기억날 것이다. 그 코드들과 거의 비슷한 코드들이 그냥 Spring Data JPA가 만들어서 넣어준다고 보면 된다.
Spring Data 분석
Spring Data Commons라는 라이브러리가 있고, 이는 Spring Data project들이 다 사용하는것.
Spring Data JPA는 Spring Data프로젝트들 중 JPA관련 라이브러리, 여기엔 Spring Data JDBC, Spring Data JPA ,Spring Data LDAP , Spring Data MongoD, Spring Data Redis, Spring Data R2DBC, Spring Data REST
등 이 외에도 더 많은 것이 있다.
Commons에는 말그대로 Spring Data에서 공통으로 사용할 수 있는 것이고, 다른 DB를 쓰더라도 상관없이 다 같이씀. JPA쪽 프로젝트는 JPA에서만 사용할 수 있는것.
Paging같은 기능들은 Commons에 있고, Spring에서 처리하기 쉽게 만들어줌.
주요 메서드
save(S) : 새로운 엔티티는 저장하고 이미 있는 엔티티는 병합한다.
delete(T) : 엔티티 하나를 삭제한다. 내부에서 EntityManager.remove() 호출
findById(ID) : 엔티티 하나를 조회한다. 내부에서 EntityManager.find() 호출
getOne(ID) : 엔티티를 프록시로 조회한다. 내부에서 EntityManager.getReference() 호출
findAll(…) : 모든 엔티티를 조회한다. 정렬( Sort )이나 페이징( Pageable ) 조건을 파라미터로 제공할 수 있다.
참고: JpaRepository 는 대부분의 공통 메서드를 제공한다.
만약 공통의 기능이 아니라 어떤 특화된 기능을 구현하고 싶다면?
일반적인 구현체와 같이 Implements를 해서 하면 될까?
이는 안된다. 기존의 공통 기능들조차도 전부 상속받아서 구현해야된다. Spring Data JPA는 다른 방식으로 이런 기능 구현을 지원을 한다.
메서드 이름으로 쿼리 생성
순수 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();
}
스프링 데이터 JPA의 쿼리 메서드
public interface MemberRepository extends JpaRepository<Member, Long> {
List<Member> findByUsernameAndAgeGreaterThan(String username, int age);
}
장점
단점
간단간단한 쿼리문들은 쿼리 메서드를 이용해서 짜고, 좀 더 복잡한 쿼리는 다른 방법을 이용해서 풀어 나갈 수 있다.
JPA의 NamedQuery를 호출할 수 있음
@NamedQuery 어노테이션으로 Named 쿼리 정의
@Entity
@NamedQuery(
name="Member.findByUsername",
query="select m from Member m where m.username = :username")
public class Member {
...
}
스프링 데이터 JPA로 NamedQuery 사용
@Query(name = "Member.findByUsername")
List<Member> findByUsername(@Param("username") String username);
@Query 생략 가능
List<Member> findByUsername(@Param("username") String username);
스프링 데이터 JPA는 선언한 "도메인 클래스 + .(점) + 메서드 이름"으로 Named 쿼리를 찾아서 실행. 만약 실행할 Named 쿼리가 없으면 메서드 이름으로 쿼리 생성 전략을 사용한다.
Named Query는 기본적으로 정적쿼리이다. 애플리케이션 로딩 시점에서 파싱해서 잘못된 문법이 있으면 바로 알려준다. 매우 큰 장점이다.
하지만.. Named Query를 사용을 안한다.
첫째, Entity에 직접 쿼리를 작성하는 점이 좋지 않음.
둘때, 이것이 더 큰 이유인데 Spring Data JPA가 비슷하면서도 더 강력한 기능을 제공한다.
메서드에 JPQL 쿼리 작성
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") intage);
}
단순 값 하나를 조회
@Query("select m.username from Member m")
List<String> findUsernameList();
JPA 값 타입( @Embedded )도 이 방식으로 조회할 수 있다.
DTO로 직접 조회
@Query("select new study.datajpa.dto.MemberDto(m.id, m.username, t.name) " +
"from Member m join m.team t")
List<MemberDto> findMemberDto();
@Query
애노테이션이니 적는 쿼리문은 JPQL과 똑같음.이름 기반 vs 위치 기반 바인딩
일단 위치기반은 사용하지 않는다. 이런 비슷한 방식으로 위치냐 이름이냐가 있는데, 거의 대부분 위치 기반은 사용하지 않는다고 보면 된다.
코드 가독성과 유지보수를 위해 이름 기반 파라미터 바인딩을 사용하자 (위치 기반은 파라미터가 추가되거나 순서가 바뀌기라도 한다면 찾기도 어렵고 큰 장애로 이어질 수 있다…)
컬렉션 파라미터 바인딩 - 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
조회 결과가 많거나 없으면?
컬렉션
단건 조회
결과 없음: null 반환 (원래 JPA는 NoResultException이 뜸. Spring Data JPA가 try-catch로 감싸서 null로 반환 해주는 것)
결과가 2건 이상: javax.persistence.NonUniqueResultException 예외 발생
(스프링이 IncorrectResultSizeDataAccessException으로 바꿔서 반환해줌)
단건 조회에서 이제 자바8 이후 Optional을 사용하면서 null이나 이런쪽에 신경을 덜 써도 되긴함. 반환타입을 보면서 확실하게 알 수 있기 때문에.
JPA로만 짠 페이징 쿼리
public List<Member> findByPage(int age, int offset, int limit) {
return em.createQuery("select m from Member m
where m.age = :age order by m.username desc")
.setParameter("age", age)
.setFirstResult(offset)
.setMaxResults(limit)
.getResultList();
}
public long totalCount(int age) {
return em.createQuery("select count(m) from Member m
where m.age = :age", Long.class)
.setParameter("age", age)
.getSingleResult();
}
setFirstResult(offset)
, setMaxResults(limit)
를 통해 바로 페이징을 할 수 있다Spring Data JPA 페이징과 정렬
페이징과 정렬 파라미터
org.springframework.data.domain.Sort
: 정렬 기능
org.springframework.data.domain.Pageable
: 페이징 기능 (내부에 Sort 포함)
특별한 반환 타입
org.springframework.data.domain.Page
: 추가 count 쿼리 결과를 포함하는 페이징
org.springframework.data.domain.Slice
: 추가 count 쿼리 없이 다음 페이지만 확인 가능(내부적으로 limit + 1조회)
List
(자바 컬렉션): 추가 count 쿼리 없이 결과만 반환
페이징 패키지들을 보면 JPA가 아니다. 즉 JPA에서만 사용한 기술은 아니고, 스프링이 DB전반적으로 추상화해서 제공하는 기술이다.
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);
Spring Data page는 페이지를 1부터가 아니라 인덱스와 같이 0부터 받음.
PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "username"))`
순서대로, page, size, sorting(빼도 됨), properties를 받음
Page<Member> page = memberRepository.findByUserName(name,pageRequest);
List<Member> content = page.getContent();
long tatalElements = page.getTotalElements();
totalCount를 받는 쿼리를 짤 필요가 없음. Page에서 알아서 totalCount를 계산해 준다.
totalCount 쿼리도 최적화되서 나감.
Page가 아닌 Slice로 받을 시 count쿼리를 사용안함.(페이지가아닌 모바일에서 화면을 쭉 내릴때, 더보기나 그런식으로 볼때 사용할 수 있는 것. 요청한것보다 하나 더 받음)
Page에서 Slice로 바꿀때 그냥 반환 타입만 바꿔줘도 됨.
totalCount 쿼리를 짤때 잘 짜야됨. 실제 쿼리랑 다를 수 있음. 실제 자료는 join을 했을수 있지만 toOne 관계에서는 어차피 row수는 같음. 그럼 totalCount는 join을 할 필요가 없음.
정렬도 위에 제공한 기능을 사용할 수 있지만 복잡해지면 따로 정렬 쿼리를 짜는게 맞다.
totalCount 쿼리를 따로 짤 수 있게 해준다.
@Query(value = "select m from Member m left join m.team t",
countQuery = "select count(m.username) from Member m")
Page<Member> findByAge(int age, Pageable pageable);
위의 Member를 받아오면서 바로 DTO로 변환할 수 있다.
Page<Member> page = memberRepository.findByAge(10, pageRequest);
Page<MemberDto> dtoPage = page.map(m -> new MemberDto());
Spring Data JPA를 잘 사용하면 JPA를 사용하는 것 보다 훨씬 편리한 기능을 제공하며, 폭발적인 생산성 향상이 일어날 수 있.
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();
}
스프링 데이터 JPA를 사용한 벌크성 수정 쿼리
@Modifying
@Query("update Member m set m.age = m.age + 1 where m.age >= :age")
int bulkAgePlus(@Param("age") int age);
@Modifying
을 달아줘야 그냥 JPA를 사용할때 나갓던 .executeUpdate();
가 적용이된다. 만약 @Modifying
이 없다면 ResultList나 singgleResult가 나간다.
@Modifying
이 없다면 DML 명령어를 날릴 수 없다고 한다.
벌크성 연산을 조심해야 되는 점. 영속성 컨텍스트를 무시하고 DB에 바로 쿼리를 날려버린다.
만약 벌크성 연산을 날리고, 바로 영속성 컨텍스트를 조회하면 벌크연산의 쿼리가 반영이 되어 있지 않다. 따라서 영속성 컨텍스트를 flush와 clear를 하고 다음 작업을 해야 됨.
Spring Data JPA는 편리하게 그런 기능을 제공한다. EntityManager를 받아서 flush와 clear를 쓰는게 번거러움. @Modifying(clearAutomatically = true)
를 사용한다면 자동으로 해준다.
연관된 엔티티들을 SQL 한번에 조회하는 방법
member team은 지연로딩 관계이다. 따라서 다음과 같이 team의 데이터를 조회할 때 마다 쿼리가 실행된다. (N+1 문제 발생)
JPQL 페치 조인을 사용하면 이런 N+1 문제에 대해 해결이 가능하다.
@Query("select m from Member m left join fetch m.team")
List<Member> findMemberFetchJoin();
Spring Data JPA를 사용할때, 메서드 이름으로 만든는 쿼리에서는 이 페치 조인을 적어줄 수 없다. 그럼 결국 JPQL을 작성해야 된다는 말인데 이때 @EntityGraph
라는 것을 이용해서 해결이 가능하다.
스프링 데이터 JPA는 JPA가 제공하는 엔티티 그래프 기능을 편리하게 사용하게 도와준다. 이 기능을 사용하면 JPQL 없이 페치 조인을 사용할 수 있다. (JPQL + 엔티티 그래프도 가능)
//공통 메서드 오버라이드
@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
, 결국 복잡하게 짤 때는 JPQL을 작성하자.JPA Hint
JPA 쿼리 힌트(SQL 힌트가 아니라 JPA 구현체에게 제공하는 힌트)
쿼리 힌트 사용
@QueryHints(value = @QueryHint(name = "org.hibernate.readOnly", value = "true"))
Member findReadOnlyByUsername(String username);
@Test
public void queryHint() throws Exception {
//given
memberRepository.save(new Member("member1", 10));
em.flush();
em.clear();
//when
Member member = memberRepository.findReadOnlyByUsername("member1");
member.setUsername("member2");
em.flush(); //Update Query 실행X
}
쿼리 힌트 Page 추가 예제
@QueryHints(value = { @QueryHint(name = "org.hibernate.readOnly",
value = "true")},
forCounting = true)
Page<Member> findByUsername(String name, Pageable pageable);
org.springframework.data.jpa.repository.QueryHints 어노테이션을 사용
forCounting : 반환 타입으로 Page 인터페이스를 적용하면 추가로 호출하는 페이징을 위한 count 쿼리도 쿼리 힌트 적용(기본값 true )
Lock
@Lock(LockModeType.PESSIMISTIC_WRITE)
List<Member> findByUsername(String name);
사용자 정의 리포지토리 구현
스프링 데이터 JPA 리포지토리는 인터페이스만 정의하고 구현체는 스프링이 자동 생성
스프링 데이터 JPA가 제공하는 인터페이스를 직접 구현하면 구현해야 하는 기능이 너무 많음
다양한 이유로 인터페이스의 메서드를 직접 구현하고 싶다면?
(JPA 직접 사용( EntityManager ) , 스프링 JDBC Template 사용 , MyBatis 사용, Querydsl 사용 등...)
사용자 정의 인터페이스
public interface MemberRepositoryCustom {
List<Member> findMemberCustom();
}
사용자 정의 인터페이스 구현 클래스
@RequiredArgsConstructor
public class MemberRepositoryImpl implements MemberRepositoryCustom {
private final EntityManager em;
@Override
public List<Member> findMemberCustom() {
return em.createQuery("select m from Member m")
.getResultList();
}
}
사용자 정의 인터페이스 상속
public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom {
}
참고: 항상 사용자 정의 리포지토리가 필요한 것은 아니다. 그냥 임의의 리포지토리를 만들어도 된다. 예를 들어 MemberQueryRepository를 인터페이스가 아닌 클래스로 만들고 스프링 빈으로 등록해서 그냥 직접 사용해도 된다. 물론 이 경우 스프링 데이터 JPA와는 아무런 관계 없이 별도로 동작한다.
엔티티를 생성, 변경할 때 변경한 사람과 시간을 추적하고 싶으면?
테이블에서는 테이블 마다 다루었지만, 객체지향적인 세상에서는 상속과 이런것을 통해 가능.
순수 JPA 사용
우선 등록일, 수정일 적용
package study.datajpa.entity;
@MappedSuperclass //
@Getter
public class JpaBaseEntity {
@Column(updatable = false)
private LocalDateTime createdDate;
private LocalDateTime updatedDate;
@PrePersist
public void prePersist() {
LocalDateTime now = LocalDateTime.now();
createdDate = now;
updatedDate = now;
}
@PreUpdate
public void preUpdate() {
updatedDate = LocalDateTime.now();
}
}
public class Member extends JpaBaseEntity {}
JPA 주요 이벤트 어노테이션
@PrePersist
, @PostPersist
- persist전, persist후
@PreUpdate
, @PostUpdate
- update전, update후
스프링 데이터 JPA 사용
설정
@EnableJpaAuditing 스프링 부트 설정 클래스에 적용해야함
@EntityListeners(AuditingEntityListener.class) 엔티티에 적용
사용 어노테이션
@CreatedDate
@LastModifiedDate
@CreatedBy
@LastModifiedBy
package jpabook.jpashop.domain;
@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
public class BaseEntity {
@CreatedDate
@Column(updatable = false)
private LocalDateTime createdDate;
@LastModifiedDate
private LocalDateTime lastModifiedDate;
@CreatedBy
@Column(updatable = false)
private String createdBy;
@LastModifiedBy
private String lastModifiedBy;
}
등록자, 수정자를 처리해주는 AuditorAware 스프링 빈 등록
@EnableJpaAuditing
@SpringBootApplication
public class DataJpaApplication {
public static void main(String[] args) {
SpringApplication.run(DataJpaApplication.class, args);
}
@Bean
public AuditorAware<String> auditorProvider() {
return () -> Optional.of(UUID.randomUUID().toString());
}
}
Spring data JPA를 사용하면?
@GetMapping("/members/{id}")
public String findMember(@PathVariable("id") Long id) {
Member member = memberRepository.findById(id).get();
return member.getUsername();
}
도메인 클래스 컨버터
@GetMapping("/members/{id}")
public String findMember(@PathVariable("id") Member member) {
return member.getUsername();
}
객체로 찾아돋 컨버터를 이용해 매칭해준다. 원래는 다양한 설정들을 등록해줘야 하는데 , 스프링 부트를 사용하면 자동으로 등록이 되어 있다.
주의: 도메인 클래스 컨버터로 엔티티를 파라미터로 받으면, 이 엔티티는 단순 조회용으로만 사용해야 한다.
(트랜잭션이 없는 범위에서 엔티티를 조회했으므로, 엔티티를 변경해도 DB에 반영되지 않는다.)
크게 사용하지 않는것이 좋음.
@GetMapping("/members")
public Page<Member> list(Pageable pageable) {
Page<Member> page = memberRepository.findAll(pageable);
return page;
}
/members?page=0size=10&sort=id,desc&sort=username,desc
이런 식으로, 페이지 , 사이즈, sort(여러개)를 넣을 수 있다.
글로벌 설정
spring.data.web.pageable.default-page-size=20 /# 기본 페이지 사이즈/
spring.data.web.pageable.max-page-size=2000 /# 최대 페이지 사이즈/
개별 설정 - @PageableDefault
이용
public String list(@PageableDefault(size = 10, sort = “username”,
direction = Sort.Direction.DESC) Pageable pageable) {
...
}
스프링 데이터는 Page를 0부터 시작한다
Page를 1부터 시작하려면?
spring.data.web.pageable.one-indexed-parameters
를 true 로 설정한다. 그런데 이 방법은 web에서 page 파라미터를 -1 처리 할 뿐이다. 따라서 응답값인 Page 에 모두 0 페이지 인덱스를 사용하는 한계가 있다.스프링 데이터 JPA가 제공하는 공통 인터페이스의 구현체
org.springframework.data.jpa.repository.support.SimpleJpaRepository
클래스파일에 들어가 코드를 살펴보면 @Repository
도 있고, @Transactional(readOnly=true)
등이 클래스에 걸려있다.
@Repository 적용: JPA 예외를 스프링이 추상화한 예외로 변환
@Transactional 트랜잭션 적용
public <S extends T> S save(S entity) {
if (entityInformation.isNew(entity)) {
em.persist(entity);
return entity;
} else {
return em.merge(entity);
}
}
*save() 메서드+영속상태의 엔티티가 어떤 이유로 영속상태를 벗어낫을때 다시 영속 상태가 되어야 될때 사용하는 것이다. 데이터 업데이트할때 쓰는게 아님.(데이터 업데이트에 사용할 수도 있어서 그렇게 사용하면 안된다는 것)
새로운 엔티티를 판단하는 기본 전략
public class Item implements Persistable<String> {
@Id
private String id;
@CreatedDate
private LocalDateTime createdDate;
public Item(String id) {
this.id = id;
}
@Override
public String getId() {
return id;
}
@Override
public boolean isNew() {
return createdDate == null;
}
}
식별자 생성 전략을 개발자가 직접 생성해서 넣어 줄때(generatedValue 사용 x) -> merge가 사용이 된다. why? 이미 엔티티 Id에 값을 넣어줬으므로, save에서 id 값이 not null이므로 merge를 호출한다.
merge() 는 우선 DB를 호출해서 값을 확인하고, DB에 값이 없으면 새로운 엔티티로 인지하므로 매우 비효율 적이다
이것을 해결 하기 위해 persistable을 상속받아 해결 가능. persistable의 isNew를 정의해서 null로 만들어 준다.(이때 주로 createdDate를 사용. 생성 날짜는 null이고, 비교하기 좋은 데이터)
엔티티 대신에 DTO를 편리하게 조회할 때 사용
전체 엔티티가 아니라 만약 회원 이름만 딱 조회하고 싶으면?
public interface UsernameOnly {
String getUsername();
}
public interface MemberRepository ... {
List<UsernameOnly> findProjectionsByUsername(String username);
}
select m.username from member m
where m.username=‘m1’;
다음과 같이 스프링의 SpEL 문법도 지원
public interface UsernameOnly {
@Value("#{target.username + ' ' + target.age + ' ' + target.team.name}")
String getUsername();
}
target = uername teamA