λ³Έλ¬Έ λ°”λ‘œκ°€κΈ°

ν”„λ‘œμ νŠΈ/Airbnb Clone

[Querydsl] Expressions.dateTemplate, timeTemplate μ‚¬μš©κΈ°

Querydsl 의 κΈ°λ³Έ μ‚¬μš©λ²•μ€ μ†Œκ°œν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.
Querydsl 을 ν•™μŠ΅ 쀑에 μž‘μ„±ν•œ 글이기 λ•Œλ¬Έμ— μ†Œκ°œν•œ 방법이 졜고의 방법이 아닐 수 μžˆμŒμ„ μ•Œλ €λ“œλ¦½λ‹ˆλ‹€πŸ™‡‍♂️
κ°œλ°œν™˜κ²½
JDK 17, Querydsl 5.0.0, Spring boot 3.x

β­μ„œλ‘ 

μ½”λ“œμŠ€μΏΌλ“œ 에어비앀비 클둠 ν”„λ‘œμ νŠΈ 쀑, μ‚¬μš©μž 쑰건에 따라 μˆ™μ†Œκ°€ κ²€μƒ‰λ˜λŠ” κΈ°λŠ₯을 κ΅¬ν˜„ν•˜κ³ μž ν–ˆμŠ΅λ‹ˆλ‹€.


μ‚¬μš©μžκ°€ 6/19 ~ 6/20 λ₯Ό μˆ™μ†Œ 검색 쑰건으둜 λ“±λ‘ν•˜λ©΄,
μ„œλ²„μ—μ„œλŠ” 6/19 ~ 6/20 κΉŒμ§€ 이용 κ°€λŠ₯ ν•œ μˆ™μ†Œ ID(μ˜ˆμ•½ λ˜μ–΄μžˆμ§€ μ•Šμ€ μˆ™μ†Œ ID) λ₯Ό μ°ΎμŠ΅λ‹ˆλ‹€.

 

예λ₯Ό λ“€μ–΄,

  • μˆ™μ†Œ IDκ°€ 1인 μˆ™μ†Œμ— `6/19 PM15:00 ~ 6/20 AM11:00` 인 μ˜ˆμ•½μ΄ μžˆλ‹€κ³  κ°€μ •ν•©λ‹ˆλ‹€.
  • λ§Œμ•½, μ‚¬μš©μžκ°€ 6/20 ~ 6/21 에 μ΄μš©κ°€λŠ₯ν•œ μˆ™μ†Œλ₯Ό μ°ΎλŠ”λ‹€λ©΄
    • IDκ°€ 1인 μˆ™μ†Œμ˜ 체크인 μ‹œκ°„μ€ PM 15:00, 체크아웃 μ‹œκ°„μ€ AM 11:00 이기 λ•Œλ¬Έμ—,
    • IDκ°€ 1인 μˆ™μ†ŒλŠ” 6/20 ~ 6/21 에 μ΄μš©κ°€λŠ₯ν•˜λ„λ‘ ν•©λ‹ˆλ‹€.
  • 이λ₯Ό μœ„ν•΄, λ‚ μ§œλŠ” λ‚ μ§œλŒ€λ‘œ μ‹œκ°„μ€ μ‹œκ°„λŒ€λ‘œ 각각 λΉ„κ΅ν•˜κ³ μž ν•©λ‹ˆλ‹€.

β­κ΅¬ν˜„

  • μ˜ˆμ•½ μ—”ν‹°ν‹°μ˜ 체크인,아웃은 LocalDateTime μž…λ‹ˆλ‹€.
