Wie werden Array-Elemente mit Mehrfachaggregation in Spring Data MongoDB richtig gezählt?

Aug 27 2020

Ich muss eine erweiterte Aggregation mit Spring Data MongoDB mit folgendem Modell erstellen:

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

Städte Sammlung:

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

Bibliothekssammlung:

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

Büchersammlung:

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

Abhängig vom Kontext des Benutzers muss ich Städte mit einer Anzahl von Bibliotheken und Büchern in der Stadt zurückgeben, die nach startsWith()oder nach dem like()Prinzip gefiltert werden können.

Angenommen, ich habe 2 Bibliotheken in einer Stadt und 1 Bibliothek in der anderen.

  1. Ich muss Bibliotheken zuerst mit Lookup zählen und zurückgeben librariesCount- es wird 2und sein 1.
  2. Ich muss Bücher in jeder Bibliothek abrufen / nachschlagen, sie dann als 'booksCount' zählen und dann mit multiplizieren librariesCount, um die Gesamtmenge booksCountin der Stadt zu erhalten (nennen wir es cityBooksCount).

Ich habe mir eine Aggregation wie diese ausgedacht:

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");

Dank der Hilfe eines Stackoverflow-Benutzers konnte ich auf die librariesCountrichtige Weise erhalten. Leider cityBooksCountimmer darauf hinweisen 0.

Ich bin mit MongoDB nicht so vertraut, aber ich weiß, dass $lookup Operationen auf Arrays möglich sind. Deshalb habe ich versucht, Bibliotheken Arrays einer Liste zuzuordnen ObjectId, aber es funktioniert nicht richtig. Wahrscheinlich mache ich etwas falsch, aber ich weiß nicht, wo das Problem liegt. Ich bekomme die richtige Anzahl von Städten mit anderen projizierten Feldern.

Kann mir jemand sagen, was ich falsch mache und wie ich es korrigieren kann?

Danke im Voraus.

Antworten

1 varman Aug 27 2020 at 21:06

Dies könnte Ihnen die erwartete Antwort geben.

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"
      }
    }
  }
])

Wie wir bereits besprochen haben, gibt es einige geringfügige Änderungen. Ich hoffe, Sie haben verstanden, wie die Mongo-Abfrage in die Spring-Datenaggregation konvertiert wird.