Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
gbaso opened this issue Oct 16, 2024 · 1 comment
Labels
theme: 5.0 type: enhancement A general enhancement

Comments

@gbaso
Copy link

gbaso commented Oct 16, 2024

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?

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Oct 16, 2024
@mp911de
Copy link
Member

mp911de commented Oct 16, 2024

Thanks for exploring that path. We generally advocate for our Aggregation Framework API to rather use programmatic guidance as aggregations can become pretty complex with their variety of options.

String-based aggregations with bindings pose two parts: Parsing stages into string and declaring bind-values.

Spring has traditionally provided by-index and by-name bindings.

Taking a step back, we wanted to introduce many more variants of expressing predicates by using e.g. Querydsl. So this is an interesting approach and should not be limited to Aggregations. We just don't know when we would be ready to explore additional variants to form queries.

For the time being it is good that the code lives on your side and once it has proven useful, we can continue that discussion.

@mp911de mp911de added type: enhancement A general enhancement and removed status: waiting-for-triage An issue we've not yet triaged labels Oct 21, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
theme: 5.0 type: enhancement A general enhancement
Projects
None yet
Development

No branches or pull requests

4 participants