Search code examples
javamongodbspring-bootspring-data-mongodbquerydsl

Fetching MongoDB documents where all array elements are NOT between a range of dates


I'm learning Springboot and MongoDB.

I have this model class:

@Data
@Document(collection = "accommodations")
public class Accommodation {

    @Id
    private String accommodationId;
    @TextIndexed
    private String title;
    @Indexed
    private double pricePerNight;
    private int guests;
    @Indexed
    private double rating;
    private AccommodationType type;
    private String description;
    private boolean listed;
    private Address address;
    private Landlord landlord;
    private List<Image> images;
    private List<Review> reviews;
    private List<Booking> bookings;

    public Accommodation(String title, double pricePerNight, int guests, AccommodationType type, String description, Address address, Landlord landlord) {
        this.title = title;
        this.pricePerNight = pricePerNight;
        this.guests = guests;
        this.type = type;
        this.description = description;
        this.listed = true;
        this.address = address;
        this.landlord = landlord;
        this.rating = 0.0;
        this.images = new ArrayList<>();
        this.reviews = new ArrayList<>();
        this.bookings = new ArrayList<>();
    }

    public boolean isAvailableBetween(LocalDate checkin, LocalDate checkout) {
        return this.bookings
                .stream()
                .noneMatch(b -> (b.getCheckin().isBefore(checkout) || b.getCheckin().isEqual(checkout)) &&
                        (b.getCheckout().isAfter(checkin)) || b.getCheckout().isEqual(checkin));
    }

}

I'm currently using the method "isAvailableBetween" to filter accommodations that are already booked in a certain range of dates (checkin and checkout).

I would like to achieve the same through a Mongo query, but can't figure out how.

This is what I've tried so far (ignore the other filters in the query):

@Repository
public interface AccommodationRepository extends MongoRepository<Accommodation, String>, QuerydslPredicateExecutor<Accommodation> {    

    @Query("{" +
            "$and: [" +
                "{listed:true}," +
                "{pricePerNight:{$lte:?0}}," +
                "{guests:{$gte:?1}}," +
                "{rating:{$gte:?2}}," +
                "{'landlord.trusted':?3}," +
                "{bookings:{$not:{$elemMatch:{checkin:{$lte:?5},checkout:{$gte:?4}}}}}" +
            "]" +
            "}")
    Page<Accommodation> findAccommodationsByFilter(Pageable pageable,
                                                   Double pricePerNight,
                                                   Integer guests,
                                                   Double rating,
                                                   Boolean trusted,
                                                   LocalDate checkin,
                                                   LocalDate checkout

}

Also tried using QueryDSL & Mongo Criterias with Spring Data MongoDB, but couldn't figure out a way to get the same result as the "isAvailableBetween".

Does anyone know how to do this?


Solution

  • After studying Mongo Criterias further, I found a solution!

    public Page<Accommodation> findAccommodationsByFilter(AccommodationFilter filter, Pageable pageable) {
    
        Criteria listedTrue = where("listed").is(true);
        Query query = query(listedTrue);
        filter.pricePerNight().map(where("pricePerNight")::lte).ifPresent(query::addCriteria);
        filter.guests().map(where("guests")::gte).ifPresent(query::addCriteria);
        filter.type().map(where("type")::is).ifPresent(query::addCriteria);
        filter.rating().map(where("rating")::gte).ifPresent(query::addCriteria);
        filter.trustedLandlord().map(where("landlord.trusted")::is).ifPresent(query::addCriteria);
        filter.country().map(where("address.country")::is).ifPresent(query::addCriteria);
        filter.city().map(where("address.city")::is).ifPresent(query::addCriteria);
    
        if(filter.checkout().isPresent() && filter.checkin().isPresent()){
            Criteria c = where("bookings")
                    .not()
                    .elemMatch(where("checkin").lte(filter.checkout().get())
                            .and("checkout").gte(filter.checkin().get()));
    
            query.addCriteria(c);
        }
    
        long total = this.mongoOperations.count(query, Accommodation.class);
    
        query.with(pageable);
        List<Accommodation> accommodations = this.mongoOperations.find(query, Accommodation.class);
    
        return new PageImpl<>(accommodations, pageable, total);
    }
    

    This achieves the same as the @Query with the added advantage that the parameters can be optional.