JPQL, QueryDSL, Spring Data JPA2

Seung jun Cha·2022년 8월 22일
0

1. JPQL

1-1 검색

  • 테이블이 아닌 엔티티 객체를 대상으로 검색
    SQL을 추상화해서 특정 데이터베이스 SQL에 의존X
    em.createQuery("select m from Member m where m.age > 18", Member.class)
    => 단점: 문자라서 컴파일 오류가 발생하지 않음
    =>TypeQuery<>: 반환 타입이 명확할 때 사용, Query: 반환 타입이 명확하지 않을 때 사용

  • 파라미터 바인딩
    SELECT m FROM Member m where m.username=:username

1-2 페이징 처리

  • setFirstResult(0) : 페이지의 번호가 아니라, 조회를 시작할 row의 위치이다(0부터 시작)
    setMaxResult(10) : 조회할 데이터 개수 지정 .getResultList();
//페이징 쿼리
 String jpql = "select m from Member m order by m.name desc";
 List<Member> resultList = em.createQuery(jpql, Member.class)
 .setFirstResult(10)
 .setMaxResults(20)
 .getResultList();

1-3 조인

  1. 내부 조인
    SELECT m FROM Member m [INNER] JOIN m.team t
  2. 외부 조인: 여러 테이블 중 한쪽에는 데이터가 있고, 다른 쪽은 데이터가 없는 경우
    SELECT m FROM Member m LEFT(RIGHT) [OUTER] JOIN m.team t
  3. 세타 조인:
    select count(m) from Member m, Team t where m.username
    = t.name

1-3-1 조인 on절

  • 조인 대상 필터링
예) 회원과 팀을 조인하면서, 팀 이름이 A인 팀만 조인
JPQL:
SELECT m, t FROM Member m LEFT JOIN m.team t on t.name = 'A'
  • 연관관계 없는 엔티티 외부 조인 -> 결국 이것도 필터링인듯
예) 회원의 이름과 팀의 이름이 같은 대상 외부 조인
JPQL:
SELECT m, t FROM
Member m LEFT JOIN Team t on m.username = t.name

1-3-2 서브쿼리

  • JPA는 select, where, having절에서만 서브쿼리 사용가능

  • [NOT] EXISTS : 서브쿼리에 결과가 존재하면 참
    ALL 모두 만족하면 참
    ANY, SOME: 같은 의미, 조건을 하나라도 만족하면 참
    [NOT] IN (subquery): 서브쿼리의 결과 중 하나라도 같은 것이 있으면 참

• 이가 평균보다 많은 회원
select m from Member m
where m.age > (select avg(m2.age) from Member m2) 

• 한 건이라도 주문한 고객
select m from Member m
where (select count(o) from Order o where m = o.member) > 0 

• A 소속인 회원
select m from Member m
where exists (select t from m.team t where t.name = ‘팀A') 

• 전체 상품 각각의 재고보다 주문량이 많은 주문들
select o from Order o 
where o.orderAmount > ALL (select p.stockAmount from Product p) 

• 어떤 팀이든 팀에 소속된 회원
select m from Member m 
where m.team = ANY (select t from Team t)

2. QueryDSL

  • 검색이나 조회조건이 복잡하지 않다면 @Query를 사용해도 됨
  • QueryDSL을 사용하려면 QDomain을 생성해야한다.
// queryDSL 설정
    implementation "com.querydsl:querydsl-jpa"
    implementation "com.querydsl:querydsl-core"
    implementation "com.querydsl:querydsl-collections"
    annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jpa" // querydsl JPAAnnotationProcessor 사용 지정
    annotationProcessor "jakarta.annotation:jakarta.annotation-api" // java.lang.NoClassDefFoundError (javax.annotation.Generated) 대응 코드
    annotationProcessor "jakarta.persistence:jakarta.persistence-api" // java.lang.NoClassDefFoundError (javax.annotation.Entity) 대응 코드
}

// Querydsl 설정부
def generated = 'src/main/generated'

// querydsl QClass 파일 생성 위치를 지정
tasks.withType(JavaCompile) {
    options.getGeneratedSourceOutputDirectory().set(file(generated))
}

