개발놀이터

Spring Data JPA 본문

JPA/JPA

Spring Data JPA

마늘냄새폴폴 2021. 9. 23. 02:00

*스프링 데이터 JPA


*공통 인터페이스
1. 인터페이스로 repository를 만든다
2. JpaRepository를 상속받는다.

상속 받을 때 public interface MemberRepository extends JpaRepository<Member, Long> 이렇게 상속하는데 제네릭 첫번 째 부분은 매핑할 엔티티이고 두번 째는 해당 엔티티의 PK타입이다. 


*메소드 이름으로 쿼리 생성
메소드 이름을 분석해서 JPQL 쿼리를 실행해주는 기능으로 파라미터가 많지않을 경우 (한두개) 해당 기능을 사용하면 좋다. 

public List<Member> findByName(String name); 이렇게 선언만 해주면 스프링 데이터 JPA가 메소드 이름을 분석해 JPQL을 작성해준다. 
위의 예제는 select m from Member m where m.name = :name 이다. 


*리포지토리 메소드에 쿼리 정의하기 
인터페이스 안에서 @Query 어노테이션을 사용하면 된다. 
@Query("select m from Member m where m.name = :name and m.age = :age")
List<Member> findMember(@Param("name") String name, @Param("age") int age);


*@Query로 값, DTO 조회하기
-단순히 값 하나를 조회할 때
@Query("select m.name from Member m")
List<String> findNameList();

-DTO로 직접 조회할 때
@Query("select new study.datajpa.dto.MemberDto(m.id, m.name, t.name) from Member m join m.team t)
List<MemberDto> findMemberDto();

DTO로 직접 조회할 때에는 JPA의 new 명령어를 사용해야 한다. 그리고 다음과 같이 생성자가 맞는 DTO가 필요하다.


*파라미터 바인딩
-위치기반
select m from Member m where m.name = ?0
-이름기반
select m from Member m where m.name = :name

코드의 가독성과 유지보수를 위해 이름기반 파라미터 바인딩을 사용해야 한다.

cf) 컬렉션 파라미터 바인딩
컬렉션 타입으로 in절을 지원한다.
ex)
@Query("select m from Member m where m.name in :names")
List<Member> findByNames(@Param("names") List<String> names);

실제로 사용할 때에는 
List<Member> result = memberRepository.findByNames(Arrays.asList("AAA", "BBB")); 이렇게 사용한다.


*스프링 데이터 JPA의 반환타입
스프링 데이터 JPA는 유연한 반환 타입을 지원한다.
스프링 데이터 JPA 공식 문서: https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#repository-query-return-types

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


*스프링 데이터 JPA의 페이징
특별한 반환 타입
-org.springframework.data.domain.Page : totalCount쿼리 결과를 포함하는 페이징
-org.springframework.data.domain.Slice : totalCount쿼리 결과를 포함하지 않는 페이징 (다음 페이지 확인가능, 내부적으로 limit + 1조회)

Slice의 사용처
요즘 스마트폰에 많이 있는 기능으로 10개 조회하고 뒤에 더보기 버튼이 있는데 그 더보기 버튼을 구현할 때 사용한다. 

사용방법
Page<Member> findByAge(int age, Pageable pageable);

Pageable의 구현체로 PageRequest를 사용한다.

PageRequest page = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "name"));

PageRequest.of의 속성 PageRequest.of(offset, limit, sorting)

page.getContent() //List타입으로 꺼내올 수 있다.
page.getTotalElements() //총 개수가 몇개인지
page.getNumber() //페이지 번호
page.getTotalPages() //페이지 개수
page.isFirst() //페이지가 첫번째인지 아닌지
page.hasNext() //페이지의 다음이 있는지
등등 사용할 수 있다. 

cf) count 쿼리를 다음과 같이 분리할 수 있다.
@Query(value = "select m from Member m left join m.team t", countQuery = "select m from Member m")
Page<Member> findMemberAndTeam(Pageable, pageable);
count쿼리를 분리하는 이유 : count의 개수가 JPQL이 가져온 개수와 같을 때 countQuery가 없으면 count쿼리도 조인을 일일이 한다. 이는 곧 성능 저하로 이어질 수 있다. 그렇기 때문에 countQuery로 좀 단순한 쿼리를 만들어서 성능 저하를 피할 수 있다. 

