wms's Programming&Study

JPA Querydsl from절에 subquery 사용하기(@Subselect 이용) 본문

Programming/JAVA

JPA Querydsl from절에 subquery 사용하기(@Subselect 이용)

wms2275 2023. 1. 14. 13:48

이슈사항

이번 내용도 배치로 통계로 구축하다가 생긴 이슈이다.
통계 쿼리를 mybatis로 작성하는 것은 쉬운데 검색해보니
querydsl은 subquery가 select 절이나 where 절에만 사용가능하고
from절에는 불가하다는 내용이 가득해서 통계 쿼리를 작성하는데 골머리 썩었다.
그래도 방법을 찾아서 적어보려고 한다.

해결방안

@Subselect를 이용해서 서브 쿼리로 사용할 엔티티에 일종의 가상 테이블을 만드는 view 처럼 사용하는 것이다.
그렇게 되면 native query로 작성하는 것처럼 사용이 가능하다.
아니면 어플리케이션 레벨에서 처리해야한다.

해결

from 절 조회해오는 데이터의 크기를 줄이는 것이 목적이어서 @Subselect를 사용해서 view처럼 사용하기로 했다.

subquery 엔티티 부분

엔티티 부분을 작성할 때 주의할 점은 select절의 컬럼 이름과 엔티티의 @Column(name = "")
이 부분이 같아야 컬럼을 찾을 수 있으니 주의해야한다.
그리고 이부분의 단점은 where 절에 넣을 수 있는 조건이 동적으로 불가해서 고정적으로 

@Entity
@Subselect(
    "SELECT " +
        "id AS member_id, " +
        "TIMESTAMPDIFF(YEAR, birth, CURDATE()) AS age," +
        "gender," +
        "STR_TO_DATE(created_at, '%Y-%m-%d') AS created_at, " +
        "member_status "
    "FROM member " +
    "WHERE birth IS NOT NULL " +
    "AND gender IS NOT NULL"
)
@Immutable
@Synchronize("member")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class AgeGenderSub {

  @Column(name = "member_id")
  private String memberId;

  @Column(name = "age")
  private Long age;

  @Column(name = "gender")
  private String gender;

  @Column(name = "created_at")
  private LocalDateTime createdAt;

  @Column
  @Enumerated(EnumType.STRING)
  private MemberStatus memberStatus;

  public AgeGenderSub(String memberId, Long age, String gender, LocalDateTime createdAt, MemberStatus memberStatus) {
    this.memberId = memberId;
    this.age = age;
    this.gender = gender;
    this.createdAt = createdAt;
    this.memberStatus = memberStatus;
  }
}


repository querydsl 부분

아까 생성한 엔티티를 Q테이블로 선언해주고 from 절에 엔티티 부분을 넣어주면 @Subselect 부분에서 조회된 데이터를 사용할 수 있다.
그리고 엔티티에 선언되어 있는 필드를 사용해서 querydsl을 작성할 수 있다.

@Override
public List<AgeGenderDto> findAgeGenderStatistics(
    AnalysisKey analysisKey) {
  LocalDateTime startDate = analysisKey.getStartDate().atStartOfDay();
  LocalDateTime endDate = analysisKey.getEndDate().atStartOfDay();

  QAgeGenderSub ageGenderSub = QAgeGenderSub.ageGenderSub;

  StringTemplate dateFormat = Expressions.stringTemplate("DATE_FORMAT( {0}, {1} )",
      ageGenderSub.createdAt, ConstantImpl.create("%Y-%m-%d"));
  StringPath ageType = Expressions.stringPath("age_type");
  return queryFactory
      .select(
          Projections.constructor(AgeGenderDto.class,
              dateFormat,
              new CaseBuilder()
                  .when(ageGenderSub.age.lt(10)).then(AgeType.LESS_THAN_10.toString())
                  .when(ageGenderSub.age.between(11, 19))
                  .then(AgeType.BETWEEN_11_AND_19.toString())
                  .when(ageGenderSub.age.between(20, 29))
                  .then(AgeType.BETWEEN_20_AND_29.toString())
                  .when(ageGenderSub.age.between(30, 39))
                  .then(AgeType.BETWEEN_30_AND_39.toString())
                  .when(ageGenderSub.age.between(40, 49))
                  .then(AgeType.BETWEEN_40_AND_49.toString())
                  .when(ageGenderSub.age.between(50, 59))
                  .then(AgeType.BETWEEN_50_AND_59.toString())
                  .when(ageGenderSub.age.goe(60)).then(AgeType.GREATER_THAN_60.toString())
                  .otherwise(AgeType.ETC.toString()).as("age_type"),
              ageGenderSub.gender,
              ageGenderSub.count()
          )
      )
      .from(ageGenderSub)
      .where(
          ageGenderSub.isNotNull()
              .and(ageGenderSub.createdAt.goe(startDate))
              .and(ageGenderSub.createdAt.lt(endDate))
              .and(ageGenderSub.memberStatus.eq(MemberStatus.NORMAL))
      )
      .groupBy(dateFormat, ageType, ageGenderSub.gender)
      .fetch();
}