스프링 데이터 JPA

개발하는 도비·2023년 5월 6일

JPA

목록 보기
10/13
post-thumbnail

공통 인터페이스 기능

공통 인터페이스 적용

  • JpaRepository 상속 받은 interface 작성
  • 코드
 public interface MemberRepository extends JpaRepository<Member, Long> {
  }

공통인터페이스 분석

  • 인터페이스 구조
  • 제네릭 타입
    • T : 엔티티
    • ID : 엔티티의 식별자 타입
    • S : 엔티티와 그 자식 타입
  • 주요 메서드
    • save(S) : 새로운 엔티티는 저장하고 이미 있는 엔티티는 병합한다.
    • delete(T) : 엔티티 하나를 삭제한다. 내부에서 EntityManager.remove() 호출
    • findById(ID) : 엔티티 하나를 조회한다. 내부에서 EntityManager.find() 호출
    • getOne(ID) : 엔티티를 프록시로 조회한다. 내부에서 EntityManager.getReference() 호출
    • findAll(...) : 모든 엔티티를 조회한다. 정렬( Sort )이나 페이징( Pageable ) 조건을 파라미터로 제공할 수 있다.
  • 추가 내용 : 쿼리 메소드를 보면 인터페이스임에도 쉽게 추가가 가능 -> 스프링 데이터 JPA에서 이에대한 처리를 잘해줌. => 편히하게 사용 가능.

쿼리 메소드 기능

메소드 이름으로 쿼리 생성

순수 JPA

  • em.createQuery() 사용

스프링 데이터 JPA

JPA NamedQuery

  • @NamedQuery 어노테이션으로 Named 쿼리 정의
@Entity
@NamedQuery( name="Member.findByUsername",
          query="select m from Member m where m.username = :username")

JPA를 직접 사용해서 Named 쿼리 호출

public class MemberRepository {
      public List<Member> findByUsername(String username) {
          ...
          List<Member> resultList =
              em.createNamedQuery("Member.findByUsername", Member.class)
                  .setParameter("username", username)
                  .getResultList();
	} 
}          

스프링 데이터 JPA로 NamedQuery 사용

  • JpaRepository를 상속받은 inteface에 메소드 작성
  • @Query가 없을 경우 {도메인 클래스}.{메서드 이름}으로 찾음.
  • 예시 코드
    • 파라미터가 있을 경우 @Param필요
@Query(name = "Member.findByUsername")
List<Member> findByUsername(@Param("username") String username);

@Query, 리포지토리 메소드에 쿼리 정의하기

  • JpaRepository를 상속받은 inteface에 메소드 작성
 @Query("select m from Member m where m.username= :username and m.age = :age")
    List<Member> findUser(@Param("username") String username, @Param("age") int
    age);

@Query, 값, DTO 조회하기

  • JpaRepository를 상속받은 inteface에 메소드 작성
  • 단순히 값 하나 조회
@Query("select m.username from Member m")
    List<String> findUsernameList();
  • DTO로 직접 조회
@Query("select new study.datajpa.dto.MemberDto(m.id, m.username, t.name) " +
          "from Member m join m.team t")
  List<MemberDto> findMemberDto();

파라미터 바인딩

  • 위치기반
select m from Member m where m.username = ?0 //위치 기반 
  • 이름 기반
select m from Member m where m.username = :name //이름 기반
  • 실직적으로 이름기반을 사용해야함. -> 위치 기반의 경우 순서가 바뀌거나 할 경우 유지보수가 힘듬.

컬렉션 파라미터 바인딩

  • Collection 타입으로 in절 지원
@Query("select m from Member m where m.username in :names")
    List<Member> findByNames(@Param("names") List<String> names);

반환 타입

  • 유연한 반환 타입 지원
  • 컬렉션
List<Member> findByUsername(String name); //컬렉션 
  • 단건
Member findByUsername(String name); //단건
  • 단건 optional -> DB에 있을지 없을지 모를 경우 optional 이후에 처리 로직 추가
Optional<Member> findByUsername(String name); //단건 Optional
  • 조회 결과가 없을 경우
    • 컬렉션 : 빈 컬렉션
    • 단건 : null
  • 결과가 많을 경우
    • javax.persistence.NonUniqueResultException 발생.

페이징과 정렬

순수 JPA 페이징과 정렬

  • 예제
    • 검색 조건: 나이가 10살
    • 정렬 조건: 이름으로 내림차순
    • 페이징 조건: 첫 번째 페이지, 페이지당 보여줄 데이터는 3건
 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();

