spring-projects / spring-data-mongodb

Provides support to increase developer productivity in Java when using MongoDB. Uses familiar Spring concepts such as a template classes for core API usage and lightweight repository style data access.
https://spring.io/projects/spring-data-mongodb/
Apache License 2.0
1.62k stars 1.09k forks source link

$in on array fields #3528

Open SledgeHammer01 opened 3 years ago

SledgeHammer01 commented 3 years ago

Using spring-data-mongodb 3.1.2, my repo looks like:

@Repository
public interface AirBnbRepository extends  MongoRepository<Listing, String>,
                                           QuerydslPredicateExecutor<Listing> {
}

The doc has a field thats a string array:

"amenities": ["TV", "Cable TV",

I'm trying to build a dynamic query with dsl, so something like:

BooleanBuilder builder = new BooleanBuilder();

if ((amenities != null) && (amenities.length > 0))
builder.and(QListing.listing.amenities.in(amenities));

This resulted in: find using query: { "amenities" : ["Wifi2"]}, which is wrong since its missing the whole $in portion. I stepped through the code and found there is a code path in there that if you have only 1 element, you change it to an eq. So I tried 2. I also tried bypassing the whole thing with:

builder.and(Expressions.booleanOperation(Ops.IN, QListing.listing.amenities, ConstantImpl.create(amenities)));

and got the same result. No $in.

How can we get the $in to work on an array with a dynamic query at runtime? I do also need to have pageable support :).

Thanks.

SledgeHammer01 commented 3 years ago

Spent a few hours debugging this :). Seems like there are several issues in play:

  1. In SimpleExpression.java (which I'm not sure is under your umbrella), for some reason the code looks like:
    public BooleanExpression in(T... right) {
        if (right.length == 1) {
            return eq(right[0]);
        } else {
            return Expressions.booleanOperation(Ops.IN, mixin, ConstantImpl.create(ImmutableList.copyOf(right)));
        }
    }

So it looks like if you only have a single item in the array/collection, it will switch to an $eq instead. Not sure why you'd want to do that, but... :).

  1. In my model, I currently pull the field down as a String[], which will fail in MongodbSerializer::visit because there is a check there to make sure its a collection class. So an array will fail.

  2. If I switch my model to a List<String>, which I guess it should have been, then the QListing property becomes a ListPath which seems to drop the in operator entirely :).

So, long story short, there seems to be a few options to get this to work "as is":

keep the model as a String[] (which isn't really "the Spring way" it seems) in the service layer, convert it to a List<String> use builder.and(Expressions.booleanOperation(Ops.IN, QListing.listing.amenities, ConstantImpl.create(amenities))); to bypass the 1 item "feature"

OR

switch the model to a List still have to use builder.and(Expressions.booleanOperation(Ops.IN, QListing.listing.amenities, ConstantImpl.create(amenities))); in the service layer to bypass the one item "feature", but you at least avoid the list conversion step. Also you need to use that bypass since the property will now be a ListPath and won't even have the in operator.