I have 2 documents, Topic and comment. Each topic has many comments and the documents look like this:
@Document
public class Topic {
@Id
private String id;
@Indexed(unique = true)
@NotBlank
private String title;
}
@Document
public class Comment {
@NotBlank
private String text;
@Indexed
@NotBlank
private String topic; // id of topic
@CreatedDate
@Indexed
private LocalDateTime createdDate;
}
So I actually save the id reference of Topic within comments.
This is my aggregation scenario: List the topics, which received most comments today. So three things:
This is the code so far:
MatchOperation filterCreatedDate = match(Criteria.where("createdDate").gte(today.atStartOfDay()).lt(today.plusDays(1).atStartOfDay()));
GroupOperation groupByTopic = group("topic").count().as("todaysCommentsCount");
SortOperation sortByTodaysCommentsCount = sort(Sort.Direction.DESC, "todaysCommentsCount");
Aggregation aggregation = newAggregation(filterCreatedDate, groupByTopic, sortByTodaysCommentsCount);
AggregationResults<TopTopic> result = mongoTemplate.aggregate(aggregation, Comment.class, TopTopic.class);
This is a special Class for this output:
public class TopTopic {
private int todaysCommentsCount;
}
And this is the output of my aggregation where there is only one topic:
{
"mappedResults": [
{
"todaysCommentsCount": 3
}
],
"rawResults": {
"results": [
{
"_id": "5dbdca8112a617031728c417", // topic id
"todaysCommentsCount": 3
}
],
"ok": 1.0
},
"serverUsed": null,
"uniqueMappedResult": {
"todaysCommentsCount": 1
}
}
I thought I am actually pretty close, but somehow it works only when I have only one topic. When there are comments from more than one topic which should then create multiple groups, I get this error:
Could not write JSON: Expected unique result or null, but got more than one!; nested exception is com.fasterxml.jackson.databind.JsonMappingException: Expected unique result or null, but got more than one! (through reference chain: org.springframework.data.mongodb.core.aggregation.AggregationResults[\"uniqueMappedResult\"]
.. althought i do not call any getUniqueMappedResult method.
What do I do wrong?
Secondly, how can I get rid of my output class TopTopic, and instead return the original Topic values extended with todaysCommentsCount, without creating a special output class?
I appreciate for any help.
First Part
You are sending the AggregationResults
back to caller where jackson is serializing into json and failing when it is calling getUniqueMappedResult
.
Add the topic field _id to TopTopic
and read mapped results in AggregationResults
.
List<TopTopic> topTopics = result.getMappedResults()
Your output would look like
[
{
"_id": "5dbdca8112a617031728c417",
"todaysCommentsCount": 3
}
]
Second Part
You can use $$ROOT
variable with $first
to map the entire document in the $group
stage followed by $replaceRoot
to promote the merge document ( $mergeObjects
to merge the doc
and todaysCommentCount
).
Add the todaysCommentsCount
to Topic class.
Something like
MatchOperation filterCreatedDate = match(Criteria.where("createdDate").gte(today.atStartOfDay()).lt(today.plusDays(1).atStartOfDay()));
GroupOperation groupByTopic = group("topic").first("$$ROOT").as("doc").count().as("todaysCommentsCount");
SortOperation sortByTodaysCommentsCount = sort(Sort.Direction.DESC, "todaysCommentsCount");
ReplaceRootOperation replaceRoot = Aggregation.replaceRoot().withValueOf(ObjectOperators.valueOf("doc").mergeWith(new Document("todaysCommentsCount", "$todaysCommentsCount")));
Aggregation aggregation = newAggregation(filterCreatedDate, groupByTopic, sortByTodaysCommentsCount, replaceRoot);
AggregationResults<TopTopic> result = mongoTemplate.aggregate(aggregation, Comment.class, Topic.class);
List<Topic> topTopics = result.getMappedResults();
Your output would look like
[
{
"_id": "5dbdca8112a617031728c417",
"title" : "topic",
"todaysCommentsCount": 3
}
]
Update ( Add $lookup stage to pull in topic fields)
MatchOperation filterCreatedDate = match(Criteria.where("createdDate").gte(today.atStartOfDay()).lt(today.plusDays(1).atStartOfDay()));
GroupOperation groupByTopic = group("topic").count().as("todaysCommentsCount");
SortOperation sortByTodaysCommentsCount = sort(Sort.Direction.DESC, "todaysCommentsCount");
LookupOperation lookupOperation = LookupOperation.newLookup().
from("topic").
localField("_id").
foreignField("_id").
as("topic");
ReplaceRootOperation replaceRoot = Aggregation.replaceRoot().withValueOf(ObjectOperators.valueOf("todaysCommentsCount").mergeWithValuesOf(ArrayOperators.arrayOf("topic").elementAt(0)));
Aggregation aggregation = newAggregation(filterCreatedDate, groupByTopic, sortByTodaysCommentsCount, lookupOperation, replaceRoot);
AggregationResults<TopTopic> result = mongoTemplate.aggregate(aggregation, Comment.class, Topic.class);
List<Topic> topTopics = result.getMappedResults();