Spring Data JPA

이재용·2025년 1월 1일
0

스프링 데이터 JPA가 구현 클래스 대신 생성 = 구현 코드가 필요없음

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

Generic - <T: Entity 타입, ID: PK 타입>
@Repository 애노테이션 생략 가능

주요 메서드

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

쿼리 메소드

인터페이스에 없는 기능을 추가하고 싶음 -> 쿼리 메소드가 해결
__메소드 이름으로 쿼리 생성

public interface MemberRepository extends JpaRepository<Member, Long> {
	List<Member> findByUsernameAndAgeGreaterThan(String username, int age);
	List<Member> findHelloBy(); - 조회
}
  • 조회: find…By
  • COUNT: count…By 반환타입 long
  • EXISTS: exists…By 반환타입 boolean
  • 삭제: delete…By, remove…By 반환타입 long
  • DISTINCT: findDistinct, findMemberDistinctBy
  • LIMIT: findFirst3, findFirst, findTop, findTop3

__@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);
	
	//Collection 타입으로 in절 지원
	@Query("select m form Member m where m.username in :names")
	List<Member> findByNames(@Param("names") List<String> names);
	
	//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가 필요하다.

페이징

특별한 반환 타입

  • Page : 추가 count 쿼리 결과를 포함하는 페이징
  • Slice : 추가 count 쿼리 없이 다음 페이지만 확인 가능(내부적 으로 limit + 1조회)
  • List(자바 컬렉션) : 추가 count 쿼리 없이 결과만 반환
public interface MemberRepository extends JpaRepository<Member, Long> {
	Page<Member> findByAge(int age, Pageable pageable);
}

//실제 사용. 파라미터는(page, size, 정렬조건)
PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "username")); //PageReuest는 Pageable의 구현체
Page<Member> page = memberRepository.findByAge(10, pageRequest);


List<Member> content = page.getContent(); //조회된 데이터
long totalElements = page.getTotalElements(); //totalCount=전체 데이터 수
int pageNum = page.getNumber(); //현재 page. 0부터 시작
int totalPage = page.getTotalPages(); //전체page개수
boolean first = page.isFirst(); //첫번째 page인지 여부
boolean next = page.hasNext(); //다음 페이지가 있는지 여부

count 쿼리 분리 - 실무에서 매우 중요!!!

@Query(value = "select m from Member m left join m.team t",
	   countQuery = "select count(m) from Member m")
Page<Member> findMemberAllCountBy(Pageable pageable);

분리하는 이유 : 이 경우 Member를 가져올 때 Team을 left outer 조인해서 가져오는데 이러면 totalcount는 left outer 조인할 필요없이 Member값만 참조하면 된다. 조인해서 totalcount를 가져온다면 값은 똑같겠지만 성능이 저하된다.

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

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

Web 확장

스프링 데이터가 제공하는 페이징과 정렬 기능을 스프링 MVC에서 편리하게 사용할 수 있다.

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

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

참고 - 하이버네이트 6에서 의미없는 left join 최적화

"select m from Member m left join m.team t"
= select m from Member m
왜냐하면 쿼리가 Team을 전혀 사용하지 않는다. = select절이나, where절에서 사용하지 않는다.

이런 경우 JPA는 최적화를 해서 join 없이 해당 내용만으로 SQL을 만든다. 만약 Member와 Team을 하나의 SQL로 한번에 조회하고 싶다면 JPA가 제공하는 fetch join을 사용해야 한다.

벌크성 수정 쿼리

public interface MemberRepository extends JpaRepository<Member, Long> {
	@Modifying
	@Query("update Member m set m.age = m.age + 1 where m.age >= :age")
	int bulkAgePlus(@Param("age") int age); //리턴값은 수정된 데이터개수
}

벌크성 수정, 삭제 쿼리는 @Modifying 어노테이션을 사용
문제
벌크 연산은 영속성 컨텍스트를 무시하고 실행하기 때문에, 영속성 컨텍스트에 있는 엔티티의 상태와 DB에 엔티티 상태가 달라질 수 있다.
해결
1. 영속성 컨텍스트에 엔티티가 없는 상태에서 벌크 연산을 먼저 실행한다.
2. 부득이하게 영속성 컨텍스트에 엔티티가 있으면 벌크 연산 직후 영속성 컨텍스트를 초기화 한다. em.flush()&em.clear()
@Modifying(clearAutomatically = true) : 벌크성 쿼리를 실행하고 나서 영속성 컨텍스트 초기화

@EntityGraph

  • 사실상 페치 조인(FETCH JOIN)의 간편 버전
  • LEFT OUTER JOIN 사용
//공통 메서드 오버라이드
@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을 쓰는 동시에 Querydsl을 사용하려고 한다면 or 인터페이스의 메서드를 직접 구현하고 싶다면
문제
스프링 데이터 JPA가 제공하는 인터페이스를 직접 구현하면 구현해야 하는 기능이 너무 많음
해결
1. 사용자 정의 인터페이스&구현 클래스 만들기
2. 스프링 데이터 JPA 인터페이스에 사용자 정의 인터페이스 상속

public interface MemberRepository extends JpaRepository, MemberRepositoryCustom {}

사용자 정의 구현 클래스 네이밍 규칙

사용자 정의 인터페이스 명 + Impl
ex)MemberRepositoryCustomImpl
스프링 데이터 JPA가 인식해서 스프링 빈으로 등록

김영한의 팁

하나의 리포지토리 인터페이스에 기능이 너무 많으면 안좋다.
핵심 비즈니스에 쓰는 리포지토리와 화면에 맞춘 Dto를 쓰는 리포지토리를 분리해라.

Auditing

엔티티를 생성, 변경할 때 변경한 사람과 시간을 추적

설정

스프링부트 설정 클래스에 적용

  • @EnableJpaAuditing

    엔티티에 적용
  • @EntityListeners(AuditingEntityListener.class)

  • @MappedSuperclass

사용 어노테이션

  • @CreatedDate

  • @LastModifiedDate

  • @CreatedBy

  • @LastModifiedBy

등록자, 수정자를 처리해주는 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());
	 }
}

0개의 댓글