Skip to content

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

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

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions