코드 : 실전스프링데이터_JPA 코드
출처 : 실전 스프링 데이터 JPA
Gradle 전체 설정
plugins {
id 'java'
id 'org.springframework.boot' version '2.7.6'
id 'io.spring.dependency-management' version '1.0.15.RELEASE'
}
group = 'study'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
tasks.named('test') {
useJUnitPlatform()
}
gradle 의존관계 보기
./gradlew dependencies --configuration compileClasspath
스프링 부트 라이브러리 살펴보기
핵심 라이브러리
기타 라이브러리
주의
제네릭 타입
주요 메서드
도메인에 특화된 메소드는 어떻게 해결할까? --> 쿼리 메소드
참고 : 이 기능은 엔티티의 필드 명이 변경되면 인터페이스에 정의한 메소드 이름도 함께 변경해야 한다. 그렇지 않으면 애플리케이션을 시작하는 시점에 오류가 발생한다.
--> 애플리케이션 로딩 시점에 오류를 인지할 수 있는 스프링 데이터 JPA의 장점
참고 : 스프링 데이터 JPA를 사용하면 실무에서 Named Query를 직접 등록해서 사용하는 일은 드물다. 대신 @Query를 사용해서 리파지토리 메서드에 쿼리를 직접 정의한다.
@Entity
@NamedQuery(
name="Member.findByUsername",
query="select m from Member m where m.username = :username")
public class Member {
...
}
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
어노테이션 사용 @Query("select m.username from Member m")
List<String> findUsernameList();
@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와 사용방식이 동일)*
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);
스프링 데이터 JPA는 유연한 반환 타입 지원
null
반환javax.persistence.NonUniqueResultException
예외 발생JPA에서 페이징을 어떻게 할 것인가?
페이징과 정렬 파라미터
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);
public interface Page<T> extends Slice<T> {
int getTotalPages(); //전체 페이지 수
long getTotalElements(); //전체 데이터 수
<U> Page<U> map(Function<? super T, ? extends U> converter); //변환기
}
Page<Member> page = memberRepository.findByAge(10, pageRequest);
Page<MemberDto> dtoPage = page.map(m -> new MemberDto());
@Modifying
@Query("update Member m set m.age = m.age + 1 where m.age >= :age")
int bulkAgePlus(@Param("age") int age);
@Modifying
어노테이션을 사용@Modifying(clearAutomatically = true)
(이 옵션 기본값이 false)참고 : 벌크 연산은 영속성 컨텍스트를 무시하고 실행하기 때문에, 영속성 컨텍스트에 있는 엔티티의 상태와 DB에 엔티티 상태가 달라질 수 있다.
연관된 엔티티들을 SQL 한번에 조회하는 방법
member -> team은 지연로딩 관계
연관된 엔티티를 한번에 조회하려면 페치 조인이 필요
스프링 데이터 JPA는 JPA가 제공하는 엔티티 그래프 기능을 편리하게 사용하게 도와준다.
이 기능을 사용하면 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)
사실상 페치 조인의 간편 버전
스프링 데이터 JPA 리포지토리는 인터페이스만 정의하고 구현체는 스프링이 자동 생성
참고 : 실무에서는 주로 QueryDSL이나 SpringJdbcTemplate을 함께 사용할 때 사용자 정의 리포지토리 기능 자주 사용
참고 : 항상 사용자 정의 리포지토리가 필요한 것은 아니다. 그냥 임의의 리포지토리를 만들어도 된다. 예를들어 MemberQueryRepository를 인터페이스가 아닌 클래스로 만들고 스프링 빈으로 등록해서 그냥 직접 사용해도 된다. 이 경우 스프링 데이터 JPA와는 아무런 관계 없이 별도로 동작한다.
엔티티를 생성, 변경할 때 변경한 사람과 시간을 추적하고 싶으면?
등록일
수정일
등록자
수정자
@EnableJpaAuditing
-> 스프링 부트 설정 클래스에 적용해야함
@EntityListeners(AuditingEntityListener.class)
-> 엔티티에 적용
사용 어노테이션
@CreatedDate
@LastModifiedDate
@CreatedBy
@LastModifiedBy
참고 : 실무에서 대부분의 엔티티는 등록시간, 수정시간이 필요하지만, 등록자, 수정자는 없을 수도 있다. 그래서 Base 타입을 분리하고, 원하는 타입을 선택해서 상속한다.
참고 : 저장시점에 등록일, 등록자는 물론이고, 수정일, 수정자도 같은 데이터가 저장된다. 데이터가 중복 저장되는 것 같지만, 이렇게 해두면 변경 컬럼만 확인해도 마지막에 업데이트한 유저를 확인 할 수 있으므로 유지보수 관점에서 편리하다. 이렇게 하지 않으면 변경 컬럼이 null
일 때 등록 컬럼을 또 찾아야 한다.
HTTP 파라미터로 넘어온 엔티티의 아이디로 엔티티 객체를 찾아서 바인딩
@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();
}
id
를 받지만 도메인 클래스 컨버터가 중간에 동작해서 회원 엔티티 객체를 반환주의 : 도메인 클래스 컨버터로 엔티티를 파라미터로 받으면, 이 엔티티는 단순 조회용으로만 사용해야 한다. (트랜잭션이 없는 범위에서 엔티티를 조회했으므로, 엔티티를 변경해도 DB에 반영되지 않는다.)
스프링 데이터가 제공하는 페이징과 정렬 기능을 스프링 MVC에서 편리하게 사용할 수 있다.
@GetMapping("/members")
public Page<Member> list(Pageable pageable) {
Page<Member> page = memberRepository.findAll(pageable);
return page;
}
Pageable
은 인터페이스, 실제는 org.springframework.data.domain.PageRequest
객체 생성/members?page=0&size=3&sort=id,desc&sort=username,desc
접두사
@Qualifier
에 접두사명 추가 "{접두사명}_xxx"/mebers?member_page=0&order_page=1
엔티티를 API로 노출하면 다양한 문제가 발생한다. 그래서 엔티티를 꼭 DTO로 변환해서 반환해야 한다.
Page는 `map()을 지원해서 내부 데이터를 다른 것으로 변경할 수 있다.
스프링 데이터 JPA가 제공하는 공통 인터페이스의 구현체
org.springframework.data.jpa.repository.support.SimpleJpaRepository
JPA 예외를 스프링이 추상와한 예외로 변환
readOnly = true
옵션을 사용하면 플러시를 생략해서 약간의 성능 향상을 얻을 수 있음매우 중요
새로운 엔티티를 판단하는 기본 전략
null
로 판단0
으로 판단Persistable
인터페이스를 구현해서 판단 로직 변경 가능실무에서는 잘 사용 안하며 QueryDSL 사용
실무에서 사용하기 어려움 QueryDSL 사용
엔티티 대신에 DTO를 편리하게 조회할 때 사용
가급적 네이티브 쿼리는 사용하지 않는게 좋음, 정말 어쩔 수 없을 때 사용
최근 방법 -> 스프링 데이터 Projections 활용