Count operation returns 1 instead of 0 after grouping array in Spring Data MongoDB Aggregation

Viewed 454

I need to create advanced aggregation using Spring Data MongoDB having model like that:

@Getter
@Setter
@Document
public class Library {

  @Id
  @JsonSerialize(using = ToStringSerializer.class)
  private ObjectId id;

  private Address address;

  private String workingHours;

  ...

}

@Getter
@Setter
@Document
public class Book {

  @Id
  @JsonSerialize(using = ToStringSerializer.class)
  private ObjectId id;

  private Boolean published;

  private Boolean hidden;

  private String title;

  @JsonSerialize(using = ToStringSerializer.class)
  private ObjectId libraryId;

  ...

}

pom.xml

<dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
<dependency>
        <groupId>org.springframework.data</groupId>
        <artifactId>spring-data-mongodb</artifactId>
        <version>2.2.0</version>
</dependency>

Libraries collection:

{ 
    "_id" : ObjectId("5f45440ee89590218e83a697"), 
    "workingHours" : "8:00 PM - 8:00 AM",
    "address" : DBRef("addresses", ObjectId("5f4544198da452a5523e3d11"))
}

Books collection:

{ 
    "_id" : ObjectId("5f454423be823729015661ed"), 
    "published": true,
    "hidden": false,
    "title": "The Hobbit, or There and Back Again"
    "libraryId": ObjectId("5f45440ee89590218e83a697")
},
{ 
    "_id" : ObjectId("5f45445b876d08649b88ed5a"), 
    "published": true,
    "hidden": false,
    "title": "Harry Potter and the Philosopher's Stone"
    "libraryId": ObjectId("5f45440ee89590218e83a697")
},
{ 
    "_id" : ObjectId("5f45446c7e33ca70363f629a"), 
    "published": true,
    "hidden": false,
    "title": "Harry Potter and the Cursed Child"
    "libraryId": ObjectId("5f45440ee89590218e83a697")
},
{ 
    "_id" : ObjectId("5f45447285f9b3e4cb8739ad"), 
    "published": true,
    "hidden": false,
    "title": "Fantastic Beasts and Where to Find Them"
    "libraryId": ObjectId("5f45440ee89590218e83a697")
},
{ 
    "_id" : ObjectId("5f45449fc121a20afa4fbb96"), 
    "published": false,
    "hidden": false,
    "title": "Universal Parks & Resorts"
    "libraryId": ObjectId("5f45440ee89590218e83a697")
},
{ 
    "_id" : ObjectId("5f4544a5f13839bbe89edb23"), 
    "published": false,
    "hidden": true,
    "title": "Ministry of Dawn"
    "libraryId": ObjectId("5f45440ee89590218e83a697")
}

Depending on the context of the user, I have to return a different count of books that can be filtered based on startsWith() or like() principle.

Assuming that I have 4 published books, one more added by normal user and one more hidden.

  1. Administrator should know about all of them, so he will see booksCount as 6.
  2. The regular user sees only published or added by himself, so he will see booksCount as 5.
  3. There can be some other conditions in the future.

I came up with aggregation like this:

Criteria criteria = Criteria.where("_id").ne(null).and("address.city").is("Chicago");

MatchOperation matchOperation = Aggregation.match(criteria);
            
LookupOperation lookupOperation = LookupOperation.newLookup().from("books").localField("_id").foreignField("topicId").as("books");

UnwindOperation unwindOperation = Aggregation.unwind("books", true);

MatchOperation secondMatchOperation = Aggregation.match(Criteria.where("books.published").is(Boolean.TRUE).orOperator(Criteria.where("creator.userId").is(context.getUserId()));

AggregationOperation group = Aggregation.group("_id")
            .first("_id").as("id")
            .first("published").as("published")
            .first("title").as("title")
            .push("books").as("books").count().as("booksCount");

Aggregation aggregation = !isAdministrator() ?
Aggregation.newAggregation(matchOperation, lookupOperation, unwindOperation, secondMatchOperation, group) : 
Aggregation.newAggregation(matchOperation, lookupOperation, unwindOperation, group);
            
mongoTemplate.aggregate(aggregation, "libraries", Document.class).getRawResults().get("results");

Everything works fine instead of count() operation.

  1. If books array size is 0 it always returns 1.
  2. If books array size is greater than 0 it returns the proper amount.
  3. I've tried using newBuilder(GroupOps.SUM, null, 0) instead of count(), but now it always return 0.
  4. If I use newBuilder(GroupOps.SUM, null, 2) it returns size + 2. I don't know what is going on.

My quesions:

  1. Can anyone tell me what I am doing wrong and how to correct it?
  2. In addition I need to parse "booksCount" from Integer to String. Is it possible in group stage?

Thank you in advance.

1 Answers

It happens because of Aggregation.unwind("books", true);. When there is no join, this statement keeps as the document unless you make it as Aggregation.unwind("books"). Default behavior is false. Then when you counting, the document is counted as a document. That's why its giving you the 1 as output. Example with wrong output

So what you can do is, you can count the size in the next stage.

 project("_id", "published", "title", "books")
   .and(ArrayOperators.Size.lengthOfArray(ConditionalOperators.ifNull("books").then(Collections.emptyList()))).as("booksCount")

Working Mongo playground with correct answer

Related