cf) 페이지를 유지하면서 엔티티를 DTO로 변환하기
Page, Slice, List 모두 스트림의 map과 같은 map을 제공한다.
page.map(m -> new MemberDto(m.getId(), m.getName(), m.getTeam());


*스프링 데이터 JPA를 사용한 벌크성 수정 쿼리
벌크성 수정 쿼리란?
update문을 날리면 영속성컨텍스트는 하나하나 일일이 update문을 날린다. 이는 데이터의 개수가 많아지면 엄청난 성능 저하로 이어진다. 그렇기 때문에 한방에 쿼리를 날려주는 기능을 제공하는데 그것이 바로 벌크성 수정 쿼리이다.

사용법
@Transactional
@Modifying(clearAutomatically = true)
@Query("update Member m set m.age = m.age + 1 where m.age >= :age)
int bulkAgePlus(@Param("age") int age);

Modifying해주는 이유 : Modifying이 없으면 해당 쿼리를 벌크 연산으로 인식을 못한다.

clearAutomatically 는 뭔가? : 벌크 연산에서의 가장 조심해야할 부분은 벌크 연산은 영속성 컨텍스트를 무시하고 곧장 DB로 달려가서 데이터를 업데이트 하는 것이기 때문에 영속성 컨텍스트가 데이터의 변동을 눈치채지 못한다. 데이터의 변동을 눈치채지 못한 영속성 컨텍스트는 해당 데이터의 값을 업데이트된 값이 아닌 이전에 등록한 값을 가지고 있기 때문에 영속성 컨텍스트를 깨끗하게 비우고 다시 작업을 해야한다. 


*@EntityGragh
페치조인을 좀 쉽게 할 수 있는 장치이다. 하지만 직접 쿼리를 적는것이나 엔티티그래프를 사용하는것이나 크게 차이는 없으니 편한대로 사용하면 된다.

원래 페치조인 사용법
@Query("select m from Member m join fetch m.team t")
List<Member> findMemberAndTeam();

엔티티그래프 사용법
@EntityGraph(attributePaths = {"team"})
@Query("select m from Member m")
List<Member> findMemberAndTeam();


*사용자 정의 리포지토리 구현
만약에 findByName()이라는 인터페이스 하나만 구현을 하고싶은데 JpaRepository를 상속받은 MemberRepository에 findByName을 적고 구현하려 하면 MemberRepository 안에 있는 수많은 인터페이스를 전부 다 구현해야 한다. 나는 하나만 구현하고 싶은데... 이 때 필요한 것이 바로 사용자 정의 리포지토리이다. 

사용법
1. MemberRepositoryCustom이라는 새로운 인터페이스를 만든다.
2. MemberRepository에서 MemberRepositoryCustom을 상속받는다. 
3. MemberRepositoryCustom에 내가 구현하고싶은 것만 적는다. 
4. MemberRepositoryImpl 이라는 구현체를 만들어서 구현한다.

이 때 주의!
4번에서 구현체를 만들 때 몇가지 규칙이 있는데 첫번 째로 JpaRepository를 상속받은 인터페이스명을 쓴다. 둘 째로 Impl 네글자를 붙여준다. 위의 예제대로면 MemberRepositoryImpl이 될것이다. 이렇게 규칙을 지켜야 스프링 데이터 JPA에서 알아서 연관관계로 묶어서 처리한다. 

사용처
보통 JDBC template을 사용하거나 QueryDSL을 사용할 때 사용자 정의 리포지토리를 사용한다. 하지만 항상 사용자 정의 리포지토리가 필요한 것은 아니다. 임의의 리포지토리를 만들어도 된다. 위의 예제에서 예를 들어 MemberRepositoryCustom같은 인터페이스가 아니라 클래스로 만들고 스프링 빈으로 등록해서 직접 사용해도 된다. 물론 이 경우 스프링 데이터 JPA와는 아무런 관계 없이 별도로 동작한다. 


*Auditing
Auditing은 등록일 수정일 등록자 수정자를 관리하는 것으로 실무에서 꼭 필요한 기능이다. 

사용법
1. 스프링 부트 설정 클래스에 @EnableJpaAuditing 적용
2. 엔티티에 @EntityListeners(AuditingEntityListener.class) 적용
3. 사용할 어노테이션 : @CreatedDate, @LastModifiedDate, @CreatedBy, @LastModifiedBy

@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
@Getter
public class BaseTimeEntity {

    @CreatedDate
    @Column(updatable = false)
    private LocalDateTime createdDate;

    @LastModifiedDate
    private LocalDateTime lastModifiedDate;

    @CreatedBy
    @Column(updatable = false)
    private String createdBy;

    @LastModifiedBy
    private String lastModifiedBy;

}
이렇게 적용하고 사용할 엔티티에서 상속만 하면 된다. extends BaseTimeEntity

LocalDateTime은 자동으로 .now해서 찍어주면 되는 것 같은데 createdBy랑 lastModifiedBy는 어떻게 찍어주지? 

스프링 부트 설정 클래스에서 
@Bean
public AuditorAware<String> auditorProvider() {
return () -> Optional.of(UUID.randomUUID().toString());
}
원래대로라면 session에 있는거 꺼내서 Optional로 반환하면 된다. 


*Web확장 - 페이징과 정렬
스프링 데이터가 제공하는 페이징과 정렬 기능을 스프링 MVC에서 편리하게 사용할 수 있다. 
@GetMapping("/members")
public Page<Member> list(Pageable pageable) {
Page<Member> page = memberRepository.findAll(pageable);
return page;
}

localhost:8080/members?page=0&sort=name.desc 이런식으로 url 파라미터값으로 페이징을 할 수 있다. 주의할 점은 page가 0부터 시작이라는 점이다.

페이지 하나당 데이터는 20개가 디폴트값이며 이 디폴트 값을 변경하고 싶을 때는 글로벌하게 바꾸는 방법과 국소적으로 바꾸는 방법이 있는데 국소적으로 바꾸는 방법이 더 실용성 있다.

@GetMapping("/members")
public Page<Member> list(@PageableDefault(size = 5) Pageable pageable) {
Page<Member> page = memberRepository.findAll(pageable);
return page;
}
하지만 이러한 방법은 복잡한 sorting같은 것은 구현하기 힘들다. 

'JPA > JPA' 카테고리의 다른 글

@CreatedDate, @LastModifiedDate  (0) 2022.08.23
AuditorAware  (0) 2021.09.23
JPA를 활용한 (XToOne) Restful API설계  (0) 2021.09.13
JPQL 중급  (0) 2021.08.26
JPQL 초급  (0) 2021.08.26