본문 바로가기
JPA/실전! 스프링 데이터 JPA

Spring Data JPA

by JHyun0302 2023. 8. 24.
728x90

예제 도메인 모델 ERD

 

 

 

 

 

 

스프링 데이터 JPA가 구현 클래스 대신 생성

 

 

 

 

스프링 데이터 JPA 기반 MemberRepository

public interface MemberRepository extends JpaRepository<Member, Long> {
}

 

 

 

 

공통 인터페이스 분석

public interface JpaRepository<T, ID extends Serializable>
{
...
}

 

JpaRepository 인터페이스: 공통 CRUD 제공

Generic

    T : 엔티티 타입

    ID : 식별자 타입(PK)

 

 

 

 

Generic

    T : 엔티티

    ID : 엔티티의 식별자 타입(PK)

    S : 엔티티와 그 자식 타입

 

 

 

 

주요 메서드

  • save(S) : 새로운 엔티티는 저장하고 이미 있는 엔티티는 병합한다.
  • delete(T) : 엔티티 하나를 삭제한다. 내부에서 EntityManager.remove() 호출
  • findById(ID) : 엔티티 하나를 조회한다. 내부에서 EntityManager.find() 호출
  • getOne(ID) : 엔티티를 프록시로 조회한다. 내부에서 EntityManager.getReference() 호출
  • findAll(...) : 모든 엔티티를 조회한다. 정렬( Sort )이나 페이징( Pageable ) 조건을 파라미터로 제공할 수 있다.

 

 

 

 

 

 


쿼리 메소드 기능

  

 

1. 메소드 이름으로 쿼리 생성

 

public interface MemberRepository extends JpaRepository<Member, Long> {  
	List<Member> findByUsernameAndAgeGreaterThan(String username, int age);
}

 

 

 

 

 

스프링 데이터 JPA가 제공하는 쿼리 메소드 기능

 

 

 

 

 

2. JPA NamedQuery

 

 

스프링 데이터 JPA로 NamedQuery 사용

// @Query 생략해도 됨 -> 자동으로 JpaRepository<Member> + 메서드 명(findByUsername)
@Query(name = "Member.findByUsername") 
List<Member> findByUsername(@Param("username") String username);

→ 스프링 데이터 JPA는 선언한 "도메인 클래스 + .() + 메서드 이름"으로 Named 쿼리를 찾아서 실행

 

 

 

 

 

 

 


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

 

 

 

메서드에 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") int age);
}

 

 

◎ 참고 : 메서드 이름으로 쿼리 생성 기능은 파라미터가 증가하면 메서드 이름이 지저분해진다. 그래서 @Query 기능을 자주 사용한다.

 

 

 

 


파라미터 바인딩

 

 

 

이름 기반 파라미터 바인딩 쓰기!

 public interface MemberRepository extends JpaRepository<Member, Long> {
    @Query("select m from Member m where m.username = :name")
    Member findMembers(@Param("name") String username);
}

 

 

 

 

 


반환 타입

 

 

List<Member> findByUsername(String name); //컬렉션 
//결과 없음: 빈 컬렉션 반환

Member findByUsername(String name); //단건
Optional<Member> findByUsername(String name); //단건 Optional
//결과 없음: null 반환
//결과가 2건 이상: javax.persistence.NonUniqueResultException 예외 발생

 

 

 


페이징과 정렬

 

페이징과 정렬 파라미터

  • 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 쿼리 없이 결과만 반환

 

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 MemberRepository extends Repository<Member, Long> {
	Page<Member> findByAge(int age, Pageable pageable);
}

 

 

 

Page 사용 예제 실행 코드

//페이징 조건과 정렬 조건 설정
@Test
public void page() throws Exception {
    //given
    ...
    
    //when
    PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "username"));
    Page<Member> page = memberRepository.findByAge(10, pageRequest);

    //then
    assertThat...
}

 

 

 

 

 

주의: Page1부터 시작이 아니라 0부터 시작이다.

 

 

 

 

 

★ Count 쿼리 분리 : Count 쿼리가 단순한 경우 (굳이 조인이 필요 없을 때 - 성능향상 가능!!)

@Query(value = “select m from Member m”, countQuery = “select count(m.username) from Member m”)
Page<Member> findMemberAllCountBy(Pageable pageable);

 

 

 

 

 

 

 

 

 


벌크성 수정 쿼리

 

 

 

// 조건에 해당하는 모든 회원의 나이 + 1

