Spring Data JPA - 싹다 정리

초보개발자·2024년 1월 8일

JPA

목록 보기
8/11
post-thumbnail

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

org.springframework.data.repository.Repository 를 구현한 클래스는 스캔 대상

  • MemberRepository 인터페이스가 동작한 이유
  • 실제 출력해보기(Proxy)
  • memberRepository.getClass() class com.sun.proxy.$ProxyXXX

@Repository 애노테이션 생략 가능

  • 컴포넌트 스캔을 스프링 데이터 JPA가 자동으로 처리
  • JPA 예외를 스프링 예외로 변환하는 과정도 자동으로 처리
public interface MemberRepository extends JpaRepository<Member, Long> {
}

📗공통 인터페이스 구성

변경사항

  • T findOne(ID) -> Optional< T> findById(ID) 변경
  • boolean exists(ID) -> boolean existsById(ID) 변경

제네릭 타입

  • T : 엔티티
  • ID : 엔티티의 식별자 타입
  • S : 엔티티와 그 자식 타입

주요 메서드

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

📘메소드 이름으로 쿼리 생성

1. 예시) 이름과 나이를 기준으로 회원을 조회하려면?

// 순수 JPA 리포지토리
public List<Member> findByUsernameAndAgeGreaterThan(String username, int age) {
	return em.createQuery("select m from Member m where m.username = :username and m.age > :age")
			.setParameter("username", username)
			.setParameter("age", age)
			.getResultList();
// 스프링 데이터 JPA
public interface MemberRepository extends JpaRepository<Member, Long> {
	List<Member> findByUsernameAndAgeGreaterThan(String username, int age);
}

2. 다양한 쿼리 메소드

  • COUNT: count…By 반환타입 long
  • EXISTS: exists…By 반환타입 boolean
  • 삭제: delete…By, remove…By 반환타입 long
  • DISTINCT: findDistinct, findMemberDistinctBy
  • LIMIT: findFirst3, findFirst, findTop, findTop3

엔티티의 필드명이 변경되면 인터페이스에 정의한 메서드 이름도 꼭 함께 변경!!!
그렇지 않으면 애플리케이션을 시작하는 시점에 오류가 발생한다
애플리케이션 로딩 시점에 오류를 인지할 수 있는 것이 스프링 데이터 JPA의 매우 큰 장점


📕@Query

  • 리포지토리 메소드에 쿼리 정의하기
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);
}
  • @org.springframework.data.jpa.repository.Query 어노테이션을 사용
  • 실행할 메서드에 정적 쿼리를 직접 작성하므로 이름 없는 Named 쿼리라 할 수 있음
  • JPA Named 쿼리처럼 애플리케이션 실행 시점에 문법 오류를 발견할 수 있음(매우 큰 장점!)

1. 단순히 값 하나를 조회

@Query("select m.username from Member m")
List<String> findUsernameList();
  • JPA 값 타입( @Embedded )도 이 방식으로 조회할 수 있다

2. DTO로 직접 조회

@Query("select new study.datajpa.dto.MemberDto(m.id, m.username, t.name) " +
 "from Member m join m.team t")
List<MemberDto> findMemberDto();
  • 주의! DTO로 직접 조회 하려면 JPA의 new 명령어를 사용해야 한다
  • 생성자가 맞는 DTO가 필요하다 (JPA와 사용방식이 동일하다.)

3. 파라미터 바인딩

@Query("select m from Member m where m.username = :name")
Member findMembers(@Param("name") String username);

4. 컬렉션 파라미터 바인딩

@Query("select m from Member m where m.username in :names")
List<Member> findByNames(@Param("names") List<String> names);

📜반환 타입

  • 스프링 데이터 JPA는 유연한 반환 타입 지원
List<Member> findByUsername(String name); //컬렉션
Member findByUsername(String name); //단건
Optional<Member> findByUsername(String name); //단건 Optional

📑페이징과 정렬

  • org.springframework.data.domain.Page : 추가 count 쿼리 결과를 포함하는 페이징 (페이지는 0부터 시작)
  • 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);

1. Pagable 인터페이스 구현

PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "username"));

2. Page 인터페이스

public interface Page<T> extends Slice<T> {
	int getTotalPages(); //전체 페이지 수
	long getTotalElements(); //전체 데이터 수
	<U> Page<U> map(Function<? super T, ? extends U> converter); //변환기
}

3. 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); //변환기
}

4. count 쿼리 분리

  • 카운트 쿼리 분리(이건 복잡한 sql에서 사용, 데이터는 left join, 카운트는 left join 안해도 됨)
    -> 실무에서 매우 중요!!!
@Query(value = "select m from Member m", 
	countQuery = "select count(m.username) from Member m")
Page<Member> findMemberAllCountBy(Pageable pageable);

5. 페이지를 유지하면서 엔티티를 DTO로 변환

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

📓벌크성 수정 쿼리

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

참고: 벌크 연산은 영속성 컨텍스트를 무시하고 실행하기 때문에, 영속성 컨텍스트에 있는 엔티티의 상태와 DB에 엔티티 상태가 달라질 수 있다

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


