1

I am looking for a way to filter out all objects from a given List of Object after providing some conditions.

For example

Class A

@Entity(value = "tbl_A")
public class A {

private String notes;
@Embedded
private List<SampleObject> sampleObject;

....getter and setter ...
}

Class B

@Embedded
public class SampleObject {
    private boolean read;
    private boolean sentByBot;

   ... getter and setter ...
   }

Now, I only want to collect the SampleObject which does have sentByBot parameter set to true. I am using following approach:

Query<A> queryForA = datastore.find(A.class);
queryForA.field("sampleObject.sentByBot").equal(false).retrievedFields(true, "sampleObject.sentByBot");

Above code is giving me the whole list of objects which have sampleObject.sentByBot true and false both.

I also tried filter approach i.e.

 queryForA.filter("sampleObject.sentByBot", false).retrievedFields(true, "sampleObject.sentByBot");

But no luck. Is there any way to get those fields only which have sampleObject.sentByBot set true?

Edit

After implementing following code I got this:

Database Image

enter image description here

Code

  AggregationOptions options = AggregationOptions.builder()
              .outputMode(AggregationOptions.OutputMode.CURSOR)
              .build();

  //System.out.println(options.toString());

  Projection filterProjection = Projection.projection(
              "sampleObjects",
              Projection.expression(
                      "$filter",
                      new BasicDBObject("input","$sampleObjects")
                              .append("cond",new BasicDBObject("$eq", Arrays.asList("$$this.sentByBot",true)))
              )
      );

 AggregationPipeline pipeline = datastore.createAggregation(A.class)
              .match(datastore.createQuery(A.class).filter("sampleObjects.sentByBot", true))
              .project(
                      Projection.projection("fieldA"),
                      Projection.projection("fieldB"),
                      filterProjection
              );

      Iterator<A> cursor = pipeline.aggregate(A.class, options);

Output