// java source set 에 querydsl QClass 위치 추가
sourceSets {
    main.java.srcDirs += [ generated ]
}

// gradle clean 시에 QClass 디렉토리 삭제
clean {
    delete file(generated)
}

2-1 검색

  • JPQL 빌더 역할을 하며, 문자가 아닌 자바코드로 JPQL을 작성할 수 있음
  1. 동적쿼리 작성 편리함
  2. 파라미터 바인딩 자동으로 처리됨
  3. 컴파일 시점 오류 발견
  4. 동시성 문제없음
    ※JPAQueryFactory 등록 (bean으로 등록)
    @Bean
    JPAQueryFactory jpaqueryfactory(EntityManager em) {
    Return new JPAQueryFactory(em);
    
    //JPQL 
    //select m from Member m where m.age > 18
    
    JPAFactoryQuery query = new JPAQueryFactory(em);
    QMember m = QMember.member; 
    List<Member> list = 
    query.selectFrom(m)
    .where(m.age.gt(18)) 
    .orderBy(m.name.desc())
    .fetch();

2-2 페이징 처리

  • offsetlimit를 통해서 페이징을 할 수 있다. 중요한 것이 offset은 시작 페이지를 정하는 것이 아니라 시작 row를 정하는 것이다
    (몇 번째 row에서 데이터 조회를 시작할 지 정하는 것)
    ex) 페이지의 사이즈가 5이고, 0번째 페이지를 조회하고 싶다면 offset은 0, 1번째 페이지를 조회하고 싶다면 5, 2번째 페이지를 조회하고 싶다면 10, 이런 식으로 반환되어야 하는 것이다.

3. 스프링 데이터 JPA

3-1 페이징 처리

  • Pageable 인터페이스는 PageRequest.of(page, size, sort)로 구현해서 넘겨주면, 따로 코드 구현 없이 인수를 넘겨주는 것만으로도 페이징 혹은 정렬이 된다!
    =>결과를 DTO로 변환해서 반환할 것
    ex) return memberRepository.findAll(pageable).map(MemberDto::new);

  • Pageable의 반환 타입 : Sort에 페이징 기능까지 추가된 것이다.
    ①Page<> : 추가 count 쿼리 발생, 현재 페이지에 대한 정보와, 전체 데이터의 개수 조회
    ②Slice<> : 추가 count 쿼리 없이 다음 페이지만 확인 가능(내부적으로 limit +1을 조회하여 다음 페이지가 있는지 없는지만 확인)

결론은 Spring Data JPA + QueryDSL로 페이징 처리하면 됨

3-1-2 Spring Data JPA + QueryDSL

  • Spring Data JPA와 QueryDSL를 함께 사용하기 위해서는 사용자 정의 Repository를 정의해야한다.

  • QueryDSL을 사용하여 검색, 페이징 처리 등의 결과 값을 받을 때, 객체가 아닌 DTO로 받으려면 Dto생성자에 @QueryProjection을 사용
    @QueryProjection를 사용하면 객체로 값을 받은 후 Dto클래스로 변환하는 과정 없이 QDto를 생성해서 바로 Dto객체를 뽑을 수 있음

  1. 사용자정의 Custom 인터페이스를 만들고 필요한 메서드를 추상메서드로 입력
public interface ItemRepositoryCustom {
    
    Page<Item> getAdminItemPage(ItemSearchDto itemSearchDto, 
    Pageable pageable);
    
    /* 
    상품 검색키워드을 담고 있는 ItemSearchDto 객체와 페이징 정보를 담고있는 
    Pageable 객체를 파라미터로 받는 메서드를 정의
    */
  1. Impl을 클래스를 만들고 Custom인터페이스를 implement받아서 구현한다
public class ItemRepositoryCustomImpl implements ItemRepositoryCustom{

    private JPAQueryFactory queryFactory; //동적으로 쿼리를 생성하기 위해 필요



    public ItemRepositoryCustomImpl(EntityManager em) {
        this.queryFactory = new JPAQueryFactory(em);  //생성자로 EntityManager를 넣어줌줌
   }