📒@EntityGraph

  • 연관된 엔티티들을 SQL 한번에 조회하는 방법

JPQL 페치 조인

@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)

🔒JPA Hint & Lock

쿼리 힌트 사용

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

쿼리 힌트 Page 추가 예제

@QueryHints(value = { @QueryHint(name = "org.hibernate.readOnly", value = "true")},
			forCounting = true)
Page<Member> findByUsername(String name, Pageable pageable);
  • org.springframework.data.jpa.repository.QueryHints 어노테이션을 사용
  • forCounting : 반환 타입으로 Page 인터페이스를 적용하면 추가로 호출하는 페이징을 위한 count 쿼리도 쿼리 힌트 적용(기본값 true )

Lock

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

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

사용자 정의 인터페이스

public interface MemberRepositoryCustom {
 List<Member> findMemberCustom();
}

사용자 정의 인터페이스 구현 클래스

@RequiredArgsConstructor
public class MemberRepositoryCustomImpl 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가 인식해서 스프링 빈으로 등록

⭐Auditing

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

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

1. 설정

  • @EnableJpaAuditing 스프링 부트 설정 클래스에 적용해야함
  • @EntityListeners(AuditingEntityListener.class) 엔티티에 적용
@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;
}

분리 가능(등록자, 수정자는 없을 수도 있기에...)

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

2. AuditorAware 스프링 빈 등록

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

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

💻Web 확장 - 페이징과 정렬

1. 파라미터로 Pageable

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

요청 파라미터

  • 예) /members?page=0&size=3&sort=id,desc&sort=username,desc
  • page: 현재 페이지, 0부터 시작한다
  • size: 한 페이지에 노출할 데이터 건수
  • sort: 정렬 조건을 정의한다
    예) 정렬 속성,정렬 속성...(ASC | DESC), 정렬 방향을 변경하고 싶으면 sort 파라미터 추가 ( asc 생략 가능)

2. @PageableDefault

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

3. 페이징 정보가 둘 이상

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

4. Page 내용을 DTO로 변환

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

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

@Repository
@Transactional(readOnly = true)
public class SimpleJpaRepository<T, ID> ...{
	@Transactional
	public <S extends T> S save(S entity) {
		if (entityInformation.isNew(entity)) {
			em.persist(entity);
			return entity;
		} else {
			return em.merge(entity);
		}
	}
	...
}

@Repository 적용

  • JPA 예외를 스프링이 추상화한 예외로 변환

@Transactional 트랜잭션 적용

  • JPA의 모든 변경은 트랜잭션 안에서 동작
  • 스프링 데이터 JPA는 변경(등록, 수정, 삭제) 메서드를 트랜잭션 처리
  • 서비스 계층에서 트랜잭션을 시작하지 않으면 리파지토리에서 트랜잭션 시작
  • 서비스 계층에서 트랜잭션을 시작하면 리파지토리는 해당 트랜잭션을 전파 받아서 사용
  • 그래서 스프링 데이터 JPA를 사용할 때 트랜잭션이 없어도 데이터 등록, 변경이 가능했음(사실은 트랜잭션이 리포지토리 계층에 걸려있는 것임)

@Transactional(readOnly = true)

  • 데이터를 단순히 조회만 하고 변경하지 않는 트랜잭션에서 readOnly = true 옵션을 사용하면 플러시를 생략해서 약간의 성능 향상을 얻을 수 있음
  • 자세한 내용은 JPA 책 15.4.2 읽기 전용 쿼리의 성능 최적화 참고

⭐save() 메서드(매우 중요!!!)

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

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

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

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

참고: JPA 식별자 생성 전략이 @GenerateValue 면 save() 호출 시점에 식별자가 없으므로 새로운 엔티티로 인식해서 정상 동작한다. 그런데 JPA 식별자 생성 전략이 @Id 만 사용해서 직접 할당이면 이미 식별자 값이있는 상태로 save() 를 호출한다. 따라서 이 경우 merge() 가 호출된다. merge() 는 우선 DB를 호출해서 값을 확인하고, DB에 값이 없으면 새로운 엔티티로 인지하므로 매우 비효율 적이다. 따라서 Persistable 를 사용해서 새로운 엔티티 확인 여부를 직접 구현하게는 효과적이다. 참고로 등록시간( @CreatedDate )을 조합해서 사용하면 이 필드로 새로운 엔티티 여부를 편리하게 확인할 수 있다. (@CreatedDate에 값이 없으면 새로운 엔티티로 판단)

@Entity
@EntityListeners(AuditingEntityListener.class)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Item implements Persistable<String> {
	@Id
	private String id;
	@CreatedDate
	private LocalDateTime createdDate;
	public Item(String id) {
		this.id = id;
	}
	@Override
	public String getId() {
		return id;
	} 
    @Override //중요
	public boolean isNew() {
		return createdDate == null;
	}
}

Reference

[인프런] 실전! 스프링 데이터 JPA - 김영한

profile
꾸준히 빠르게

0개의 댓글