-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Description
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?
Activity
mp911de commentedon 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.