   private BooleanExpression searchSellStatusEq(ItemSellStatus searchSellStatus){
        return searchSellStatus == null ? null : item.itemSellStatus.eq(searchSellStatus);
        //조건이 null이라서 null이 반환되면 해당 검색조건은 무시된다.
   }

   private BooleanExpression regDtsAfter(String searchDateType){
       LocalDateTime dateTime = LocalDateTime.now();

       if (StringUtils.equals("all", searchDateType) || searchDateType == null) {
           return null;
       }else if (StringUtils.equals("1d", searchDateType)){
           dateTime = dateTime.minusDays(1);  // 시간을 현재부터 1일전으로 세팅
       }else if (StringUtils.equals("1w", searchDateType)){
           dateTime = dateTime.minusWeeks(1);
       }else if (StringUtils.equals("1m", searchDateType)){
           dateTime = dateTime.minusMonths(1);
       }else if (StringUtils.equals("6m", searchDateType)){
           dateTime = dateTime.minusMonths(6);
       }
       return item.regTime.after(LocalDate.from(dateTime));  // 해당 시간 이후로 등록된 상품
   }

   private BooleanExpression searchByLike(String searchBy, String searchQuery){ //검색조건, 검색어
        if (StringUtils.equals("itemNm", searchBy)){
            return item.itemNm.like("%" + searchQuery + "%");
        } else if (StringUtils.equals("createdBy" , searchBy)) {
            return item.createdBy.like("%" + searchQuery + "%");
        }
        return null;
   }

    @Override
    public Page<Item> getAdminItemPage(ItemSearchDto itemSearchDto, Pageable pageable) {
        List<Item> content = queryFactory.selectFrom(item)
                .where(searchSellStatusEq(itemSearchDto.getItemSellStatus()))
                .where(regDtsAfter(itemSearchDto.getSearchDateType()))
                .where(searchByLike(itemSearchDto.getSearchBy(), itemSearchDto.getSearchQuery()))
                .orderBy(item.id.desc())
                .offset(pageable.getOffset())  // 데이터를 가지고 올 시작 인덱스를 지정
                .limit(pageable.getOffset())   // 한 번에 가지고 올 최대 개수 지정
                .fetch();

        Long total = queryFactory.select(Wildcard.count).from(item)
                .where(regDtsAfter(itemSearchDto.getSearchDateType()),
                        searchSellStatusEq(itemSearchDto.getItemSellStatus()),
                        searchByLike(itemSearchDto.getSearchBy(), itemSearchDto.getSearchQuery()))
                .fetchOne();

        return new PageImpl<>(content, pageable, total);
    }
}
  1. Custom인터페이스를 사용자정의 Repository가 extend한다.
    -QueryDSL을 사용해서 동적쿼리 등을 처리할 때 주로 구현

  2. Service에서 메서드 활용

  3. controller에서 PageRequest.of로 Pageable을 생성

4. 페치조인(Fetch join)

  • JPQL에서 성능 최적화를 위해 제공하는 기능으로 연관된 엔티티나 컬렉션을 SQL 한 번에 함께 조회하는 기능(distinct 사용가능)
    QueryDsl에서 페치조인 : join().fetchjoin()
    JPQL : join.fetch(@Query로 작성)

  • 페치조인의 특징과 한계
    ①엔티티에 직접 적용하는 글로벌 로딩 전략보다 우선함
    ②페치 조인 대상에는 별칭을 줄 수 없다.
    ③둘 이상의 컬렉션은 페치 조인 할 수 없다.
    ④컬렉션을 페치 조인하면 페이징 API(setFirstResult, setMaxResults)를 사용할 수 없다.
    (하이버네이트는 경고 로그를 남기고 메모리에서 페이징(매우 위험))
    ⑤여러 테이블을 조인해서 엔티티가 가진 모양이 아닌 전혀 다른 결과를 내야 하면, 페치 조인 보다는 일반 조인을 사용하고 필요 한 데이터들만 조회해서 DTO로 반환하는 것이 효과적

0개의 댓글