티스토리 뷰

JPA

[JPA] Auditing, Projections, Pageable 이해하기

개발도 운동만큼 2023. 3. 30. 20:51
반응형

Auditing

  • Entity를 생성 변경할 때 변경한 사람과 시간을 추적하고 싶으면?

    • 등록일
    • 수정일
    • 등록자
    • 수정자

    스프링 데이터 JPA 사용

    설정

    @EnableJpaAuditing -> 스프링 부트 설정 클래스에 적용해야함

    @EntityListeners(AuditingEntityListener.class) -> Entity에 적용

    사용 어노테이션

    • @CreatedDate
    • @LastModifiedDate
    • @CreateBy
    • @LastModifiedBy

@EntityListeners(AuditingEntityListener.class)
@MappedSuperClass
@Getter
public class BaseEntity{

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

    @LastModifiedDate
    private LocalDateTime lastModifiedDate;


}

등록자, 수정자를 처리해주는 AuditorAware 스프링 빈 등록


@Bean
public AuditorAware<String> auditorProvider(){
  return () -> Optional.of(UUID.randomUUID().toString());
}

실무에서는 세션 정보나 스프링 시큐리티 로그인 정보에서 ID를 받음

참고 : 실무에서 대부분의 Entity는 등록시간, 수정시간이 필요하지만, 등록자, 수정자는 없을 수도 있다. 그래서 다음과 같이 Base 타입을 분리하고 원하는 타입을 선택해서 상속한다.

public class BaseTimeEntity{

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


}

public class BaseEntity extends BaseTimeEntity{
  @CreatedBy
  @Column(updatable = false)
  private String createdBy;
  @LastModifiedBy
  private String lastModifiedBy;
}

참고 : 저장시점에 등록일, 등록자는 물론이고 수정일, 수정자도 같은 데이터가 저장된다. 데이터가 중복 저장되는 것 같지만, 이렇게 해두면 변경 컬럼만 확인해도 마지막에 업데이트한 유저를 확인 할 수 있으므로 유지보수 관점에서 편리하다. 이렇게 하지 않으면 변경 컬럼이 null 일때 등록 컬럼을 또 찾아야 한다. 참고로 저장시점에 저장데이터만 입력하고 싶으면 @EnableJpaAuditing(modifyOnCreate = false) 옵션을 사용하면 된다.

Pageable

  • 설명하기에 앞서 실무에서 가장 중요한 내용 먼저 언급하고 써내려 갈려고 한다.

    실무에서 가장 중요한 것은 페이지를 유지하면서 Entity를 DTO로 변환하여 Controller에 뿌리는 것이다 -> 이는 Entity가 절대로 노출되서는 안되기 때문이다.

    Page<Member> page = memberRepository.findByAge(10, pageRequest);
    Page<MemberDto> dtoPage = page.map(m -> new MemberDto());

    카운트 쿼리를 분리해야 한다(실무에서는 left join, right join 등을 통해서 데이터를 가져오는데 총페이지 쿼리는 join을 할 필요가없다. 어차피 데이터 개수는 동일하다)

    • 카운트 쿼리에서 성능 저하가 발생하기 때문이다.
    @Query(value = "select m from Member m", countQuery = "select count(m) from Member m")
    Page<Member> findByMember(Pageable pageble);
  • 스프링 데이터 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 쿼리 없이 결과만 반환

    예제

    두번째 파라미터로 받은 Pageable은 인터페이스 이다. 즉 실제로 사용할 때는 해당 인터페이스를 구현한 org.springframework.data.domain.PageRequest 객체를 사용한다.

    PageRequest 생성자의 첫번째 파라미터에는 현재페이지를, 두번째 파라미터에는 조회할 데이터 수를 입력한다. 추가로 정렬 정보도 파라미터로 사용할 수 있다.

    참고로 페이지는 0 부터 시작한다.

    @Test
    public void page() throws Exception {
       //given
       memberRepository.save(new Member("member1", 10));
       memberRepository.save(new Member("member2", 10));
       memberRepository.save(new Member("member3", 10));
       memberRepository.save(new Member("member4", 10));
       memberRepository.save(new Member("member5", 10));
    
       //when
       PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC,"username"));
       Page<Member> page = memberRepository.findByAge(10, pageRequest);
    
       //then
       List<Member> content = page.getContent(); //조회된 데이터
       assertThat(content.size()).isEqualTo(3); //조회된 데이터 수
       assertThat(page.getTotalElements()).isEqualTo(5); //전체 데이터 수
       assertThat(page.getNumber()).isEqualTo(0); //페이지 번호
       assertThat(page.getTotalPages()).isEqualTo(2); //전체 페이지 번호
       assertThat(page.isFirst()).isTrue(); //첫번째 항목인가?
       assertThat(page.hasNext()).isTrue(); //다음 페이지가 있는가?
    }
  • 추가로 모바일 처럼 스크롤을 아래로 내리거나 더보기 같은 버튼을 클릭해서 보여주는 페이징은 Slice를 사용한다.
    별도의 개발필요없이 반환타입만 Slice로 변경해주면 된다.

    DB에 쿼리를 날릴때 limit + 1 숫자만큼 쿼리를 날린다.

    Slice<Member> findByAge(int age, Pageable pageable);
    
    //Slice 로 반환된 객체로 알 수 있는 메소드
     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();//이전 페이지 객체

Projections

Entity 대신에 DTO를 편리하게 조회할 때 사용

전체 Entity가 아니라 만약 회원 이름만 딱 조회하고 싶으면?

  1. 먼저 인터페이스를 구현한다.
public interface UsernameOnly{
      String getUsername();
    }
  • 조회할 Entity의 필드를 getter 형식으로 지정하면 해당 필드만 선택해서 조회(Projection)
  1. Repository를 구현
public interface MemberRepository ... {
  List<UsernameOnly> findProjectionsByUsername(String username);
}
  • 메서드 이름은 자유, 반환 타입으로 인지 한다.
  1. Test

@Test
public void projections() throws Exception {
 //given
 Team teamA = new Team("teamA");
 em.persist(teamA);

 Member m1 = new Member("m1", 0, teamA);
 Member m2 = new Member("m2", 0, teamA);

 em.persist(m1);
 em.persist(m2);

 em.flush();
 em.clear();

 //when
 List<UsernameOnly> result = memberRepository.findProjectionsByUsername("m1");

 //then

 Assertions.assertThat(result.size()).isEqualTo(1);

 }
  1. query 확인
    select m.username from member m where m.username = 'm1';
  • username 만 조회하는 것을 확인 할 수 있다.

인터페이스 기반 Closed Projections

프로퍼티 형식(getter)의 인터페이스를 제공하면, 구현체는 스프링 데이터 JPA가 제공

public interface UsernameOnly{
 String getUsername();
}

클래스 기반 Projection

다음과 같이 인터페이스가 아닌 구체적인 DTO 형식도 가능

생성자의 파라미터 이름으로 매칭

public class UsernameOnlyDto {

    private final String username;

 public UsernameOnlyDto(String username) {
 this.username = username;
 }

 public String getUsername() {
 return username;
 }
}

Native Query + Projection 활용


@Query(value = "select m.member_id as id, m.username, t.name as teamName " +
               "from member m left join team t",
               countQuery = "select count(*) from member",
               nativeQuery = true)

Page<MemberProjection> findByNativeProjection(Pageable pageable);
반응형
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/02   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28
글 보관함