"Command failed with error 28646: '$filter only supports an object as its argument'. The full response is { \"ok\" : 0.0, \"errmsg\" : \"$filter only supports an object as its argument\", \"code\" : 28646 }"
Amit Pal
  • 9,143
  • 23
  • 64
  • 141
  • What you want is actually achieved by the [`$filter`](https://docs.mongodb.com/manual/reference/operator/aggregation/filter/) aggregation operator as demonstrated on [Retrieve only the queried element in an object array in MongoDB collection](https://stackoverflow.com/q/3985214/2313887). It's not the job a a regular query expression to "alter documents" in such a way and you can only at best return the ["first match"](https://docs.mongodb.com/manual/reference/operator/projection/positional/) with a regular query. You need to construct the aggregate statement for this instead. – Neil Lunn May 29 '18 at 03:35
  • @NeilLunn How can I achieve `$filter` in `morphia` ? – Amit Pal May 29 '18 at 03:50

1 Answers1

3

As stated earlier, what you want in order to only return "multiple" array elements which match a given condition is the $filter aggregation pipeline operator in projection. In order to issue such an aggregation statement with Morphia you want something like this:

Projection filterProjection = projection(
        "sampleObjects",
        expression(
                "$filter",
                new BasicDBObject("input","$sampleObjects")
                .append("cond",new BasicDBObject("$eq", Arrays.asList("$$this.sentByBot",true)))
        )
);

AggregationPipeline pipeline = datastore.createAggregation(A.class)
        .match(datastore.createQuery(A.class).filter("sampleObjects.sentByBot", true))
        .project(
                projection("fieldA"),
                projection("fieldB"),
                filterProjection
        );

Which issues a pipeline to the server as:

[ 
  { "$match" : { "sampleObjects.sentByBot" : true } },
  { "$project" : {
    "fieldA" : 1,
    "fieldB" : 1,
    "sampleObjects" : { 
      "$filter" : {
        "input" : "$sampleObjects",
        "cond" : { "$eq" : [ "$$this.sentByBot", true ] }
      }
    }
  }}
]

And returns only array elements from the matching documents that also match the condition:

{ 
  "className" : "com.snakier.example.A" ,
  "_id" : { "$oid" : "5b0ce52c6a6bfa50084c53aa"} ,
  "fieldA" : "something" ,
  "fieldB" : "else" ,
  "sampleObjects" : [
   { "name" : "one" , "read" : false , "sentByBot" : true} ,
   { "name" : "three" , "read" : true , "sentByBot" : true}
  ]
}

Note that you need to build the expression() in argument manually from DBObject() as there are no current "builders" supported in Morphia for this type of operation. It would be expected that future releases would change to the Document interface which has been standard in the underlying Java driver for some time now.

As a full example listing:

package com.snakier.example;

import com.mongodb.AggregationOptions;
import com.mongodb.BasicDBObject;
import com.mongodb.MongoClient;
import org.bson.types.ObjectId;
import org.mongodb.morphia.Datastore;
import org.mongodb.morphia.Morphia;
import org.mongodb.morphia.aggregation.AggregationPipeline;
import org.mongodb.morphia.aggregation.Projection;
import org.mongodb.morphia.annotations.Embedded;
import org.mongodb.morphia.annotations.Entity;
import org.mongodb.morphia.annotations.Id;

import java.util.Arrays;
import java.util.Iterator;
import java.util.List;

import static org.mongodb.morphia.aggregation.Projection.*;

public class Application {

    public static void main(String[] args) {
        final Morphia morphia = new Morphia();

        morphia.mapPackage("com.snakier.example");

        final Datastore datastore = morphia.createDatastore(new MongoClient(),"example");

        // Clean example database
        datastore.getDB().getCollection("example").drop();

        // Create some data
        final A first = new A("something","else");
        final A second = new A("another","thing");

        final SampleObject firstSample = new SampleObject("one", false, true);
        final SampleObject secondSample = new SampleObject("two", false, false);
        final SampleObject thirdSample = new SampleObject("three", true,true);
        final SampleObject fourthSample = new SampleObject("four", true, false);

        first.setSampleObjects(Arrays.asList(firstSample,secondSample,thirdSample));
        datastore.save(first);

        second.setSampleObjects(Arrays.asList(fourthSample));
        datastore.save(second);

        AggregationOptions options = AggregationOptions.builder()
                .outputMode(AggregationOptions.OutputMode.CURSOR)
                .build();

        //System.out.println(options.toString());

        Projection filterProjection = projection(
                "sampleObjects",
                expression(
                        "$filter",
                        new BasicDBObject("input","$sampleObjects")
                        .append("cond",new BasicDBObject("$eq", Arrays.asList("$$this.sentByBot",true)))
                )
        );

        AggregationPipeline pipeline = datastore.createAggregation(A.class)
                .match(datastore.createQuery(A.class).filter("sampleObjects.sentByBot", true))
                .project(
                        projection("fieldA"),
                        projection("fieldB"),
                        filterProjection
                );

        Iterator<A> cursor = pipeline.aggregate(A.class, options);

        while (cursor.hasNext()) {
            System.out.println(morphia.toDBObject(cursor.next()));
        }

    }
}

@Entity(value = "example")
class A {
    @Id
    private ObjectId id;
    private String fieldA;
    private String fieldB;

    @Embedded
    private List<SampleObject> sampleObjects;

    public  A() {

    }

    public A(String fieldA, String fieldB) {
        this.fieldA = fieldA;
        this.fieldB = fieldB;
    }

    public void setSampleObjects(List<SampleObject> sampleObjects) {
        this.sampleObjects = sampleObjects;
    }

    public List<SampleObject> getSampleObjects() {
        return sampleObjects;
    }

    public ObjectId getId() {
        return id;
    }

    public String getFieldA() {
        return fieldA;
    }

    public void setFieldA(String fieldA) {
        this.fieldA = fieldA;
    }

    public void setFieldB(String fieldB) {
        this.fieldB = fieldB;
    }

    public String getFieldB() {
        return fieldB;
    }

}

@Embedded
class SampleObject {
    private String name;
    private boolean read;
    private boolean sentByBot;

    public SampleObject() {

    }

    public SampleObject(String name, boolean read, boolean sentByBot) {
        this.name = name;
        this.read = read;
        this.sentByBot = sentByBot;
    }

    public String getName() {
        return name;
    }

    public boolean isRead() {
        return read;
    }

    public boolean isSentByBot() {
        return sentByBot;
    }
}
Neil Lunn
  • 130,590
  • 33
  • 275
  • 280
  • Thanks for the explanation and example but I am getting this error `$filter only supports an object as its argument'` at `Iterator cursor = pipeline.aggregate(A.class, options);`. I am looking for an error but not able to figure it out – Amit Pal May 30 '18 at 11:56
  • @AmitPal You will not get that from the "self enclosed" sample, which is exactly the reason you got this. I showed the sample for the purpose of a reproducible case, which you did not provide in the question though you are actually expected to do so when posting here. Such errors come from different data to what sis presented. If you have a new question then [Ask a new Question](https://stackoverflow.com/questions/ask), but the question you actually asked has been reasonably answered here. – Neil Lunn May 30 '18 at 12:57
  • I understand it but what is it saying (error)? I changed everything as per the sample code too. – Amit Pal May 30 '18 at 13:05
  • @AmitPal This code runs without error. If you are running your own code then it does not look like this code and therefore is a different question. Of course you can actually look at what will be clearly different in your own code from this, but if you still cannot work that out then [Ask a new Question](https://stackoverflow.com/questions/ask). You were just shown how to do something with working code, therefore the question asked here has been answered. – Neil Lunn May 30 '18 at 23:10
  • I am just asking "What does that error mean"? I am being very polite here – Amit Pal May 30 '18 at 23:50
  • `"$filter", new BasicDBObject("input","$sampleObjects")` Is a "key" and "object" pair. If you have done anything different to that then you will get an error. Just run the code you have been given and work out whatever else you are doing wrong later. I've given you code as proof his does the right thing. Further debugging if you cannot follow the example is another question. – Neil Lunn May 30 '18 at 23:54
  • I have tested your code and it's even throwing the same error. Not even changed the parameter / class variable name. What morphia version are you using? – Amit Pal May 30 '18 at 23:59
  • Dude the code runs fine. Just please actually run it "as it is" and please stop commenting. – Neil Lunn May 31 '18 at 00:03