* JPA 의 ddl-auto κΈ°λŠ₯을 true 둜 μ„€μ •ν•˜λ©΄ μ—”ν‹°ν‹°μ˜ LocalDateTime 은 Mysql 에 DateTime 으둜 μ €μž₯이 λ©λ‹ˆλ‹€. 
```
@Entity
public class Reservation {

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "STAY_ID", foreignKey = @ForeignKey(name = "FK_STAY_RESERVE_ID"))
    private Stay stay;

    @Column(name = "CHECK_IN")
    private LocalDateTime checkIn;

    @Column(name = "CHECK_OUT")
    private LocalDateTime checkOut;
    
}

 

  • μ‚¬μš©μžκ°€ κ²€μƒ‰ν•˜λŠ” λ‚ μ§œμ˜ νƒ€μž…μ€ LocalDate μž…λ‹ˆλ‹€.
private BooleanExpression matchesCheckInAndCheckOut(LocalDate checkInDate, LocalDate checkOutDate)

 

μ˜ˆμ•½μ—”ν‹°ν‹°μ˜ LocalDateTime 은 Mysql 의 DateTime κ³Ό λŒ€μ‘λ˜κΈ° λ•Œλ¬Έμ—(jpa 의 ddl-auto=true 둜 μ„€μ •ν–ˆμŠ΅λ‹ˆλ‹€)

μœ„μ˜ μ†Œκ°œν•œ μ˜ˆμ‹œλ₯Ό μΆ©μ‘±ν•˜κΈ° μœ„ν•΄μ„œλŠ” DateTime 을 Date 와 Time 으둜 μͺΌκ°œμ„œ,

λ‚ μ§œλŠ” λ‚ μ§œλŒ€λ‘œ μ‹œκ°„μ€ μ‹œκ°„λΌλ¦¬ 각각 비ꡐ해야 ν–ˆμŠ΅λ‹ˆλ‹€.

 

κ·Έλž˜μ„œ, Querydsl 의 Expressions  λΌμ΄λΈŒλŸ¬λ¦¬μ— μžˆλŠ” `timeTemplate` κ³Ό `dateTemplate` 을 μ‚¬μš©ν•˜μ—¬

dateTemplate 을 μ‚¬μš©ν•΄μ„œ DateTime 을 LocalDate 둜,

timeTemplate 을 μ‚¬μš©ν•΄μ„œ DateTime 을 LocalTime 으둜 λ°”κΏ¨μŠ΅λ‹ˆλ‹€.

 

 

Expressions.timeTemplate μ˜ˆμ‹œ 

// μ˜ˆμ•½ checkInμ‹œκ°„(νƒ€μž… : LocalDateTime) Time 둜 ν˜•λ³€ν™˜ν•˜μ—¬, LocalTime 으둜 λ§Œλ“­λ‹ˆλ‹€.
TimeTemplate<LocalTime> reservationCheckInTime 
        = Expressions.timeTemplate(LocalTime.class, "CAST({0} AS TIME)", reservation.checkIn);

 

Expressions.dateTemplate μ˜ˆμ‹œ 

// μ˜ˆμ•½ checkout μ‹œκ°„(νƒ€μž… : LocalDateTime)을 Date 둜 ν˜•λ³€ν™˜ν•˜μ—¬, LocalDate둜 λ§Œλ“­λ‹ˆλ‹€.
DateTemplate<LocalDate> reservationCheckOutDate 
        = Expressions.dateTemplate(LocalDate.class, "CAST({0} AS DATE)", reservation.checkOut);

 

Expressions.timeTemplate 의 νŒŒλΌλ―Έν„° μ΄ν•΄ν•˜κΈ° 

  • LocalTime.class → timeTemplate(dateTemplate) 의 결과둜 올 νƒ€μž…μ„ μ„ μ–Έν•©λ‹ˆλ‹€.
  • `CAST({0} AS TIME)`Mysql 의 DateTime 을 Time 으둜 ν˜•λ³€ν™˜ ν•©λ‹ˆλ‹€. {0}은 자리 ν‘œμ‹œμžλ‘œ, 후에 제곡된 νŒŒλΌλ―Έν„°(reservation.checkIn)둜 λŒ€μ²΄λ©λ‹ˆλ‹€. 
  • reservation.checkIn → ν˜•λ³€ν™˜ ν•˜κ³  싢은 Mysql 의 μ»¬λŸΌμ„ μ„ μ–Έν•©λ‹ˆλ‹€.

 

전체 μ½”λ“œ

package com.airdnb.clone.domain.booking.repository;

import static com.airdnb.clone.domain.booking.entity.QBooking.booking;
import static com.airdnb.clone.domain.stay.entity.QStay.stay;

import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.core.types.dsl.DateTemplate;
import com.querydsl.core.types.dsl.Expressions;
import com.querydsl.core.types.dsl.TimeTemplate;
import com.querydsl.jpa.impl.JPAQuery;
import com.querydsl.jpa.impl.JPAQueryFactory;
import java.time.LocalDate;
import java.time.LocalTime;
import java.util.List;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
public class ReservationQuerydslImpl implements ReservationQuerydsl{

    private final JPAQueryFactory jpaQueryFactory;

    public List<Long> findUnavailableStayIdsByCheckInOut(LocalDate checkInDate, LocalDate checkOutDate) {
        return jpaQueryFactory
                .select(reservation.stay.id)
                .distinct()
                .from(reservation)
                .where(matchesCheckInAndCheckOut(checkInDate, checkOutDate))
                .fetch();
    }

    private BooleanExpression matchesCheckInAndCheckOut(LocalDate checkInDate, LocalDate checkOutDate) {

        if (checkInDate == null || checkOutDate == null) {
            return null;
        }
        if (checkInDate.isAfter(checkOutDate)) {
            throw new IllegalArgumentException();
        }

        DateTemplate<LocalDate> reservationCheckInDate = Expressions.dateTemplate(LocalDate.class, 
                "CAST({0} AS DATE)"", reservation.checkIn);

        TimeTemplate<LocalTime> reservationCheckInTime = Expressions.timeTemplate(LocalTime.class,
                "CAST({0} AS TIME)"", reservation.checkIn);

        DateTemplate<LocalDate> reservationCheckOutDate = Expressions.dateTemplate(LocalDate.class,
                "CAST({0} AS DATE)"", reservation.checkOut);

        TimeTemplate<LocalTime> reservationCheckOutTime = Expressions.timeTemplate(LocalTime.class,
                "CAST({0} AS TIME)"", reservation.checkOut);

     /*
     *  (이미 μ˜ˆμ•½ 된 체크인 λ‚ μ§œ < κ²€μƒ‰ν•œ 체크아웃 λ‚ μ§œ) or
     *  (이미 μ˜ˆμ•½λœ 체크인 λ‚ μ§œ == κ²€μƒ‰ν•œ 체크아웃 λ‚ μ§œ) and (이미 μ˜ˆμ•½λœ μˆ™μ†Œμ˜ 체크인 μ‹œκ°„ < ν•΄λ‹Ή μˆ™μ†Œμ˜ 체크아웃 μ‹œκ°„)
     *  and
     *  (이미 μ˜ˆμ•½λœ 체크아웃 λ‚ μ§œ > κ²€μƒ‰ν•œ 체크인 λ‚ μ§œ) or
     *  (이미 μ˜ˆμ•½λœ 체크아웃 λ‚ μ§œ == κ²€μƒ‰ν•œ 체크인 λ‚ μ§œ) and (이미 μ˜ˆμ•½λœ μˆ™μ†Œμ˜ 체크아웃 μ‹œκ°„ > ν•΄λ‹Ή μˆ™μ†Œμ˜ 체크인 μ‹œκ°„)
     * 에 ν•΄λ‹Ήλ˜λ©΄ μ˜ˆμ•½ λΆˆκ°€λŠ₯ν•œ μˆ™μ†Œμ— ν•΄λ‹Ή.
     */
      return  reservationCheckInDate.lt(checkOutDate)
                .or(reservationCheckInDate.eq(checkOutDate).and(reservationCheckInTime.lt(reservation.stay.checkOutTime)))             
                .and(reservationCheckOutDate.gt(checkInDate)
                     .or(reservationCheckOutDate.eq(checkInDate).and(reservationCheckOutTime.gt(reservation.stay.checkInTime))));
    }
}

이 외에도 μ‹œκ°„κ³Ό λ‚ μ§œλ₯Ό λΉ„κ΅ν•˜κΈ° μœ„ν•΄, Querydsl 의 between κ³Ό stringTemplate 도 μžˆμœΌλ‹ˆ 상황에 맞게 μ„ νƒν•˜μ‹œλ©΄ λ˜κ² μŠ΅λ‹ˆλ‹€.