스프링 데이터 JPA 페이징과 정렬

  • JPA가 아닌 인터페이스 두개로 공통화함. -> 세부구현은 알아서 할 것
    • org.springframework.data.domain.Sort : 정렬 기능
    • org.springframework.data.domain.Pageable : 페이징 기능 (내부에 Sort 포함)
  • 특별한 반환 타입
    • org.springframework.data.domain.Page : 추가 count 쿼리 결과를 포함하는 페이징
    • org.springframework.data.domain.Slice : 추가 count 쿼리 없이 다음 페이지만 확인 가능
  • 예제
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);
  • Page 인터페이스
 public interface Page<T> extends Slice<T> {
int getTotalPages(); //전체 페이지 수
long getTotalElements(); //전체 데이터 수
<U> Page<U> map(Function<? super T, ? extends U> converter); //변환기
}
  • Slice 인터페이스
public interface Slice<T> extends Streamable<T> {
	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();//이전 페이지 객체
	<U> Slice<U> map(Function<? super T, ? extends U> converter); // 변환기 
}
  • 성능을 위한 count 쿼리 분리
    • 값을 위한 쿼리와 count 쿼리가 다를 경우 분리를 하며 성능 개선을 할 수 있음.
@Query(value = “select m from Member m”, countQuery = “select count(m.username) from Member m”)
Page<Member> findMemberAllCountBy(Pageable pageable);
  • 페이지를 유지하면서 엔티티를 DTO로 변환하기
Page<Member> page = memberRepository.findByAge(10, pageRequest);
Page<MemberDto> dtoPage = page.map(m -> new MemberDto());

벌크성 수정 쿼리

  • 한번에 값 수정 -> 전체 직원의 월급이 10% 인상된 경우

순수 JPA

public int bulkAgePlus(int age) {
        int resultCount = em.createQuery(
                "update Member m set m.age = m.age + 1" +
                        "where m.age >= :age")
                .setParameter("age", age)
                .executeUpdate();
        return resultCount;
}

스프링 데이터 JPA

  • @Modifying 어노테이션을 사용
    • org.hibernate.hql.internal.QueryExecutionRequestException: Not supported for DML operations 에러 발생
  • 벌크성 -> 바로 DB가 업데이트 됨.
    • 영속성 컨텍스트 - DB가 값이 다를 수 있음.
    • em.flush() + em.clear()로 영속성 컨텍스트에 있는 값을 DB에 적용시켜야함.
    • @Modifying(clearAutomatically = true) 다음과 같이 해도 em.flush() + em.clear()와 같은 효과를 가짐.
@Modifying
@Query("update Member m set m.age = m.age + 1 where m.age >= :age")
int bulkAgePlus(@Param("age") int age);

@EntityGraph

  • 연관된 엔티티들을 SQL 한번에 조회하는 방법
    • JPQL 페치 조인
    • @EntityGraph -> 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
    • 사실상 페치 조인(FETCH JOIN)의 간편 버전
    • LEFT OUTER JOIN 사용
  • NamedEntityGraph 사용 방법
    • 잘 사용하시지는 않는다고 하심. -> 예제로 이런게 있다 정도만 알 것.
    @NamedEntityGraph(name = "Member.all", attributeNodes =@NamedAttributeNode("team"))
    @EntityGraph("Member.all")
    @Query("select m from Member m")
    List<Member> findMemberEntityGraph();

JPA Hint & Lock

JPA Hint

  • JPA 쿼리 힌트(SQL 힌트가 아니라 JPA 구현체에게 제공하는 힌트)
@QueryHints(value = @QueryHint(name = "org.hibernate.readOnly", value ="true")) // 
Member findReadOnlyByUsername(String username);

JPA Lock

  • 나중에 추가할 것. -> 현재 시점에서 쉽게 사용 가능하다 정도 인지.
@Lock(LockModeType.PESSIMISTIC_WRITE)
List<Member> findByUsername(String name);

확장 기능

사용자 정의 리포지토리 구현

  • 사용이 필요한 경우

    • JPA 직접 사용( EntityManager )
    • 스프링 JDBC Template 사용 MyBatis 사용
    • 데이터베이스 커넥션 직접 사용 등등...
    • Querydsl 사용
  • 사용 순서

      1. 새로운 인터페이스 생성
      1. 새로운 인터페이스 + Impl 에 구현체 생성
      1. 기존 리포지토리에 인터페이스 상속
    • Impl은 바꿀 수 있으나 관례임으로 유지보수를 위해 바꾸지 말것.

Auditing

  • 추적 -> 위의 두 항목은 필수
    • 등록일
    • 수정일
    • 등록자
    • 수정자

순수 JPA

  • JPA 주요 이벤트 어노테이션
    • @PrePersist
    • @PostPersist
    • @PreUpdate
    • @PostUpdate
  • 코드
