컬렉션 조회 최적화와 동적쿼리에 대한 질문입니다
미해결
실전! Querydsl
[질문 템플릿] 1. 강의 내용과 관련된 질문인가요? ( 예 /아니오) 2. 인프런의 질문 게시판과 자주 하는 질문에 없는 내용인가요? ( 예 /아니오) 3. 질문 잘하기 메뉴얼을 읽어보셨나요? ( 예 /아니오) [질문 내용] 여기에 질문 내용을 남겨주세요. 안녕하세요 강사님! 최근 JPA2 에서 수강한 컬렉션 최적화와 QueryDSL을 통해서 주변 병원 조회 기능을 구현하던중에 궁금한것이 생겨서 질문남깁니다. 현재 @Entity @Getter @AllArgsConstructor @NoArgsConstructor(access = AccessLevel.PROTECTED) @SQLDelete(sql = "UPDATE store SET store_status = 'DEACTIVATE' WHERE store_id=?") @SQLRestriction("store_status = 'ACTIVATE'") @Inheritance(strategy = InheritanceType.JOINED) @DiscriminatorColumn // 하위 테이블의 구분 컬럼 생성 @Table(name = "store", indexes = { @Index(name = "idx_store_name", columnList = "storeName") }) public abstract class Store extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "store_id") private Long storeId; @NotNull @Size(min = 2) @Column(nullable = false) private String storeName; private String storePhone; private String thumbnailUrl; private String notice; private String websiteLink; @Column(columnDefinition = "TEXT") private String storeInfo; private String storeInfoPhoto; @Enumerated(EnumType.STRING) private BaseStatus storeStatus; @Embedded private Address address; @OneToMany(mappedBy = "store", fetch = FetchType.LAZY) private List<BusinessHour> businessHours = new ArrayList<>(); @OneToMany(mappedBy = "store", fetch = FetchType.LAZY) private List<StorePhoto> storePhotos = new ArrayList<>(); // @OneToMany(mappedBy = "store", fetch = FetchType.LAZY) // private List<Reserve> reserves = new ArrayList<>(); @OneToMany(mappedBy = "store", fetch = FetchType.LAZY) private List<Review> reviews = new ArrayList<>(); @OneToOne(mappedBy = "store", fetch = FetchType.LAZY, cascade = CascadeType.ALL, optional = false) private RegistrationInfo registrationInfo; } 현재 이런식으로 Store 엔티티가 준비되어 있는 상황입니다. 아래는 이를 상속한 Hospital 엔티티입니다. @Entity @Getter @Builder @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor @DiscriminatorValue("H") @OnDelete(action = OnDeleteAction.CASCADE) @Table(name = "hospital") public class Hospital extends Store { private String additionalServiceTag; @OneToMany(mappedBy = "hospital", fetch = FetchType.LAZY) private List<TagMapper> tags = new ArrayList<>(); } 변경전 코드 List<StoreQueryInfo> hospitalInfoList = jpaQueryFactory .select( Projections.constructor( StoreQueryInfo.class, hospital.storeId.as("storeId"), hospital.storeName.as("storeName"), hospital.thumbnailUrl.as("thumbnailUrl"), businessHour.startTime.as("startTime"), businessHour.endTime.as("endTime"), businessHour.breakStartTime.as("breakStartTime"), businessHour.breakEndTime.as("breakEndTime"), review.reviewId.count().as("reviewCount"), review.rating.avg().as("ratingAvg"), Expressions.stringTemplate( "ST_Distance_Sphere(ST_PointFromText({0}, 4326), {1})", point, hospital.address.point ).castToNum(Double.class).as("distance") ) ).from(hospital) .leftJoin(hospital.businessHours, businessHour) .on(businessHour.dayOfWeek.eq(dayOfWeek)) .leftJoin(hospital.reviews, review) .leftJoin(hospital.tags, tagMapper) .leftJoin(tagMapper.hospitalTag, hospitalTag) .where( inDistance(point, queryCond.radius()), businessHourEq(queryCond.businessHourCond()), specialitiesEq(queryCond.specialitiesCond()), emergencyEq(queryCond.emergencyCond()), isOpen(queryCond.openCond(), Time.valueOf(now.minusHours(4))) ) .groupBy( hospital.storeId, hospital.storeName, businessHour.startTime, businessHour.endTime, businessHour.breakStartTime, businessHour.breakEndTime ) .orderBy(Expressions.stringTemplate( "ST_Distance_Sphere(ST_PointFromText({0}, 4326), {1})", point, hospital.address.point ).asc()) .fetch(); 변경 후 코드 (변경전의 동적쿼리 미적용) public List<StoreQueryTotalInfo> findHospitalOptimization( Pageable pageable, int dayOfWeek, String point, LocalTime now, HospitalQueryCond queryCond) { NumberPath<Double> distanceAlias = Expressions.numberPath(Double.class, "distance"); // 일단 반경 내의 병원 정보를 모두 가져옴. List<Tuple> hospitals = jpaQueryFactory .select( hospital, Expressions.stringTemplate( "ST_Distance_Sphere(ST_PointFromText({0}, 4326), {1})", point, hospital.address.point ).castToNum(Double.class).as(distanceAlias) ).from(hospital) .leftJoin(hospital.registrationInfo, registrationInfo).fetchJoin() .where(inDistance(point, queryCond.radius())) .fetch(); // // 병원 돌면서 DTO 채우기 List<TestDTO> list = new ArrayList<>(); for (Tuple tuple : hospitals) { Hospital hospital1 = tuple.get(hospital); Double distance = tuple.get(distanceAlias); log.info("Hospital: " + hospital1 + ", Distance: " + distance); list.add(TestDTO.builder() .storeId(hospital1.getStoreId()) .storeName(hospital1.getStoreName()) .thumbnailUrl(hospital1.getThumbnailUrl()) .time(Times.of(hospital1.getBusinessHours(), dayOfWeek)) .reviewCount((long) hospital1.getReviews().size()) .ratingAvg(Review.getRatingAvg(hospital1.getReviews())) .distance(formatDistance(distance)) .tags(TagInfo.from(hospital1.getTags())) .build()); } return null; } 강의를 듣기 전에는 @OneToMany 관계까지 모두 leftJoin()을 이용해서 데이터를 가져왔는데 쿼리가 무진장 많이 나가는 상황이 발생하더라고요. 그래서 컬렉션 쿼리 최적화 수업을 들은 후 위의 코드로 변경하였습니다. (@ToOne 관계만 fetchJoin 하기, @OneToMany 관계는 가져와진 객체에 직접 접근해서 가져오는 방식으로 진행했습니다.) 근데 이때 동적쿼리를 어떤식으로 적용해야하는지 감이 잡히지 않더라고요..! 일단 원하는 반경 내의 병원을 모두 조회해서 가져오기 repository단에서 반복문을 돌면서 queryCond의 null값을 체크하며 수동으로 동적쿼리를 적용해야 하기 (코드단에서 동적쿼리 적용) 위의 방식을 생각하고 있는데 이게 과연 동적 쿼리(?)가 맞는지 의구심이 들더라고요. 적용해야 하는 동적쿼리는 아래 코드와 같습니다. private BooleanExpression businessHourEq(String businessHourCond) { return businessHourCond != null ? hospitalTag.tagType.eq(HospitalTagType.BUSINESSHOUR).and(hospitalTag.tagContent.eq(businessHourCond)) : null; } private BooleanExpression specialitiesEq(String specialitiesCond) { return specialitiesCond != null ? hospitalTag.tagType.eq(HospitalTagType.SPECIALITIES).and(hospitalTag.tagContent.eq(specialitiesCond)) : null; } private BooleanExpression emergencyEq(String emergencyCond) { return emergencyCond.equals("EMERGENCY") ? hospitalTag.tagType.eq(HospitalTagType.EMERGENCY).and(hospitalTag.tagContent.eq(emergencyCond)) : null; } private BooleanExpression inDistance(String point, Integer radius) { return radius != null ? Expressions.booleanTemplate( "ST_Contains(ST_Buffer(ST_PointFromText({0}, 4326), {1}), {2})", point, radius, hospital.address.point ) : null; } private BooleanExpression isOpen(String isOpen, Time now) { return isOpen.equals("OPEN") ? businessHour.startTime.isNotNull().and(businessHour.endTime.isNotNull()) .and(businessHour.startTime.loe(now)).and(businessHour.endTime.goe(now)) : null; }
- java
- jpa
- querydsl





