나머지 기능들

LeeKyoungChang·2022년 4월 28일
0
post-thumbnail

실전! 스프링 데이터 JPA 수업을 듣고 정리한 내용입니다.

 

Specifications와 Query By Example은 실무에서 거의 사용하지 않는다.
그러므로, 정리하지 않고 넘어가기

실무에서는 JPA Criteria를 거의 쓰지 않는다. (진짜 복잡한 코드를 사용한다.) 대신에 QueryDSL을 사용하자!

Query By를 실무에서 사용하기에는 매칭 조건이 너무 단순하고, LEFT 조인이 되지 않는다. 실무에서는 QueryDSL을 사용하자!

 

📚 1. Projections

Projections

엔티티 대신에 DTO를 편리하게 조회할 때 사용한다.

ex) 전체 엔티티가 아니라 만약 회원 이름만 조회하고 싶을 때

📖 A. 단일 구조 처리

✔️ 인터페이스 기반 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);  
  
}

 

실행 결과
스크린샷 2022-04-28 오후 4 59 41

  • SQL에서도 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();
}
  • 단, 이렇게 SpEL 문법을 사용하면, DB에서 엔티티 필드를 다 조회해온 다음에 계산한다.
  • 따라서, JPQL SELECT 절 최적화가 안된다.

 

테스트 실행 결과

스크린샷 2022-04-28 오후 5 04 08

 

✔️ 클래스 기반 Projection

  • 인터페이스가 아닌 구체적인 DTO 형식도 가능하다.
  • 생성자의 파라미터 이름으로 매칭한다.

 

UsernameOnlyDto클래스 생성

public class UsernameOnlyDto {

    private final String username;

    public UsernameOnlyDto(String username) {
        this.username = username;
    }

    public String getUsername() {
        return username;
    }
}
  • 인터페이스가 아닌 구체적인 DTO 형식도 가능하다.
  • 생성자의 파라미터 이름으로 매칭한다.

 

실행 결과

스크린샷 2022-04-28 오후 5 06 50

 

✔️ 동적 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());  
    }  
  
}

 

실행 결과
스크린샷 2022-04-28 오후 5 50 51

 

📖 B. 중접 구조 처리

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);  
    }  
  
}

 

실행 결과
스크린샷 2022-04-28 오후 5 56 02

  • memberusername만, team은 모두 조회되었다.
  • 첫 번째는 최적화가 되지만, 두 번째부터는 최적화가 되지 않는다.

 

⚠️ 주의

  • 프로젝션 대상이 root 엔티티면, JPQL SELECT 절 최적화가 가능하다.
  • 프로젝션 대상이 root가 아니라면
    • LEFT OUTER JOIN으로 처리한다.
    • 모든 필드를 SELECT해서 엔티티로 조회한 다음에 계산한다.

 

📌 정리

  • 프로젝션 대상이 root 엔티티면 projection이 유용하다.
  • 프로젝션 대상이 root 엔티티를 넘어가면 JPQL SELECT 최적화가 안된다!
    • 실무의 복잡한 쿼리를 해결하기에는 한계가 있다.
  • 실무에서는 단순할 때만 사용하고, 조금만 복잡해지면 QueryDSL을 사용하자!

 

📚 2. 네이티브 쿼리

가급적 네이티브 쿼리는 사용하지 않는게 좋다.
최근에 Projections 활용이 나왔는데, 이 기능을 사용하자!

📖 A. 스프링 데이터 JPA 기반 네이티브 쿼리

  • 페이징 지원
  • 반환 타입
    • Object[]
    • Tuple
    • DTO(스프링 데이터 인터페이스 Projections 지원)
  • 제약
    • Sort 파라미터를 통한 정렬이 정상 동작하지 않을 수 있다. (믿지 말고 직접 처리)
    • JPQL처럼 애플리케이션 로딩 시점에 문법을 확인할 수 없다.
    • 동적 쿼리 불가

 

✔️ 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);  
  
}

 

실행 결과

스크린샷 2022-04-28 오후 6 42 03
  • 네이티브 SQL은 위치 기반 파라미터를 0 부터 시작한다. (JPQL은 위치 기반 파라미터를 1부터 시작한다.)
  • 네이티브 SQL을 엔티티가 아닌 DTO로 변환하려면
    • DTO 대신 JPA TUPLE 조회
    • DTO 대신 MAP 조회
    • @SqlResultSetMapping → 복잡
    • Hibernate ResultTransformer를 사용해야 한다. → 복잡
    • Hibernate 참고자료
    • 네이티브 SQL을 DTO로 조회할 때는 JdbcTemplate or myBatis 권장한다.

 

📖 B. Projections 활용

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());  
    }  
  
}

 

실행 결과

스크린샷 2022-04-28 오후 7 01 57 스크린샷 2022-04-28 오후 7 02 07
  • 2년 전에 나온 기술
  • Page 할 수 있어 많이 사용될 것 같다.

 

✔️ 동적 네이티브 쿼리

  • 하이버네이티를 직접 활용
  • 동적 네이티브 쿼리 방식보다는 스프링 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();
}

 

profile
"야, (오류 만났어?) 너두 (해결) 할 수 있어"

0개의 댓글