Querydsl 사용시 Entity가 아닌 DTO로 받는 방법을 간단하게 정리해보고자 한다.
API
- 작성하고자 하는 API를 설명해보자면 가게마다 운영시간 정보가 있다. 가게 이름, 가게 쉬는 시간 리스트, 가게 시작시간, 가게 종료 시간, 예약 다시 받는 시간 간격(예를 들어, 한시간 뒤에 해당 테이블을 다시 받겠다.)
- 여기서 가게 쉬는 시간은 여러 시간이 존재할 수 있으므로 다른 테이블을 통해 조인 관계로 설정을 해두었다.
- 이제 API에서 클라이언트가 받고싶은 데이터는 해당 데이터 전체이다. 따라서 Entity보단 입력 받은 데이터를 그대로 전달해줄 수 있는 DTO를 통해 데이터를 전달하는게 더 효율적이다.
StoreTimeInfoDTO result = queryFactory
.select(Projections.fields(StoreTimeInfoDTO.class,
storeTimeInfoEntity.startTime,
storeTimeInfoEntity.endTime,
storeTimeInfoEntity.intervalTime,
Expressions.asString(storeName).as("storeName")
))
.from(storeTimeInfoEntity)
.where(storeTimeInfoEntity.storeName.eq(storeName))
.fetchOne();
위와 같이 작성을 했는데 그 속에서 우선적으로 DTO를 받기위해 필요한 정보를 정리해보고자 한다.
기본적으로, Querydsl의 Projections는 DB의 데이터 즉, Entity를 DTO로 만들어주기 위해 여러가지 메서드를 제공한다.
대표적으로는 두가지가 있다.
- constructor : 우선적으로 DTO안에 생성자를 통해 Entity를 DTO로 변경을 한다. 기본적으로는 이름이 같음을 요한다.
- fields : 필드명으로 매핑을 실행한다. 역시나 기본적으로 필드간 이름이 같아야 매핑을 해준다.
만약, 필드명이 동일하지 않다면 Expressions의 메서드를 사용할 수 있다.
예를들어,
Entity.entityfield.time.as("time") : DTO 안에 time이라는 필드를 매핑한다는 의미로 as 메서드를 사용했다.
또한, 이미 알고있는 데이터에 대해서 대입 또한 가능하다.
위의 예시처럼,Expressions.asString(storeName).as("storeName") 작성시 이미알고 있는 storeName변수를 그대로 대입해주는 의미이다.
사실, 이 글을 Trouble Shooting에 적은 이유는 아래와 같다.
시도 1
아래 코드가 전체 메서드 코드인데, 두번의 쿼리문을 통해 DTO를 완성시킨다.
항상 쿼리 수가 많아지면 별로 좋지않음을 인지하고 있다.
해결하기 위해 Expressions.list를 통해 한번에 받아오고자 했지만 결과적으로 오류가 많이 나고 또한 JPAExpressions로 쿼리문을 한번 더 날리기에 쿼리문 양의 변화는 없어서 그냥 아래와 같이 작성을 했다.
하지만 좀 계속 더 자료를 찾아보고 더 간단하게 만들 수 있는 방법이 있다면 수정이 필요할 것 같다.
@Override
public StoreTimeInfoDTO getTimeInfo(String storeName) {
List<String> resultList =
queryFactory.selectDistinct(storeTimeInfoMapEntity.time).from(storeTimeInfoMapEntity)
.leftJoin(storeTimeInfoMapEntity.storeTimeInfoEntity, storeTimeInfoEntity)
.where(storeTimeInfoEntity.storeName.eq(storeName)).fetch();
StoreTimeInfoDTO result = queryFactory
.select(Projections.fields(StoreTimeInfoDTO.class,
storeTimeInfoEntity.startTime,
storeTimeInfoEntity.endTime,
storeTimeInfoEntity.intervalTime,
Expressions.asString(storeName).as("storeName")
))
.from(storeTimeInfoEntity)
.where(storeTimeInfoEntity.storeName.eq(storeName))
.fetchOne();
result.setBreakTime(resultList.stream().sorted().toList());
return result;
}
시도 2
한 쿼리문안에 끝내기 위해서 위의 코드에서 두 쿼리문을 left join으로 합쳤다. 하지만 List와 String 호환에 문제가 생긴다. 즉, 아래 코드로는 breakTime의 모든 데이터를 List로 가져오지 못하는 것이다. 따라서 아래 코드에서 breakTime은 여러개의 데이터지만 데이터를 모두 가져오지 못해서 String만 생성되고 List<String> 과 호환이 되지 않는 것이다.
따라서 결론적으로는 시도1 코드를 사용중에 있다. 이런식으로 자식테이블 또는 부모테이블을 한번에 가져와 DTO를 만들기 위해서는 한개의 부모 또는 자식 테이블의 결과가 매칭되어야 한다.
StoreTimeInfoDTO result = queryFactory
.select(Projections.fields(StoreTimeInfoDTO.class,
storeTimeInfoEntity.startTime,
storeTimeInfoEntity.endTime,
storeTimeInfoEntity.intervalTime,
storeTimeInfoMapEntity.time.as("breakTime"),
Expressions.asString(storeName).as("storeName")
))
.from(storeTimeInfoEntity)
.leftJoin(storeTimeInfoEntity.breakTime, storeTimeInfoMapEntity)
.where(storeTimeInfoEntity.storeName.eq(storeName))
.fetchOne();
'Trouble Shooting > Data API' 카테고리의 다른 글
QueryDSL - 3중 Join N+1 해결 (0) | 2023.06.27 |
---|---|
Querydsl 적용과 고찰 (0) | 2023.06.05 |
SELECT + JOIN 개선에 대한 고찰 (0) | 2023.05.15 |