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?