앞으로 나올 기능들은 실무에서 쓰기에는 애매한 기능들이라 나머지 기능들이라 정하였으니 비교적 가볍게 들어도 되는 주제입니다.
Specifications(명세)
책 도메인 주도 설계라는 책을 보면 Specification(명세)라는 개념을 소개합니다. 스프링 데이터 JPA는 JPA Criteria를 활용해서 이 개념을 사용할 수 있도록 지원하고 있습니다.
술어(predicate)
- 참 또는 거짓으로 평가
- AND OR 같은 연산자로 조합해서 다양한 검색조건을 쉽게 생성(컴포지트 패턴)
- 예) 검색 조건 하나하나
- 스프링 데이터 JPA는
org.springframework.data.jpa.domain.Spcification클래스로 정의
명세 기능 사용 방법
JpaSpecificationExecutor 인터페이스 상속
public interface MemberRepository extends JpaRepository<Member, Long>,
JpaSpecificationExecutor<Member> {
}JpaSpecificationExecutor 인터페이스
public interface JpaSpcificationExecutor<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(),not()제공findAll을 보면 회원 이름 명세(username)와 팀 이름 명세 (teamName)를and로 조합해서 검색 조건으로 사용
MemberSpec 명세 정의 코드
public class MemberSpec {
public static Specification<Member> teamName(final String teamName) {
return (root, query, criteriaBuilder) -> {
if (StringUtils.isEmpty(teamName)) {
return null;
}
Join<Object, Team> t = root.join("team", JoinType.INNER); // 회원과 조인
return criteriaBuilder.equal(t.get("name"), teamName);
};
}
public static Specification<Member> username(final String username) {
return (root, query, criteriaBuilder) ->
criteriaBuilder.equal(root.get("username"), username);
}
}- 명세를 정의하려면
Specification인터페이스를 구현 - 명세를 정의할 때는
toPredicate(...)메서드만 구현하면 되는데 JPA Criteria의Root,CriteriaQuery,CriteriaBuilder클래스를 파라미터 제공
이전 JPA 강의에서 설명했듯 알아 보기가 힘들기 때문에 criteria는 실무에서 거의 쓰지 않기에 QueryDSL을 사용해야 합니다.
Query By Example
@Test
public void queryByExample() 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
// then
// Probe
Member member = new Member("m1");
ExampleMatcher matcher = ExampleMatcher.matching()
.withIgnorePaths("age");
Example<Member> example = Example.of(member, matcher);
memberRepository.findAll(example);
List<Member> result = memberRepository.findAll(example);
assertThat(result.get(0).getUsername()).isEqualTo("m1");
}- Probe : 필드에 데이터가 있는 실제 도메인 객체
- ExampleMathcer: 특정 필드를 일치시키는 상세한 정보 제공, 재사용 가능
- Example: Probe와 ExampleMatcher로 구성, 쿼리를 생성하는데 사용
장점
- 동적 쿼리를 편리하게 처리
- 도메인 객체를 그대로 사용
- 데이터 저장소를 RDB에서 NOSQL로 변경해도 코드 변경이 없게 추상화 되어 있음
- 스프링 데이터 JPA
JpaRepository인터페이스에 이미 포함
단점
- 조인은 가능하지만 내부 조인(INNER JOIN)만 가능함 외부 조인(LEFT JOIN) 안됨
-
다음과 같은 중첩 제약조건 안됨
firstname = ?0 or (fristername = ?! and lastname = ?@)
-
매칭 조건이 매우 단순함
- 문자는
starts/contains/ends/regex - 다른 속성은 정확한 매칭(
=)만 지원
- 문자는
정리
- 실무에서 사용하기에는 매칭 조건이 너무 단순하고, LEFT 조인이 안됨
- 실무에서는 QueryDSL을 사용하자
Projections
엔티티 대신에 DTO를 편리하게 조회할 때 사용하는 기능으로 전체 엔티티가 아니라 특정 필드만 조회하고 싶을 때 사용합니다. 편하게 select를 통해 찝어서 가져온다 생각하면 됩니다.
인터페이스 기반 Proejction
public interface UsernameOnly {
String getUsername();
}- 조회할 엔티티의 필드를 getter 형식으로 지정하면 해당 필드만 선택해서 조회함(Projection)
// Return Type으로 해당 DTO 지정
List<UsernameOnly> findProjectionsByUsername(@Param("username") String username);- 메서드 이름은 자유, 반환 타입으로 인지
ClosedProjection 테스트
@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");
for (UsernameOnly usernameOnly: result) {
System.out.println("usernameOnly = " + usernameOnly);
}
// then
}스프링의 SpEL 문법을 활용하여 쿼리를 사용할 수도 있습니다.
@Value("#{target.username + ' ' + target.age}")
String getUsername();단, 이렇게 SpEL 문법을 사용하면, DB에서 엔티티 필드를 다 조회해온 다음에 계산하기 때문에 JPQL SELECT 최적화는 되지 않습니다.
클래스 기반 Proejction
public class UsernameOnlyDto {
private final String username;
public UsernameOnlyDto(String username) {
this.username = username;
}
public String getUsername() {
return username;
}
}인터페이스가 아닌 구체적인 DTO 형식으로도 Proejction을 사용할 수 있습니다. JPA는 생성자의 파라미터 이름을 기준으로 매칭하게 됩니다.
동적 Projections
다음과 같이 Generic type을 주면 동적으로 프로젝션 데이터 변경도 가능합니다.
<T> List<T> findProjectionsByUsername(@Param("username") String username, Class<T> type);중첩 구조 처리
public interface NestedClosedProjections {
String getUsername();
TeamInfo getTeam();
interface TeamInfo {
String getName();
}
}주의사항
- 프로젝션 대상이 root 엔티티면, JPQL SELECT절로 최적화 가능
-
프로젝션 대상이 ROOT가 아니면
- LEFT OUTER JOIN 처리
- 모든 필드를 SELECT해서 엔티티로 조회한 다음에 계산
정리
- 프로젝션 대상이 root 엔티티면 유용하다.
- 프로젝션 대상이 root 엔티티를 넘어가면 JPQL SELECT 최적화가 안된다
- 실무의 복잡한 쿼리를 해결하기에는 한계가 있다.
- 실무에서는 단순할 때만 사용하고, 조금만 복잡해지면 QueryDSL을 사용하자
JPA 네이티브 SQL 지원
스프링 데이터 JPA 기반 네이티브 쿼리
- 페이징 지원
-
반환 타입
- Object[]
- Tuple
- DTO(스프링 데이터 인터페이스 Proejctions 지원)
-
제약
- Sort 파라미터를 통한 정렬이 정상 동작하지 않을 수 있음(믿지 말고 직접 처리)
- JPQL처럼 애플레키에션 로딩 시점에 문법 확인 불가
- 동적 쿼리 불가
@Query(value = "select * from member where username = ?", nativeQuery = true)
Member findByNativeQuery(String username);- JPQL은 위치 기반 파라미터를 1부터 시작하지만 SQL은 0부터 시작
-
네이티브 SQL을 엔티티가 아닌 DTO로 변환을 하려면
- DTO 대신 JPA TUPLE 조회
- DTO 대신 MAP 조회
- @SqlResultSetMapping ⇒ 복잡
- Hibernate ResultTransformer를 사용해야 함 ⇒ 복잡
- 네이티브 SQL을 DTO로 조회할 때는 JdbcTemplate or mybatis 권장
Projctions 활용
예) 스프링 데이터 JPA 네이티브 쿼리 + 인터페이스 기반 Proejctions 활용
@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);동적 네이티브 쿼리
- 하이버네이트를 직접 활용
- 스프링 JdbcTemplate, myBatis, jooq같은 외부 라이브러리 사용
예) 하이버네이트 기능 사용
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();
}이 링크를 통해 구매하시면 제가 수익을 받을 수 있어요. 🤗