인프런 김영한 강사님의
실전! 스프링 데이터 JPA
을 정리한 글입니다.
참 도는 거짓으로 평가
AND OR 같은 연산자로 조합하여 다양한 검색조건 쉽게 생성 (컴포지트 패턴)
스프링 데이터 JPA는 org.springframework.data.jpa.domain.Specification
클래스로 정의
JpaSpecifiationExecutor
인터페이스 상속public interface MemberRepository extends JpaRepository<Member, Long>,
JpaSpecificationExecutor<Member> {
}
JpaSpecificationExecutor 인터페이스
public interface JpaSpecificationExecutor<T> {
Optional<T> findOne(@Nullable Specification<T> spec);
List<T> findAll(Specification<T> spec);
Page<T> findAll(Specification<T> spec, Pageable pageable);
List<T> findAll(Specification<T> spec, Sort sort);
long count(Specification<T> spec);
}
Specification
을 파라미터로 받아서 검색 조건 사용@Test
public void specBasic() throws Exception {
//given
Team teamA = new Team("teamA");
em.persist(teamA);
Member m1 = new Member("m1", 0, teamA);
Member m2 = new Member("m2", 0, teamA);
em.persist(m1);
em.persist(m2);
em.flush();
em.clear();
//when
Specification<Member> spec =
MemberSpec.username("m1").and(MemberSpec.teamName("teamA"));
List<Member> result = memberRepository.findAll(spec);
//then
Assertions.assertThat(result.size()).isEqualTo(1);
}
Specification
을 구현하면 명세들을 조립할 수 있다.where()
, and()
, or()
, not()
제공findAll
을 보면 회원 이름 명세(username
)과 팀 이름 명세(teamName
)를 and
로 조합해서 검색 조건으로 사용MemberSpec
명세 정의 코드public class MemberSpec {
public static Specification<Member> teamName(final String teamName) {
return (Specification<Member>) (root, query, builder) -> {
if (StringUtils.isEmpty(teamName)) {
return null;
}
Join<Member, Team> t = root.join("team", JoinType.INNER); //회원과 조인
return builder.equal(t.get("name"), teamName);
};
}
public static Specification<Member> username(final String username) {
return (Specification<Member>) (root, query, builder) -> builder.equal(root.get("username"), username);
}
}
명세를 정의하려면 Specification
인터페이스 구현
명세를 정의할 때는 toPredicate(...)
메서드만 구현하면 되는데 JPA Criteria의 Root
, CriteriaQuery
, CriteriaBuilder
클래스를 파라미터 제공
참고 : 실무에서는 JPA Criteria를 거의 안쓰고, QueryDSL을 사용한다.
package study.datajpa.repository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.domain.Example;
import org.springframework.data.domain.ExampleMatcher;
import org.springframework.transaction.annotation.Transactional;
import study.datajpa.entity.Member;
import study.datajpa.entity.Team;
import javax.persistence.EntityManager;
import java.util.List;
import static org.assertj.core.api.Assertions.*;
@SpringBootTest
@Transactional
public class QueryByExampleTest {
@Autowired MemberRepository memberRepository;
@Autowired EntityManager em;
@Test
public void basic() throws Exception {
//given
Team teamA = new Team("teamA");
em.persist(teamA);
em.persist(new Member("m1", 0, teamA));
em.persist(new Member("m2", 0, teamA));
em.flush();
//when
//Probe 생성
Member member = new Member("m1");
Team team = new Team("teamA"); //내부조인으로 teamA 가능
member.setTeam(team);
//ExampleMatcher 생성, age 프로퍼티는 무시
ExampleMatcher matcher = ExampleMatcher.matching().withIgnorePaths("age");
Example<Member> example = Example.of(member, matcher);
List<Member> result = memberRepository.findAll(example);
//then
assertThat(result.size()).isEqualTo(1);
}
}
Probe : 필드에 데이터가 있는 실제 도메인 객체
ExampleMatcher : 특정 필드를 일치시키는 상세한 정보 제공, 재사용 가능
Example : Probe와 ExampleMatcher로 구성, 쿼리를 생성하는데 사용
동적 쿼리 편리하게 처리
도메인 객체 그대로 사용
데이터 저장소를 RDB에서 NOSQL로 변경해도 코드 변경 없이 추상화되어 있음
스프링 데이터 JPA JpaRepository
인터페이스에 이미 포함
조인은 가능하지만, 내부 조인(INNER JOIN)만 가능함.
다음과 같은 중첩 제약조건 X
firstname - ?0
or (firstname = ?1 and lastname = ?2)매칭 조건이 매우 단순
starts/contains/ends/regex
=
)만 지원실무에서 사용하기에는 매칭 조건이 너무 단순하고, LEFT 조인 불가능
실무에서는 QueryDSL을 사용하자
public interface UsernameOnly {
String getUsername();
}
public interface MemberRepository ... {
List<UsernameOnly> findProjectionsByUsername(String username);
}
@Test
public void projections() throws Exception {
//given
Team teamA = new Team("teamA");
em.persist(teamA);
Member m1 = new Member("m1", 0, teamA);
Member m2 = new Member("m2", 0, teamA);
em.persist(m1);
em.persist(m2);
em.flush();
em.clear();
//when
List<UsernameOnly> result =
memberRepository.findProjectionsByUsername("m1");
//then
Assertions.assertThat(result.size()).isEqualTo(1);
}
select m.username from member m
where m.username=‘m1’;
public interface UsernameOnly {
String getUsername();
}
public interface UsernameOnly {
@Value("#{target.username + ' ' + target.age + ' ' + target.team.name}")
String getUsername();
}
아래와 같이 인터페이스가 아닌 구체적인 DTO 형식도 가능
생성자의 파라미터 이름으로 매칭
public class UsernameOnlyDto {
private final String username;
public UsernameOnlyDto(String username) {
this.username = username;
}
public String getUsername() {
return username;
}
}
T> List<T> findProjectionsByUsername(String username, Class<T> type);
List<UsernameOnly> result = memberRepository.findProjectionsByUsername("m1",
UsernameOnly.class);
public interface NestedClosedProjection {
String getUsername();
TeamInfo getTeam();
interface TeamInfo {
String getName();
}
}
select
m.username as col_0_0_,
t.teamid as col_1_0_,
t.teamid as teamid1_2_,
t.name as name2_2_
from
member m
left outer join
team t
on m.teamid=t.teamid
where
m.username=?
프로젝션 대상이 root 엔티티면, JPQL SELECT 절 최적화 가능
프로젝션 대상이 ROOT가 아니면
프로젝션 대상이 root 엔티티면 유용하다.
프로젝션 대상이 root 엔티티를 넘어가면 JPQL SELECT 최ㅐ적화가 안된다.
실무의 복잡한 쿼리를 해결하기에는 한계가 있다.
실무에서는 단순할 때만 사용하고, 조금만 복잡해지면 QueryDSL 사용
가급적 네이티브 쿼리는 사용 X. 정말 어쩔 수 없을 때
최근 방식 : 스프링 데이터 Projections 활용
페이징 지원
반환 타입
[]
제약
public interface MemberRepository extends JpaRepository<Member, Long> {
@Query(value = "select * from member where username = ?", nativeQuery =
true)
Member findByNativeQuery(String username);
}
JPQL은 위치 기반 파라미터를 1부터 시작하지만 네이티브 SQL은 0부터 시작
네이티브 SQL을 엔티티가 아닌 DTO로 변환하려면 ...
@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);
하이버네이트를 직접 활용
스프링 JdbcTemplate, myBatis, jooq 같은 외부 라이브러리 사용
ex> 하이버네이트 기능 사용
//given
String sql = "select m.username as username from member m";
List<MemberDto> result = em.createNativeQuery(sql)
.setFirstResult(0)
.setMaxResults(10)
.unwrap(NativeQuery.class)
.addScalar("username")
.setResultTransformer(Transformers.aliasToBean(MemberDto.class))
.getResultList();
}