본문 바로가기
스프링/JPA

Spring-Data-Jpa

by 공부 안하고 싶은 사람 2021. 8. 6.
반응형

참고: 최근 IntelliJ 버전은 Gradle로 실행을 하는 것이 기본 설정이다.

이렇게 하면 실행속도가 느리다. 다음과 같이 변경하면 자바로 바로 실행하므로 좀 더 빨라진다.

  • Preferences
  • Build, Execution, Deployment
  • Build Tools
  • Gradle
  • Build and run using: Gradle -> IntelliJ IDEA
  • Run tests using: Gradle -> IntelliJ IDEA

 

 

롬복 적용

  1. Preferences - plugin - lombok 검색 실행 (재시작)
  2. Preferences - Annotation Processors 검색 - Enable annotation processing 체크 (재시작)
  3. 임의의 테스트 클래스를 만들고 @Getter, @Setter 확인

 

 

공통 인페이스 설정

  • 스프링 부트의 경우 어플리케이션 위에 아래의 annotation을 추가하지 않아도 된다.(componentscan을 하기 때문)
    • @EnableJpaRepositories(basePackages = "jpabook.jpashop.repository")
  • public interface TestReopository extends JapRepositroty<Test, Long>
    • 인터페이스이고 구현체인데 동작하는 이유 :
    • spring-data-jpa가 해당 인터페이스들을 확인하고 프록시로 구현체를 빈에 등록하기 때문
  • @Repository가 없어도 빈으로 등록가능

 

 

공통 인터페이스 분석

 

쿼리 메소드 기능

  • 메소드 이름으로 쿼리 생성
  • NamedQuery
  • @Query - 리파지토리 메소드에 쿼리 정의
  • 파라미터 바인딩
  • 반환 타입
  • 페이징과 정렬
  • 벌크성 수정 쿼리
  • @EntityGraph

메소드 이름으로 쿼리 생성

필드명이 변경 됐을때, 어플리케이션 실행시점에서 오류를 찾을 수 있다.

 

 

 

 

NamedQuery(비추)

클래스에 추가

@NamedQuery(
    name = "Member.findByUsername",
    query="select m from Member m where m.username = :username"
)
public class Member {

jparepository

public List<Member> findByUsername(String username){
  return em.createQuery("Member.findByUsername", Member.class)
    .setParameter("username", username)
    .getResultList();
}

repository

@Query(name = "Member.findByUsername")
List<Member> findByUsernmae(@Param("username")String usernmae);
  • 네임드 쿼리는 app로딩 시점에 쿼리 에러를 발견할 수 있는 장점은 있다.
  • 스프링 데이터 JPA는 선언한 "도메인 클래스 + .(점) + 메서드 이름"으로 Named 쿼리를 찾아서 실행
  • 만약 실행할 Named 쿼리가 없으면 메서드 이름으로 쿼리 생성 전략을 사용한다.
  • 필요하면 전략을 변경할 수 있지만 권장하지 않는다.

 

 

 

 

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

@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);
  • 이름 없는 NamedQuery라 할 수 있다.
  • NamedQuery처럼 app실행 시점에 에러를 발견할 수 있다.

 

 

 

 @Query 값, DTO 조회

@Query를 통해 이전과 같이 entitiy를 조회할 수 있지만,

Dto클래스를 아래와 같이 생성자로 만들어 반환할 수 있다. (패키지 경로까지 설정해줘야 함)

@Query("select new study.datajpa.dto.MemberDto(m.id, m.username, t.name) from Member m join m.team t")
List<MemberDto> findMemberDto();

 

 

 

파라미터 바인딩

이름기반 과 위치기반이 있지만, 이름기반으로 사용하는것이 가독성이 좋다.

이름기반 - :paramname -> 파라미터 이름중 paramname에 대응된다.

// 컬렉션 기반
@Query("select m from Member m where m.username in :names")
List<Member> findByNames(@Param("names") List<String> names);

 

 

 

반환타입

  • 컬렉션 -> 없으면 빈 컬렉션
  • 클래스 -> 없으면 null, 복수 -> NonUniqueResultException
  • Optional

사실 단건 클래스 조회가 없으면 NoResultException이 발생하지만, 예외처리로 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();
    }

 

Spring-Data-Jpa

interface Page extends Slice

Page : 추가 count 쿼리 결과를 포함(전체 페이지수, 전체 데이터 수)

Slice : 추가 count 쿼리 결과를 미포함, 다음 페이지만 확인(내부적으로 limit+1 조회)