@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();
      }
}
 

스프링 데이터 JPA

  • 설정
    • @EnableJpaAuditing
    • 스프링 부트 설정 클래스에 작성
  • 사용 어노테이션
    • @CreatedDate
    • @LastModifiedDate
    • @CreatedBy
    • @LastModifiedBy
  • 코드(등록일, 수정일)
@EntityListeners(AuditingEntityListener.class)
  @MappedSuperclass
  @Getter
  public class BaseEntity {
      @CreatedDate
      @Column(updatable = false)
      private LocalDateTime createdDate;
      
      @LastModifiedDate
      private LocalDateTime lastModifiedDate;
}
  • 코드(등록자, 수정자)
@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;
  • 스프링 빈 등록
@Bean
public AuditorAware<String> auditorProvider() {
	return () -> Optional.of(UUID.randomUUID().toString());
 }

Web 확장 - 도메인 클래스 컨버터

  • HTTP 파라미터로 넘어온 엔티티의 아이디로 엔티티 객체를 찾아서 바인딩
  • HTTP 요청은 회원 id를 받지만 도메인 클래스 컨버터가 중간에 동작해서 회원 엔티티 객체를 반환
  • 주의
    • 단순 조회용으로만 사용
    • 트랜잭션이 없는 범위에서 엔티티를 조회했으므로, 엔티티를 변경해도 DB에 반영되지 않음
  • 기본
@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();
}

Web 확장 - 페이징과 정렬

  • 스프링 데이터가 제공하는 페이징과 정렬 기능을 스프링 MVC 사용 가능
    • 개념적인 부분에서 추가는 아님. 사용 가능하다는 예시
    • 파라미터로 Pageable을 받을 수 있음
  • 코드
@GetMapping("/members")
public Page<Member> list(Pageable pageable) {
	Page<Member> page = memberRepository.findAll(pageable);
	return page;
}
  • 요청 파라미터

    • page: 현재 페이지, 0부터 시작
    • size: 한 페이지에 노출할 데이터 수
    • sort: 정렬 조건을 정의
  • 설정 기본 값

    spring.data.web.pageable.default-page-size=20 /# 기본 페이지 사이즈
    spring.data.web.pageable.max-page-size=2000 /# 최대 페이지 사이즈/
    • 글로벌

      • application.yml에 아래 내용 추가
      data:
        web:
          pageable:
            default-page-size: n
            max-page-size: m
      
    • 개별 변경

      • @PageableDefault 사용
      @RequestMapping(value = "/members_page", method = RequestMethod.GET)
      public String list(@PageableDefault(size = 12, sort = “username”, direction = Sort.Direction.DESC) Pageable pageable) {
      ...
      }
      
  • DTO 사용

    • page.map(classDto::new)
      • DTO에 생성자를 추가해둬야함.
  • page 1 부터 시작하기

    • 기본적으로 page는 0부터 시작.
      1. Pageable, Page를 파리미터와 응답 값으로 사용히지 않고, 직접 클래스를 만들어서 처리
      • PageRequest(Pageable 구현체)를 생성해서 리포지토리에 넘김
      • 답값도 Page 대신에 직접 만들어서 제공
      1. spring.data.web.pageable.one-indexed-parameters 를 true 로 설정
      • 한계는 존재

스프링 데이터 JPA 분석

