Skip to content

Add support for building aggregation pipelines from json strings with bindable parameters #4813

@gbaso

Description

@gbaso

I'm currently converting aggregations pipelines from MongoRepository to concrete classes with MongoTemplate, due to #4808, among other issues.

In repositories, you can write pipelines as an array of json strings with placeholders for parameter binding:

@Aggregation(pipeline = {
    """
       {
         $match: {
           name: ?0
         }
       }
       """,
    """
       {
         $count: count
       }
       """
})
long countByName(String name);

This is easy enough to port to an Aggregation to use with MongoTemplate, but it can become quite burdensome for more complex pipelines.

To simplify the process, I have created a couple helper classes, shamelessly copying taking inspiration from StringAggregationOperation (which is not public) combined with BindableMongoExpression:

public class BindableAggregationOperation implements AggregationOperation {

  private static final Pattern OPERATOR_PATTERN = Pattern.compile("\\$\\w+");

  private final Class<?> domainType;
  private final BindableMongoExpression expression;
  private final String operator;

  public BindableAggregationOperation(Class<?> domainType, BindableMongoExpression expression, String operator) {
    this.domainType = domainType;
    this.expression = expression;
    this.operator = operator;
  }

  public static BindableAggregationOperation stage(String json, @Nullable Object... args) {
    return stage(null, json, args);
  }

  public static BindableAggregationOperation stage(@Nullable Class<?> domainType, String json, @Nullable Object... args) {
    json = json.trim(); // remove trailing whitespaces to work more easily with text blocks 
    BindableMongoExpression expression = new BindableMongoExpression(json, args);
    Matcher matcher = OPERATOR_PATTERN.matcher(json);
    String operator = matcher.find() ? matcher.group() : null;
    return new BindableAggregationOperation(domainType, expression, operator);
  }

  public BindableAggregationOperation bind(Object... args) {
    return new BindableAggregationOperation(domainType, expression.bind(args), operator);
  }

  @Override
  public Document toDocument(@Nullable AggregationOperationContext context) {
    return context.getMappedObject(expression.toDocument(), domainType);
  }

  @Override
  public String getOperator() {
    return operator != null ? operator : AggregationOperation.super.getOperator();
  }
}
public class BindableAggregation {

  private final List<BindableAggregationOperation> operations;

  public BindableAggregation(List<BindableAggregationOperation> operations) {
    this.operations = operations;
  }

  public static BindableAggregation newAggregation(String... stages) {
    return newAggregation(null, stages);
  }

  public static BindableAggregation newAggregation(Class<?> domainType, String... stages) {
    List<BindableAggregationOperation> operations = Stream.of(stages)
        .map(stage -> BindableAggregationOperation.stage(domainType, stage))
        .toList();
    return new BindableAggregation(operations);
  }

  public Aggregation bind(Object... args) {
    return Aggregation.newAggregation(operations.stream().map(op -> op.bind(args)).toList());
  }
}

This can be used as follows:

Aggregation aggregation = BindableAggregation.newAggregation(
    """
       {
         $match: {
           name: ?0
         }
       }
       """,
    """
       {
         $count: count
       }
       """)
    .bind(name);
mongoTemplate.aggregate(aggregation, InputType.class, OutputType.class);

And supports type and properties conversion as well as parameter binding.

Is this a feature the Spring Data MongoDB project would be interested in?

Metadata

Metadata

Assignees

No one assigned

    Labels

    theme: 5.1Issues related to the 5.1 releasetype: enhancementA general enhancement

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions