일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- Spring Data JPA
- 불변 객체
- 스프링MVC
- Bean Validation
- JPA
- jpa 활용
- 트위터
- JPA 활용2
- JPA 활용 2
- 벌크 연산
- JPQL
- 스프링
- 일론머스크
- 스프링 데이터 JPA
- API 개발 고급
- 타임리프 문법
- 로그인
- QueryDSL
- 검증 애노테이션
- 예제 도메인 모델
- 타임리프
- 임베디드 타입
- 컬렉션 조회 최적화
- 김영한
- 기본문법
- 프로젝트 환경설정
- 값 타입 컬렉션
- 페이징
- 실무활용
- 스프링 mvc
- Today
- Total
RE-Heat 개발자 일지
[Spring Data JPA] [7] 나머지 기능들 - Projections, 네이티브 쿼리 등 본문
인프런 김영한 님의 강의를 듣고 작성한 글입니다.
[1] Specifications (명세)
JPA Criteria는 실무에선 안 쓰인다.
술어(predicate)
- 참 또는 거짓으로 평가
- AND OR 같은 연산자로 조합해 다양한 검색조건을 쉽게 생성함(컴포지트 패턴으로)
- ex) 검색 조건 하나하나
- 스프링 데이터 JPA는 org.springframework.data.jpa.domain.Specification 클래스로 정의
JpaSpecificationExecutor
public interface JpaSpecificationExecutor<T> {
Optional<T> findOne(@Nullable Specification<T> spec);
List<T> findAll(@Nullable Specification<T> spec);
...
}
MemberRepository
public interface MemberRepository extends JpaRepository<Member, Long>, JpaSpecificationExecutor<Member> {
}
- memberRepository가 JpaSpecificationExecutor 상속
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);
}
}
JPA Criteria의 Root, CriteriaQuery, CriteriaBuilder 클래스를 파라미터로 받아 조건으로 사용할 메서드 구현
결론 : 조금만 복잡해도 거의 읽기가 힘들어 사실상 안 쓰인다. Querydsl을 쓰자.
[2] Query By Example
조건이 여러 개이고 동적 쿼리를 짤 때 사용.
QueryByExampleExecutor
@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로 구성, 쿼리를 생성하는 데 사용
정리
조인은 가능하지만, 내부 조인만 가능하다(외부 조인 X)
또 매칭 조건이 너무 단순해 실무에선 안 쓰인다.
[3] Projections
전체 엔티티가 아니라 회원 이름만 조회하고 싶다면?
■ 인터페이스 기반 Projections
UsernameOnly
public interface UsernameOnly {
String getUsername();
}
조회할 엔티티 필드를 getter 형식으로 지정하면 해당 필드만 선택해서 조회한다.
MemberRepository
public interface MemberRepository ... {
List<UsernameOnly> findProjectionsByUsername(String username);
}
반환 타입으로 UsernameOnly를 넣어 줌.
Test 코드
@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
for (UsernameOnly usernameOnly : result) {
System.out.println("usernameOnly = " + usernameOnly);
}
}
쿼리문
딱 username만 가져오는 걸 알 수 있다.
① 인터페이스 기반 Closed Projections
public interface UsernameOnly {
String getUsername();
}
프로퍼티 형식의 인터페이스를 제공하면 구현체는 스프링 데이터 JPA가 제공한다.
② 인터페이스 기반 Open Projections
public interface UsernameOnly {
@Value("#{target.username + ' ' + target.age + ' ' + target.team.name}")
String getUsername();
}
SpringEL문법도 지원하나 이렇게 하면 DB에서 엔티티 필드를 다 조회해 온 다음에 계산한다.
■ 클래스 기반 projections
UsernameOnlyDto
public class UsernameOnlyDto {
private final String username;
public UsernameOnlyDto(String username) {
this.username = username;
}
public String getUsername() {
return username;
}
}
구체적인 DTO 형식으로도 projections을 쓸 수 있다.
MemberRepository
//Projections
List<NestedClosedProjection> findProjectionsByUsername(@Param("username") String username);
테스트 코드
이전과 동일 테스트를 진행해도 같은 결과 값이 나온다. 단, 구체적인 class를 명시해 줬으므로 proxy가 필요 없다.
■ 동적 Projections
<T> List<T> findProjectionsByUsername(String username, Class<T> type);
GenericType을 주면 동적으로 Projections 타입을 변경할 수 있다.
List<UsernameOnly> result = memberRepository.findProjectionsByUsername("m1", UsernameOnly.class);
이런 식으로 사용하면 된다.
■ 중첩구조 처리
NestedClosedProjection
public interface NestedClosedProjection {
String getUsername();
TeamInfo getTeam();
interface TeamInfo {
String getName();
}
}
테스트 코드 및 발생쿼리
- 프로젝션 대상이 root 엔티티면 JPQL SELECT 절 최적화 가능
- 프로젝션 대상이 root가 아니면
- LEFT OUTER JOIN 처리
- 모든 필드를 SELECT 해서 엔티티로 조회한 다음에 계산
결론 : 실무에선 단순할 때만 사용하고 복잡해지면 Querydsl을 쓰자.
[4] 네이티브 쿼리
가급적 네이티브 쿼리는 사용하지 않는 게 좋다.
MemberRepository
@Query(value = "select * from member where user = ?", nativeQuery = true)
Member findByNativeQuery(String username);
테스트 코드 및 SQL문
작성한 native 쿼리가 그대로 나오게 된다. 하지만, 단점이 많은 방식이므로 별도의 리포지토리를 만들거나 mybatis, jdbc template을 쓰는 게 낫다.
■ 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);
- Page도 사용 가능
테스트 코드
@Test
public void nativeQuery() 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();
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 = " + memberProjection.getUsername());
System.out.println("memberProjection = " + memberProjection.getTeamName());
}
//then
}
정적쿼리를 Projections를 활용하면 좀 더 수월하게 쓸 수 있다. 페이징 처리도 되는 게 장점이다. 그러나 동적 쿼리는 해결이 안 된다.
■ 동적 Native 쿼리
방법
- 하이버네이트 직접 활용
- 스프링 JdbcTemplate, myBatis, jooq 같은 외부 라이브러리 사용
예시] 하이버네이트 기능 사용
//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();
}
'백엔드 > 스프링 데이터 JPA' 카테고리의 다른 글
[Spring Data JPA] [6] 스프링 데이터 JPA 분석 (0) | 2023.09.13 |
---|---|
[Spring Data JPA] [5] 확장 기능 (0) | 2023.09.10 |
[Spring Data JPA] [4] 쿼리 메소드 기능(하편) - 페이징·벌크연산·EntityGraph (0) | 2023.09.09 |
[Spring Data JPA] [4] 쿼리 메소드 기능(상편) (0) | 2023.09.08 |
[Spring Data JPA] [3] 공통 인터페이스 기능 (0) | 2023.09.08 |