@Modifying
@Query("update Member m set m.age = m.age + 1 where m.age >= :age")
int bulkAgePlus(@Param("age") int age);

 

  • 벌크성 쿼리를 실행 후 영속성 컨텍스트 초기화: @Modifying(clearAutomatically = true)
     (기본값은 false)
  • 이 옵션 없이 회원을 findById로 다시 조회하면 영속성 컨텍스트에 과거 값이 남아서 문제가 될 수 있다. 

 

 

※ 권장하는 방안

  1. 영속성 컨텍스트에 엔티티가 없는 상태에서 벌크 연산을 먼저 실행한다.
  2. 부득이하게 영속성 컨텍스트에 엔티티가 있으면 벌크 연산 직후 영속성 컨텍스트를 초기화 한다.

 

 

 

 

 


@EntityGraph

 

 

 

페치 조인(FETCH JOIN)의 간편 버전

LEFT OUTER JOIN 사용

 

 

 

 

EntitiyGraph - Member와 {"team"}  페치조인

//공통 메서드 오버라이드
@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)

 

 

 

 

 

NamedEntityGraph 사용 방법 : 잘 사용하지 않는다. (엔티티에 써야함.)

@NamedEntityGraph(name = "Member.all", attributeNodes =
@NamedAttributeNode("team"))
@Entity
public class Member {}

 

 

 

@EntityGraph("Member.all")
@Query("select m from Member m")
List<Member> findMemberEntityGraph();

 

 

 

 

 

 


JPA Hint & Lock

 

스프링 데이터 JPA 사용시 변경감지 때문에 원복 data 저장해놓음!(메모리 낭비!)

→ 100% 조회용으로 쓴다면 JPA Hint 쓸 것! (큰 성능 개선 X - 암달의 법칙)

 

쿼리 힌트 사용

@QueryHints(value = @QueryHint(name = "org.hibernate.readOnly", value = "true"))
Member findReadOnlyByUsername(String username);

@QueryHints(value = { @QueryHint(name = "org.hibernate.readOnly", value = "true")}, forCounting = true)
Page<Member> findByUsername(String name, Pageable pageable);

 

forCounting : Page 인터페이스를 반환타입으로 하면 추가로 페이징을 위한 count 쿼리도 쿼리 힌트 적용(기본값 true )

 

 

 

Lock : 락 기능 제공. 잘 사용 하지 않음.

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

 

 

 

 

 

 


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

 

 

사용자 정의 인터페이스

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

 

 

 

 

사용자 정의 리포지토리 구현 최신 방식 (스프링 데이터 2.x 부터 지원)

사용자 정의 인터페이스 명 + Impl 방식
ex)
MemberRepositoryImpl 대신에 MemberRepositoryCustomImpl 가능

 

 

 

 

 

 


Auditing

 

 

  • 등록일 → @CreatedDate
  • 수정일  @LastModifiedDate
  • 등록자  @CreatedBy
  • 수정자  @LastModifiedBy




설정

@EnableJpaAuditing //스프링 부트 설정 클래스에 적용해야함
@EntityListeners(AuditingEntityListener.class) //엔티티에 적용

→ @EntityListeners(AuditingEntityLister.class) 생략하고 orm.xml 작성해도 된다.

 

 

 

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

// 실제로는 세션 or 토큰에서 만든 UUID로 등록자, 수정자 체크한다.
@Bean
public AuditorAware<String> auditorProvider() {
	return () -> Optional.of(UUID.randomUUID().toString());
}

 

 

 

 

 

스프링 데이터 Auditing 적용 - 등록자, 수정자

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;
}

 

 

 

 

 


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

 

 

도메인 클래스 컨버터 사용 후

@RestController
@RequiredArgsConstructor
public class MemberController {
    
    private final MemberRepository memberRepository;
    
    @GetMapping("/members/{id}")
    public String findMember(@PathVariable("id") Member member) {
    	return member.getUsername();
    }
}

 

주의: 도메인 클래스 컨버터로 엔티티를 파라미터로 받으면, 이 엔티티는 단순 조회용으로만 사용해야 한다. (트랜잭션 X)

 

 

 

 

 

 

 

 

 


Web 확장 - 페이징과 정렬

 

 

 

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

 

 

 

 

요청 파라미터 : ex) /members?page=0&size=3&sort=id,desc&sort=username,desc

  • page: 현재 페이지, 0부터 시작한다.
  • size: 한 페이지에 노출할 데이터 건수
  • sort: 정렬 조건을 정의한다.
        ex) 정렬 속성,정렬 속성...(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)

ex) /members?member_page=0&order_page=1

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

 

 

 

 

 

 