스프링 데이터 JPA 구현체 분석

  • @Transactional 트랜잭션 적용

    • JPA의 모든 변경은 트랜잭션 안에서 동작
    • 스프링 데이터 JPA는 변경(등록, 수정, 삭제) 메서드를 트랜잭션 처리
    • 서비스 계층에서 트랜잭션을 시작하지 않으면 리파지토리에서 트랜잭션 시작 -> 하지만 끝나면 영속성 컨텍스트가 종료됨.
    • 서비스 계층에서 트랜잭션을 시작하면 리파지토리는 해당 트랜잭션을 전파 받아서 사용
  • @Transactional(readOnly = true

    • 데이터를 단순히 조회만 하고 변경하지 않는 트랜잭션에서 readOnly = true 사용하면 flush()를 생략해서 약간의 성능 향상을 얻을 수 있음
  • save()

    • 새로운 엔티티면 저장(persist)
    • 새로운 엔티티가 아니면 병합(merge)
      • DB에서 가져온 것을 교체.
      • 데이터 업데이트가 아닌 영속성 관리에서 사용해야함.

새로운 엔티티를 구별하는 방법

  • 새로운 엔티티를 판단하는 기본 전략
    • 식별자가 객체일 때 null 로 판단
    • 식별자가 자바 기본 타입일 때 0 으로 판단
    • Persistable 인터페이스를 구현해서 판단 로직 변경 가능
  • @GenerateValue -> save() 시점 식별자가 없으므로 새로운 entity로 판단.
  • @Id + 직접 할당 -> save() 시점에 직접 할당으로 인해 이미 존재하는 entity로 판. -> merge()
    • Persistable 를 사용해서 새로운 entity 확인 여부를 직접 구현

나머지 기능

  • 실무 자주 사용x 쓸 일이 있을 수 있거나 대안이 많음

Specifications (명세)

  • JPA Criteria를 활용해서 이 개념을 사용할 수 있도록 지원
  • 실무에서 JPA Criteria는 금지 수준으로 안씀.
  • 술어(predicate)
    • 참 또는 거짓으로 평가
    • AND OR 같은 연산자로 조합해서 다양한 검색조건을 쉽게 생성(컴포지트 패턴)
    • 스프링 데이터 JPA는 org.springframework.data.jpa.domain.Specification 클래스로 정의
  • 명세 기능 사용 방법
    • JpaSpecificationExecutor 인터페이스 상속
  • 자세한 내용은 필요할 때 찾아볼 것

Query By Example

  • 장점

    • 동적 쿼리를 편리하게 처리
    • 도메인 객체를 그대로 사용
    • 데이터 저장소를 RDB에서 NOSQL로 변경해도 코드 변경이 없게 추상화 되어 있음
    • 스프링 데이터 JPA JpaRepository 인터페이스에 이미 포함
  • 단점

  • 조인은 가능하지만 내부 조인(INNER JOIN)만 가능함 외부 조인(LEFT JOIN) 안됨

  • 다음과 같은 중첩 제약조건 안됨

    • firstname = ?0 or (firstname = ?1 and lastname = ?2)
  • 매칭 조건이 매우 단순함

    • 문자는 starts/contains/ends/regex
    • 다른속성은정확한매칭( = )만지원
  • 자세한 내용은 필요할 때 찾아볼 것

Projections

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

  • 데이터 조회에서의 Projections : select에 들어가는 필드

  • 인터페이스 기반 closed Proejctions

    • 프로퍼티 형식(getter)의 인터페이스를 제공하면, 구현체는 스프링 데이터 JPA가 제공
    • 순서
        1. 인터페이스 생성
      public interface UsernameOnly {
          String getUsername();
      }
        1. 반환 타입을 생성한 인터페이스로 설정.
      List<UsernameOnly> result = memberRepository.findProjectionsByUsername("m1");
  • 인터페이스 기반 Open Proejctions

    • entity를 가져온 뒤 처리
    public interface UsernameOnly {
        @Value("#{target.username + ' ' + target.age + ' ' + target.team.name}")
        String getUsername()
     }
  • 클래스 기반 Projection

    • 생성자의 파라미터 이름으로 매칭
    public class UsernameOnlyDto {
        private final String username;
        public UsernameOnlyDto(String username) {
            this.username = username;
    }
    public String getUsername() {
            return username;
        } 
    }
  • 동적 Projections

    • Generic type을 주면, 동적으로 프로젝션 데이터 번경 가능
     <T> List<T> findProjectionsByUsername(String username, Class<T> type);
  • 중첩 구조 처리

    • 첫번째는 최적화, 두번째 부터는 모두 가져옴
    • 코드
    public interface NestedClosedProjection {
          String getUsername();
          TeamInfo getTeam();
          interface TeamInfo {
              String getName();
    		  } 
    }
    • 쿼리 결과
    select
        m.username as col_0_0_,
        t.teamid as col_1_0_,
        t.teamid as teamid1_2_,
        t.name as name2_2_
    from
    		  member m
    left outer join
        team t
           on m.teamid=t.teamid
    where
        m.username=?   
  • 주의

    • 프로젝션 대상이 root entity -> JPQL SELECT 절 최적화 가능
    • 프로젝션 대상이 ROOT가 x
      • LEFT OUTER JOIN 처리
      • 모든 필드를 SELECT해서 엔티티로 조회한 다음에 계산

네이티브 쿼리

  • 가급적 네이티브 쿼리는 사용하지 않는게 좋음
  • 스프링 데이터 JPA 기반 네이티브 쿼리
    • 페이징 지원
    • 반환 타입
      • Object[]
      • Tuple
      • DTO(스프링 데이터 인터페이스 Projections 지원)
    • 제약
      • Sort 파라미터를 통한 정렬이 정상 동작하지 않을 수 있음
      • JPQL처럼 애플리케이션 로딩 시점에 문법 확인 불가
      • 동적 쿼리 불가

참조

  • 인프런 실전! 스프링 데이터 JPA
  • 링크
profile
도비의 양말을 찾아서

0개의 댓글