17

It's the first time I am using Mongo in Java and I am having some problems with this aggregation query. I can do some simple queries in Mongo for Spring with @Query annotation in my Repository interface which extends the MongoRepository<T, ID>. It would be helpful to know which approach to take when you do long aggregations in Spring-Data.

db.post.aggregate([
    {
      $match: {}
    },
    {
      $lookup: {
        from: "users",
        localField: "postedBy",
        foreignField: "_id",
        as: "user"
      }
    },
    {
      $group: {
        _id: {
          username: "$user.name",
          title: "$title",
          description: "$description",
          upvotes: { $size: "$upvotesBy" },
          upvotesBy: "$upvotesBy",
          isUpvoted: { $in: [req.query.userId, "$upvotesBy"] },
          isPinned: {
            $cond: {
              if: { $gte: [{ $size: "$upvotesBy" }, 3] },
              then: true,
              else: false
            }
          },
          file: "$file",
          createdAt: {
            $dateToString: {
              format: "%H:%M %d-%m-%Y",
              timezone: "+01",
              date: "$createdAt"
            }
          },
          id: "$_id"
        }
      }
    },
    { $sort: { "_id.isPinned": -1, "_id.createdAt": -1 } }
])
David Prifti
  • 621
  • 2
  • 8
  • 17
  • 1
    You need to use `mongoTemplate` or native MongoDB driver. Take a look this [article](https://stackoverflow.com/q/17008947/3710490) – Valijon Jan 11 '20 at 18:45
  • Possible duplicate of https://stackoverflow.com/questions/17008947/whats-the-difference-between-spring-datas-mongotemplate-and-mongorepository – krishna Prasad Jan 14 '20 at 01:23

2 Answers2

28

Although this is old thread, but I hope whoever found this thread can now safely for doing multi stage/pipeline aggregation(not quite sure what it's call) in MongoRepository. As I'm also struggling looking for clue and example of aggregation in mongo repository without mongo template.

But now, I'm able to do the Aggregation pipeline as per spring doc said in here

My aggregation looks like this in mongoshell:

db.getCollection('SalesPo').aggregate([
    {$project: {
        month: {$month: '$poDate'},
        year: {$year: '$poDate'},
        amount: 1,
        poDate: 1
     }},
      {$match: {$and : [{year:2020} , {month:7}] 
     }}
      ,
      {$group: { 
          '_id': {
            month: {$month: '$poDate'},
            year: {$year: '$poDate'} 
          },
          totalPrice: {$sum: {$toDecimal:'$amount'}},
          }
      },
    {$project: {
        _id: 0,
        totalPrice: {$toString: '$totalPrice'}
     }}
 ])

While I transform it into @Aggregation annotation in MongoRepository become like this below (I'm removing the aposthrephe and also replace with method params):

@Repository
public interface SalesPoRepository extends MongoRepository<SalesPo, String> {

@Aggregation(pipeline = {"{$project: {\n" +
        "        month: {$month: $poDate},\n" +
        "        year: {$year: $poDate},\n" +
        "        amount: 1,\n" +
        "        poDate: 1\n" +
        "     }}"
        ,"{$match: {$and : [{year:?0} , {month:?1}] \n" +
        "     }}"
        ,"{$group: { \n" +
        "          '_id': {\n" +
        "            month: {$month: $poDate},\n" +
        "            year: {$year: $poDate} \n" +
        "          },\n" +
        "          totalPrice: {$sum: {$toDecimal:$amount}},\n" +
        "          }\n" +
        "      }"
    ,"{$project: {\n" +
        "        _id: 0,\n" +
        "        totalPrice: {$toString: $totalPrice}\n" +
        "     }}"})
    AggregationResults<SumPrice> sumPriceThisYearMonth(Integer year, Integer month);

My Document looks like this:

@Document(collection = "SalesPo")
@Data
public class SalesPo {
  @Id
  private String id;
  @JsonSerialize(using = LocalDateSerializer.class)
  private LocalDate poDate;
  private BigDecimal amount;
}

And the SumPrice class for hold the projections:

@Data
public class SumPrice {
  private BigDecimal totalPrice;
}

I hope this answer can help whoever try to do aggregation in mongorepository without using mongotemplate.

Yosep G
  • 281
  • 3
  • 5
  • how do you handle null parameters? – saran3h Jul 16 '20 at 08:33
  • @saran3h sum mongodb out of the box handle null value (or non numeric value) to zero. See https://docs.mongodb.com/manual/reference/operator/aggregation/sum – Yosep G Jul 17 '20 at 11:16
  • 2
    Hi,thanks for the answer,but i have some confusion: 1.is there a better way writing query json in @Aggregation?It is annoyed by parsing query string with the line-break pattern and quotation marks. – Anoman.M Feb 04 '21 at 05:57
  • 2.Is it possible not defining a class to retrieve the aggregation result?Because the result may contain many reference documents.and i have to define them all to do this.For example, i already defined some entities mapping to the collection.and they are refer to each other by using array of objectids.and i do aggregation to return a whole matching complete document which will replace objectids to real documents.To retreive the final document,i have redefine the whole entity tree with nothing changed except replace objectid array to a nested entity array.is there a better way to solve this? – Anoman.M Feb 04 '21 at 06:01
  • updating:for question 2 , i work out with using JSONObject ,Object or BasicDBObject to retreive the aggregation result.Like this:`AggregationResults result = someRepository.doAggregate(id)` – Anoman.M Feb 04 '21 at 06:32
  • I am getting a weird result back. I get aggregateResult with 2 items, but both items are empty. Tried with JSONObject, didnt work either. – Jay Patel - PayPal Apr 21 '22 at 07:26
27

You can implement the AggregationOperation and write the custom aggregation operation query and then use MongoTemplate to execute any mongo shell query you have executed in your mongo shell as below:

Custom Aggregation Operation

import org.springframework.data.mongodb.core.aggregation.AggregationOperation;
import org.springframework.data.mongodb.core.aggregation.AggregationOperationContext;

public class CustomAggregationOperation implements AggregationOperation {

  private String jsonOperation;

  public CustomAggregationOperation(String jsonOperation) {
    this.jsonOperation = jsonOperation;
  }

  @Override
  public org.bson.Document toDocument(AggregationOperationContext aggregationOperationContext) {
    return aggregationOperationContext.getMappedObject(org.bson.Document.parse(jsonOperation));
  }
}

Any Mongo Shell Aggregation query executor

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.aggregation.Aggregation;
import org.springframework.data.mongodb.core.aggregation.AggregationOperation;
import org.springframework.data.mongodb.core.aggregation.AggregationResults;
import org.springframework.data.mongodb.core.aggregation.TypedAggregation;
import org.springframework.stereotype.Service;
import sample.data.mongo.models.Course;

@Service
public class LookupAggregation {

  @Autowired
  MongoTemplate mongoTemplate;

  public void LookupAggregationExample() {

    AggregationOperation unwind = Aggregation.unwind("studentIds");

    String query1 = "{$lookup: {from: 'student', let: { stuId: { $toObjectId: '$studentIds' } },"
        + "pipeline: [{$match: {$expr: { $eq: [ '$_id', '$$stuId' ] },},}, "
        + "{$project: {isSendTemplate: 1,openId: 1,stu_name: '$name',stu_id: '$_id',},},], "
        + "as: 'student',}, }";

    TypedAggregation<Course> aggregation = Aggregation.newAggregation(
        Course.class,
        unwind,
        new CustomAggregationOperation(query1)
    );

    AggregationResults<Course> results =
        mongoTemplate.aggregate(aggregation, Course.class);
    System.out.println(results.getMappedResults());
  }
}

For more details, Have a look at the Github repository classes: CustomAggregationOperation & LookupAggregation

Other approaches also using MongoTemplate:

#1. Define an interface for your custom code for Model Post:

interface CustomPostRepository {
     List<Post> yourCustomMethod();
}

#2. Add implementation for this class and follow the naming convention to make sure we can find the class.

class CustomPostRepositoryImpl implements CustomPostRepository {

    @Autowired
    private MongoOperations mongoOperations;

    public List<Post> yourCustomMethod() {

      // custom match queries here
      MatchOperation match = null;
      // Group by , Lookup others stuff goes here
      // For details: https://docs.spring.io/spring-data/mongodb/docs/current/api/org/springframework/data/mongodb/core/aggregation/Aggregation.html

      Aggregation aggregate = Aggregation.newAggregation(match);

      AggregationResults<Post> orderAggregate = mongoOperations.aggregate(aggregate,
                      Post.class, Post.class);
      return orderAggregate.getMappedResults();

    }
}

#3. Now let your base repository interface extend the custom one and the infrastructure will automatically use your custom implementation:

interface PostRepository extends CrudRepository<Post, Long>, CustomPostRepository {

}
krishna Prasad
  • 3,541
  • 1
  • 34
  • 44