Page<Member> findByAge(int age, Pageable pageable);
// repository 설정
memberRepository.save(); // 5회 저장 가정

int age = 10;
// 페이지 0, 사이즈 3, sort설정 // 페이징에 필요한 정보
PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Direction.DESC, "usernmae"));
Page<Member> page = memberRepository.findByAge(age, pageRequest);
List<Member> content = page.getContent();
//then
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(); //다음 페이지가 있는가?

Page는 1부터 시작이 아니라 0부터 시작이다.

//count쿼리는 조인시 성능에 지장이 될 수 있기에, 쿼리를 분리하여 사용할 수 있도록 설계
@Query(value = “select m from Member m”,
       countQuery = “select count(m.username) from Member m”)
Page<Member> findMemberAllCountBy(Pageable pageable);

Top, First

List<Member> findTop3By(); // 가장 위 3개만 

페이징 결과 -> 외부

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

 

 

 

 

벌크성 수정쿼리

순수 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();
}

Data-Jpa

@Modifying
@Query("update Member m set m.age = m.age + 1 where m.age >= :age")
int bulkAgePlus(@Param("age") int age);
  • @Modifying이 없다면 Not supported for DML operations 발생
  • 벌크 연산은 영속성 컨텍스트를 무시하고 실행하기 때문에, DB와 1차 캐시의 값이 다르다.
    • 이후에 em을 flush 혹은 clear하거나
    • @Modifying(clearAutomatically = true) 옵션을 추가해준다.

 

 

 

 

@EntityGraph

연관관계 매핑에서(보통 Lazy로딩을 사용하므로) 연관된 객체의 데이터(프록시)를 검색할 경우 마다 쿼리가 1개씩 발생한다. -> 연관된 데이터를 1번의 쿼리로 모두 가져올 수 있도록 패치조인/EntityGraph 사용

패치조인

@Query("select m from Member m left join fetch m.team")
List<Member> findMemberFetchJoin();

EntityGraph

//공통 메서드 오버라이드
@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);
  • 패치조인의 간편버전
  • LEFT OUTER JOIN을 사용

 

 

 

 

NamedEntityGraph(비추)

NamedQuery처럼 Entity위에 지정된 EntityGraph를 사용할 수 있다. (생략)

JPA HINT

(SQL힌트가 아니라 JPA구현체에게 제공하는 힌트)

@QueryHints(
  value = @QueryHint(name = "org.hibernate.readOnly", value =
"true")
)
Member findReadOnlyByUsername(String username);
// 읽기전용으로 읽어온 데이터에 대해서 변화(update/delete)가 없을 경우, 메모리 최적화 가능(1차캐시 X)

Lock

@Lock(LockModeType.PESSIMISTIC_WRITE)
List<Member> findByUsername(String name);

다른 리소스가 접근하지 못하도록 Lock설정 가능

 

 

 

 

사용자 정의 레포지토리

사용자 정의 인터페이스 생성

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 {

규칙

  • 규칙: 리포지토리 인터페이스 이름 + Impl
  • 스프링 데이터 JPA가 인식해서 스프링 빈으로 등록
  • 바꿀 순 있지만, 그냥 쓰도록 하자

사용

  • JdbcTemplate이나 QueryDsl 사용시 주로 사용
  • 사용자 정의 인터페이스가 아닌 별도의 클래스를 빈으로 등록하여, 용도(구조적) 분리용으로 사용하여도 된다.
  • 스프링 데이터 2.x 부터는 사용자 정의 구현 클래스에 리포지토리 인터페이스 이름 + Impl 을 적용하는 대신에 사용자 정의 인터페이스 명 + Impl 방식도 지원한다.
    • 예를 들어서 위 예제의 MemberRepositoryImpl 대신에 MemberRepositoryCustomImpl 같이 구현해도 된다.

 

 

 

 

Auditing

등록일 수정일 등록자 수정자 를 추적하기 위해

순수 Jpa

아래의 슈퍼클래스를 상속받는다.

@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();
    }
}

Data-jpa

Envers 적용과 유사

설정

@EnableJpaAuditing // 옵션적용
@SpringBootApplication
public class DataJpaApplication {
    public static void main(String[] args) {
        SpringApplication.run(DataJpaApplication.class, args);
    }

    @Bean // 등록, 수정자를 설정하기 위한 설정, 시큐리티 사용중이면 시큐리티 로그인 정보에서 Id 추출
    public AuditorAware<String> auditorProvider() {
         return () -> Optional.of(UUID.randomUUID().toString());
    }
}