Page 내용을 DTO로 변환하기

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

 

 

 

 

 

 

 

 

Page1부터 시작하기

1. 직접 Page 클래스를 만들어서 처리한다. 그리고 직접 PageRequest(Pageable 구현체)를 생성한다.

2. application.yml : spring.data.web.pageable.one-indexed-parameters = true

    단점 : 페이지는 1부터 시작하나 아래 부가적인 내용은 바뀌지 않음. (ex. sort, offset 값은 index = 0 상태로 나옴.)

 

 

 

 

 

 

 


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

 

 

 

★ *save() 메서드*

  • 새로운 엔티티면 저장( persist )
  • 새로운 엔티티가 아니면 병합( merge )

 

 

 

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

  • 식별자가 객체일 때 null 로 판단. ex) Long id
  • 식별자가 자바 기본 타입일 때 0 으로 판단  ex) long id
  • Persistable 인터페이스를 구현해서 판단 로직 변경 가능

 

 

 

 

SimpleJpaRepository

@Transactional
@Override
public <S extends T> S save(S entity) {

    if (entityInformation.isNew(entity)) {
        em.persist(entity);
        return entity;
    } else {
        return em.merge(entity);
    }
}

 

문제 : 엔티티에서 @GenerateValue 없고 @Id로 PK 값을 할당하는 경우 isNew() = false로 판단. → em.merge() 작동!

해결 방법 : persistable 인터페이스 구현해서 판단 로직 변경! (@GenerateValue 못 쓰는 상황에서 할 것.)

 

 

 

 

 

 

 

 

 


Specifications (명세) - JPA Criteria 활용해서 사용

 

 

 

 


Query By Example

 

 

 

◎ 참고 : https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#query-by-example

 

 

장점

  1. 동적 쿼리를 편리하게 처리
  2. 도메인 객체를 그대로 사용


 

 

단점

  1. 조인은 가능하지만 내부 조인(INNER JOIN)만 가능함 외부 조인(LEFT JOIN) 안됨
  2. 중첩 제약조건 안됨 ex) firstname = ?0 or (firstname = ?1 and lastname = ?2)
  3. 매칭 조건이 매우 단순함
        문자는 starts/contains/ends/regex
        다른속성은정확한매칭( = )만지원


 

 

 

 

 


 

 

엔티티 대신에 DTO를 편리하게 조회할 때 사용 : 전체 엔티티 조회 X, 필요한 값만 조회 할 때 사용.

 

 

 

인터페이스 기반 Projections

@Value 있으면: Open Projections
- 단점: DB에서 엔티티 정보 다 조회해온 다음에 계산 -> JPQL Select절 최적화 안됨

@Value 없으면: Close Projections
- 정확하게 매칭. DB에서 원하는 값(username)만 select절에 담아서 가져옴.

public interface UsernameOnly {
    @Value("#{target.username + ' ' + target.age + ' ' + target.team.name}")
    String getUsername();
}

 

 

 

 

클래스 기반 Projections

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

    public String getUsername() {
        return username;
    }
}

 

 

 

 

동적 Projections

<T> List<T> findProjectionsByUsername(String username, Class<T> type);

 

 

 

 

중첩 구조 처리

public interface NestedClosedProjection {

    String getUsername();
    TeamInfo getTeam();
    
    interface TeamInfo {
    	String getName();
    }
}

 

 

 

 

☆ 주의

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

 

 

 

 

 

 


네이티브 쿼리

 

 

1. 페이징 지원
2. 반환 타입

  • Object[]
  • Tuple
  • DTO(스프링 데이터 인터페이스 Projections 지원)

3. 제약

  • Sort 파라미터를 통한 정렬이 정상 동작하지 않을 수 있음(믿지 말고 직접 처리)
  • JPQL처럼 애플리케이션 로딩 시점에 문법 확인 불가
  • 동적 쿼리 불가

 

 

 

JPA 네이티브 SQL 지원

public interface MemberRepository extends JpaRepository<Member, Long> {
    @Query(value = "select * from member where username = ?", nativeQuery = true)
    Member findByNativeQuery(String username);
}

 

 

 

 

 

Projections 활용

@Query(value = "SELECT m.member_id as id, m.username, t.name as teamName " +
          "FROM member m left join team t ON m.team_id = t.team_id",
          countQuery = "SELECT count(*) from member",
          nativeQuery = true)
Page<MemberProjection> findByNativeProjection(Pageable pageable);

 

 

 

 

 

 

 

 

☆ 정리 : Querydsl 쓰자!

반응형