실전! 스프링 데이터 JPA 수업을 듣고 정리한 내용입니다.
Specifications
와Query By Example
은 실무에서 거의 사용하지 않는다.
그러므로, 정리하지 않고 넘어가기
실무에서는
JPA Criteria
를 거의 쓰지 않는다. (진짜 복잡한 코드를 사용한다.) 대신에QueryDSL
을 사용하자!
Query By
를 실무에서 사용하기에는 매칭 조건이 너무 단순하고,LEFT 조인
이 되지 않는다. 실무에서는QueryDSL
을 사용하자!
엔티티 대신에
DTO
를 편리하게 조회할 때 사용한다.
ex) 전체 엔티티가 아니라 만약 회원 이름만 조회하고 싶을 때
✔️ 인터페이스 기반 Closed Projections
public interface UsernameOnly {
String getUsername();
}
getter
형식으로 지정하면 해당 필드만 선택해서 조회한다. (Projection
)
MemberRepository에 추가
public interface MemberRepositry ... {
List<UsernameOnly> findProjectionsByUsername(@Param("username") String username);
}
테스트 소스
@Test
public void projections() {
// 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
절에서 username
만 조회(Projection)하는 것을 확인할 수 있다.
✔️ 인터페이스 기반 Closed Projections
프로퍼티 형식(
getter
)의 인터페이스를 제공하면, 구현체는 스프링 데이터 JPA가 제공한다.
UsernameOnly 인터페이스 추가
public interface UsernameOnly{
String getUsername();
}
인터페이스 기반 Open Projections
스프링의 SpEL 문법도 지원한다.
public interface UsernameOnly {
@Value("#{target.username + ' ' + target.age}")
String getUsername();
}
JPQL SELECT 절
최적화가 안된다.
테스트 실행 결과
✔️ 클래스 기반 Projection
- 인터페이스가 아닌 구체적인 DTO 형식도 가능하다.
- 생성자의 파라미터 이름으로 매칭한다.
UsernameOnlyDto클래스 생성
public class UsernameOnlyDto {
private final String username;
public UsernameOnlyDto(String username) {
this.username = username;
}
public String getUsername() {
return username;
}
}
DTO
형식도 가능하다.
실행 결과
✔️ 동적 Projections
<T> List<T> findProjectionsByUsername(@Param("username") String username, Class<T> type);
Generic type
을 주면, 동적으로 프로젝션 데이터 변경 가능하다.
테스트 코드
@Test
public void projections() {
// 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<UsernameOnlyDto> result = memberRepository.findProjectionByUsername("m1", UsernameOnlyDto.class);
for (UsernameOnlyDto usernameOnly : result) {
System.out.println("usernameOnly = " + usernameOnly.getUsername());
}
}
실행 결과
NestedClosedProjection 인터페이스 추가
public interface NestedClosedProjection {
String getUsername();
TeamInfo getTeam();
interface TeamInfo {
String getName();
}
}
테스트 코드 추가
@Test
public void projections() {
// 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<NestedClosedProjections> result = memberRepository.findProjectionByUsername("m1", NestedClosedProjections.class);
for (NestedClosedProjections nestedClosedProjections : result) {
String username = nestedClosedProjections.getUsername();
System.out.println("username = " + username);
String teamName = nestedClosedProjections.getTeam().getName();
System.out.println("teamName = " + teamName);
}
}
실행 결과
member
는 username
만, team
은 모두 조회되었다.
⚠️ 주의
- 프로젝션 대상이
root
엔티티면, JPQL SELECT 절 최적화가 가능하다.- 프로젝션 대상이
root
가 아니라면
LEFT OUTER JOIN
으로 처리한다.- 모든 필드를 SELECT해서 엔티티로 조회한 다음에 계산한다.
📌 정리
- 프로젝션 대상이
root
엔티티면projection
이 유용하다.- 프로젝션 대상이
root
엔티티를 넘어가면 JPQL SELECT 최적화가 안된다!
- 실무의 복잡한 쿼리를 해결하기에는 한계가 있다.
- 실무에서는 단순할 때만 사용하고, 조금만 복잡해지면
QueryDSL
을 사용하자!
가급적 네이티브 쿼리는 사용하지 않는게 좋다.
최근에Projections
활용이 나왔는데, 이 기능을 사용하자!
Object[]
Tuple
DTO
(스프링 데이터 인터페이스 Projections
지원) Sort
파라미터를 통한 정렬이 정상 동작하지 않을 수 있다. (믿지 말고 직접 처리)
✔️ JPA 네이티브 SQL 지원
MemberRepository에 추가
public interface MemberRepository extends JpaRepository<Member, Long> {
@Query(value = "select * from member where username = ?", nativeQuery = true)
Member findByNativeQuery(String username);
}
테스트 코드
@Test
public void nativeQuery() {
// 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
Member result = memberRepository.findByNativeQuery("m1");
System.out.println("result = " + result);
}
실행 결과
@SqlResultSetMapping
→ 복잡 Hibernate ResultTransformer
를 사용해야 한다. → 복잡 JdbcTemplate
or myBatis
권장한다.
ex) 스프링 데이터 JPA 네이티브 쿼리 + 인터페이스 기반 Projections 활용
MemberProjection
public interface MemberProjection {
Long getId();
String getUsername();
String getTeamName();
}
MemberRepository에 추가
@Query(value = "select m.member_id as id, m.username, t.name as teamName " +
"from member m left join team t",
countQuery = "select count(*) from member",
nativeQuery = true)
Page<MemberProjection> findByNativeProjection(Pageable pageable);
테스트 코드
@Test
public void nativeQuery() {
// 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
Page<MemberProjection> result = memberRepository.findByNativeProjection(PageRequest.of(0, 10));
List<MemberProjection> content = result.getContent();
for (MemberProjection memberProjection : content) {
System.out.println("memberProjection.username = " + memberProjection.getUsername());
System.out.println("memberProjection.teamname = " + memberProjection.getTeamName());
}
}
실행 결과
✔️ 동적 네이티브 쿼리
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();
}