@CreatedDate, @lstModifiedDate, @CreatedBy, @LastModifiedBy

어노테이션으로 쉽게 설정가능

@EntityListeners(AuditingEntityListener.class) // 엔티티에도 옵션적용
@MappedSuperclass
@Getter
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;
}

시간과 등록을 분리 사용하기 위해 두 클래스를 상속으로 분리하여, 원하는 추적 정보만 사용하는 것도 좋은 방법이다.

BaseEntitiy extends BaseTimeEntitiy & BaseTimeEntity

 

 

 

 

웹 확장

도메인 클래스 컨버터(비추) - 간단한 조회용으로만 사용

@GetMapping("/members/{id}")
public String findMember(@PathVariable("id") Long id) {
  Member member = memberRepository.findById(id).get();
  return member.getUsername();
}

@GetMapping("/members2/{id}")
public String findMember(@PathVariable("id") Member member) {
  return member.getUsername();
}

위 아래 동일하게 동작

HTTP 요청은 회원 id 를 받지만 도메인 클래스 컨버터가 중간에 동작해서 회원 엔티티 객체를 반환 도메인 클래스 컨버터도 리파지토리를 사용해서 엔티티를 찾음

해당 엔티티는 단순 조회용으로 1차 캐시에 들어가지 않는다.

 

 

 

페이징과 정렬

@GetMapping("/members")
public Page<Member> list(Pageable pageable) {
  Page<Member> page = memberRepository.findAll(pageable);
  return page;
}

파라미터로 Pageable 을 받을 수 있다. Pageable 은 인터페이스, 실제는 org.springframework.data.domain.PageRequest 객체 생성

예시 ) /members?page=0&size=3&sort=id,desc&sort=username,desc

page: 현재 페이지 (0부터 시작한다)

size: 한 페이지에 노출할 데이터 건수

sort: 정렬 조건을 정의한다.

예) 정렬 속성,정렬 속성...(ASC | DESC), 정렬 방향을 변경하고 싶으면 sort 파라미터 추가 ( asc 생략 가능)

글로벌 설정

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

개별 설정

@PageableDefault

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

접두사

접두사 페이징 정보가 둘 이상이면 접두사로 구분 @Qualifier 에 접두사명 추가

