Comment compter correctement les éléments de tableau avec une agrégation multiple dans Spring Data MongoDB?

Aug 27 2020

Je dois créer une agrégation avancée à l'aide de Spring Data MongoDB ayant un modèle comme celui-ci:

@Getter
@Setter
@Document
public class City {

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

  private Address description;

  private String name;

  ...

}

@Getter
@Setter
@Document
public class Library {

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

  private Address address;

  private String workingHours;

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

  ...

}

@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>

Collection Villes:

{ 
    "_id" : ObjectId("5f47878c95f47e209402fe15"), 
    "name" : "Warsaw",
    "description" : "Sample description"
}
{ 
    "_id" : ObjectId("5f4787918b343fff4f52c270"), 
    "name" : "Chicago",
    "description" : "Sample description"
}

Collection bibliothèques:

{ 
    "_id" : ObjectId("5f45440ee89590218e83a697"), 
    "workingHours" : "8:00 PM - 8:00 AM",
    "address" : DBRef("addresses", ObjectId("5f4544198da452a5523e3d11")),
    "cityId": ObjectId("5f47878c95f47e209402fe15")
},
{ 
    "_id" : ObjectId("5f478725d1507323a80efa31"), 
    "workingHours" : "8:00 PM - 8:00 AM",
    "address" : DBRef("addresses", ObjectId("5f4787379e72f882e4d26912")),
    "cityId": ObjectId("5f47878c95f47e209402fe15")
},
{ 
    "_id" : ObjectId("5f47872f7c4872d4983961f5"), 
    "workingHours" : "8:00 PM - 8:00 AM",
    "address" : DBRef("addresses", ObjectId("5f47873d5ddedadb3d6ddd6e")),
    "cityId": ObjectId("5f4787918b343fff4f52c270")
}

Collection de livres:

{ 
    "_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")
}

En fonction du contexte de l'utilisateur, je dois renvoyer des villes avec le nombre de bibliothèques et de livres dans la ville qui peuvent être filtrés en fonction startsWith()ou en like()principe.

En supposant que j'ai 2 bibliothèques dans une ville et 1 bibliothèque dans l'autre.

  1. Je dois d'abord compter les bibliothèques en utilisant la recherche et le retour librariesCount- ce sera 2et 1.
  2. J'ai besoin d'aller chercher / rechercher des livres dans chaque bibliothèque, puis de les compter comme «booksCount», puis de multiplier par librariesCountpour obtenir le montant total de booksCountdans la ville (appelons-le cityBooksCount).

J'ai proposé une agrégation comme celle-ci:

Criteria criteria = Criteria.where("_id");

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

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

LookupOperation secondLookupOperation = LookupOperation.newLookup().
              from("books").
              localField("libraryIdArray").
              foreignField("libraryId").
              as("books");

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

AggregationOperation group = Aggregation.group("_id")
            .first("_id").as("id")
            .first("name").as("name")
            .first("description").as("description")
            .push("libraries").as("libraries")
            .push("books").as("books");

ProjectionOperation projectionOperation = Aggregation.project("id", "description", "name")              
.and(VariableOperators.mapItemsOf(ConditionalOperators.ifNull("libraries").then(Collections.emptyList()))
.as("library").andApply(aggregationOperationContext -> {
                  Document document = new Document();
                  document.append("id", "$$library._id");
                  return document;
              })).as("libraryIdArray")
.and(ConvertOperators.valueOf(ArrayOperators.Size.lengthOfArray(ConditionalOperators.ifNull("libraries").then(Collections.emptyList()))).convertToString()).as("librariesCount")        
.and(ConvertOperators.valueOf(ArrayOperators.Size.lengthOfArray(ConditionalOperators.ifNull("books").then(Collections.emptyList()))).convertToString()).as("cityBooksCount");

Aggregation aggregation = Aggregation.newAggregation(matchOperation, lookupOperation, unwindOperation, secondLookupOperation, secondUnwindOperation, group, projectionOperation);
            
mongoTemplate.aggregate(aggregation, "cities", Document.class).getRawResults().get("results");

Grâce à l'aide de l'un des utilisateurs de stackoverflow, j'ai pu obtenir librariesCountcorrectement. Malheureusement cityBooksCounttoujours pointer vers 0.

Je ne suis pas très familier avec MongoDB, mais je sais que l' $lookup opération est possible sur un tableau , j'ai donc essayé de mapper un tableau de bibliothèques à une liste ObjectId, mais cela ne fonctionne pas correctement. Je fais probablement quelque chose de mal, mais je ne sais pas où est le problème. J'obtiens la bonne quantité de villes avec d'autres champs projetés.

Quelqu'un peut-il me dire ce que je fais mal et comment y remédier?

Merci d'avance.

Réponses

1 varman Aug 27 2020 at 21:06

Cela pourrait vous donner la réponse attendue.

db.cities.aggregate([
  {
    "$lookup": { "from": "Libraries", "localField": "_id", "foreignField": "cityId", "as": "libraries" } }, { $unwind: {
      path: "$libraries", preserveNullAndEmptyArrays: true } }, { "$lookup": {
      "from": "Books",
      "localField": "libraries._id",
      "foreignField": "libraryId",
      "as": "books"
    }
  },
  {
    $unwind: { path: "$books",
      preserveNullAndEmptyArrays: true
    }
  },
  {
    $group: { _id: "$_id",
      name: {
        $first: "$name"
      },
      description: {
        $first: "$description"
      },
      libraries: {
        $push: "$libraries"
      },
      books: {
        $push: "$books"
      }
    }
  },
  {
    $project: { _id: 1, name: 1, description: 1, libraryCount: { $size: "$libraries" }, bookCount: { $size: "$books"
      }
    }
  }
])

Comme nous en avons discuté, il y a quelques légers changements. J'espère que vous avez compris comment convertir la requête mongo en agrégation de données Spring.