public String list(
 @Qualifier("member") Pageable memberPageable,
 @Qualifier("order") Pageable orderPageable,
 ...

페이지 내용을 DTO로 변환

Page.map()활용

@GetMapping("/members")
public Page<MemberDto> list(Pageable pageable) {
     return memberRepository.findAll(pageable).map(MemberDto::new);
} 

페이지를 0이 아닌 1부터 시작할 수 있는 방법들이 두가지 정도 존해하지만,

(재정의 혹은 전체 설정변경) 한계점과 복잡도가 존재하므로 그냥 쓰도록 하자~

 

 

 

 

 

Spring-Data-Jpa 분석

SimpleJpaRepository : 구현체

  • @Repository 적용: JPA 예외를 스프링이 추상화한 예외로 변환
  • @Transactional 트랜잭션 적용
    • 그래서 스프링 데이터 JPA를 사용할 때 트랜잭션이 없어도 데이터 등록, 변경이 가능했음(사실은 트랜잭션이 리포지토리 계층에 걸려있는 것임)
  • 최상단의 @Transactional(readOnly = true)
    • 단순히 조회만 하고 변경하지 않는 트랜잭션에서 readOnly = true 옵션을 사용하면 플러시를 생략해서 약간의 성능 향상을 얻을 수 있음
    • 아닌경우는 @Transactional을 별도로 함수마다 설정
  • Save()
    • 새로운 엔티티면 persist
    • 있던 엔티티면 merge

새로운 엔티티를 판단하는 기본 전략

  • 식별자가 객체일 때 null 로 판단
  • 식별자가 자바 기본 타입일 때 0 으로 판단
  • Persistable 인터페이스를 구현해서 판단 로직 변경 가능
if (entityInformation.isNew(entity)) {
    em.persist(entity);
    return entity;
} else {
    return em.merge(entity);
}

@GenerateValue를 사용하고 있다면, persist()를 호출하게 되겠지만.

사용하지 못할 경우(id값을 직접 입력)엔 merge를 호출하게 된다. merge는 insert 이전에 select를 실행하는 구조로서 비효율적이다!

이를 해결하기 위해 Persistable을 구현하도록 한다.

@Entity
public class Item implements Persistable<String> {
   @Id
   private String id;

   public Item(String id) {
           this.id = id;
   }

   @Override
   public String getId() {
           return id;
   }
   @Override
   public boolean isNew() {
       // 새로운 엔티티인지 아닌지 구분할 수 있는 로직이 필요하다
     // (createDate 유무로 판단하는것도 좋은방법이다.)
   }
}

 

 

 

 

 

나머지 기능들

명세 Specifications(비추)

Jpa Criteria로 사용 -> 가독성이 떨어지고 복잡하므로 비추 -> QueryDsl 사용

조건들을 미리 구현하여 조립하는 느낌

 

 

Query By Example(비추)

찾고 싶은 엔티티 객체를 만든 후,

Example.of()를 통해 만든 Example으로 엔티티들 조회할 수 있다.

//ExampleMatcher 생성, age 프로퍼티는 무시
ExampleMatcher matcher = ExampleMatcher.matching()
  .withIgnorePaths("age");
Example<Member> example = Example.of(member, matcher);
// 엔티티 객체 member로 같은 조건을 조회
List<Member> result = memberRepository.findAll(example);

장점

  • 엔티티 객체 그대로 사용하여 동적 쿼리 용이
  • RDB -> NoSql 으로 코드 변화 없이 변경 가능
  • Jpa Repository 인터페이스에 기능이 존재함

단점

  • 내부조인만 가능, 외부조인 불가능
  • 단순한 쿼리 조건만 사용 가능, 중첩 조건 불가능
  • QueryDsl이라는 더 좋은 기술이 있다

 

 

Projections

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

  • Close projection
public interface UsernameOnly {
  String getUsername();
  // 조회할 엔티티의 필드명을 get형식으로 작성
}
public interface MemberRepository ... {
 List<UsernameOnly> findProjectionsByUsername(String username);
  // 메서드 이름은 자유지만, 반환 타입을 위 인터페이스로 설정
}

스프링이 인터페이스의 구현체를 생성하여 원하는 필드만 조회할 수 있는 쿼리 생성

  • open projection
public interface UsernameOnly {
  @Value("#{target.username + ' ' + target.age + ' ' + target.team.name}") // 엔티티 필드 전체를 가져와서 SpEL문법에서 처리
  String getUsername();
  // 조회할 엔티티의 필드명을 get형식으로 작성
}
  • class projection

실제 클래스 구현

public class UsernameOnlyDto {
    private final String username;
    public UsernameOnlyDto(String username) {
      this.username = username;
    }
    public String getUsername() {
      return username;
    }
}
  • 동적 Projection

repository에 제네릭으로 선언하여 사용가능(중복 줄이기)

<T> List<T> findProjectionsByUsername(String username, Class<T> type);
  • 중첩 Projection
public interface NestedClosedProjection {
   String getUsername();
   TeamInfo getTeam();

   interface TeamInfo {
           String getName();
   } // 루트 엔티티가 아니라면 모든 컬럼을 다 select 해온다.
}

정리

  • 프로젝션 대상이 root 엔티티면 최적화 되므로 유용하다.
  • 프로젝션 대상이 root 엔티티를 넘어가면 LEFT OUTER JOIN 처리 되므로 JPQL SELECT 최적화가 안된다. (모든 필드를 SELECT해서 엔티티로 조회한 다음에 계산)
  • 실무의 복잡한 쿼리를 해결하기에는 한계가 있다. 조금만 복잡해지면 QueryDSL을 사용하자

 

 

네이티브 쿼리

  • 페이징 지원
  • 반환 타입
    • Object[]
    • Tuple
    • DTO(Projection 지원)
  • 제약
    • Sort 파라미터를 통한 정렬이 동작 안 할 수 있다.
    • 런타임 오류로만 오류 발견
    • 동적 쿼리 불가능

일단 차라리 JdbcTemplate 을 통한 커스텀 레포지토리를 권장함

Projection을 통한DTO반환도 가능

(select 문을 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);
728x90
반응형

'스프링 > JPA' 카테고리의 다른 글

Querydsl  (0) 2021.08.24
Envers / spring-data-envers  (0) 2021.08.03
JPA 최적화, Hint  (0) 2021.06.04
JPA 변경/삭제 @Modifying  (0) 2021.06.04
FK가 PK가 아닌 다른 컬럼과 연관관계가 있을 때  (0) 2021.05.31

댓글