saywhatman saywhatman - 1 month ago 7
Java Question

How do I nest a field within an Aggregation projection field in Spring Data Mongo

When written by hand, the

$project
step in my aggregation pipeline looks like:

{
"$project":{
"DRIVE":{
"componentSummary":{"manufacturer" : "$_id.DRIVE_manufacturer"},
"componentCount":"$_id.DRIVE_componentCount"
},
"hostnames":1,
"_id":0
}
}


I understand that I can use the
ProjectionOperationBulder
to create a single level of nesting (using builder.nested), to make something like, say:

{
"$project":{
"DRIVE":{
"manufacturer":"$_id.DRIVE_manufacturer"
},
"hostnames":1,
"_id":0
}
}


But I can't seem to figure out how to nest another level deep, as the
Field
interface only allows for a String name and a String target, rather than being able to define antother
Field
as the target.

Thanks!

Answer

For anyone else struggling with this -- Spring Data Mongo does not natively support multi level nesting as of this writing (stable version 1.9.5). However, as of 1.9.3, it does support custom AggregationExpressions that allow you to define the behavior yourself. Be aware that if you go down this route, you'll have to build the JSON for the query mostly by hand. My implementation is pretty quick and dirty but here it is for reference's sake.

      protected class NestedField implements Field {

private String name;
private List<Field> fields;

public NestedField(String name, List<Field> fields) {
  this.name = name;
  this.fields = fields;
}

public List<Field> getFields() {
  return fields;
}

@Override
public String getName() {
  return name;
}

private String escapeSystemVariables(String fieldTarget) {
  if (fieldTarget.startsWith("_id")) {
    return StringUtils.prependIfMissing(fieldTarget, "$");
  } else {
    return fieldTarget;
  }
}

private String encloseStringInQuotations(String quotable) {
  return JSON.serialize(quotable);
}

private String buildSingleFieldTarget(Field field) {
  if (field instanceof NestedField) {
    return String.join(":", encloseStringInQuotations(field.getName()), field.getTarget());
  }
  return String.join(":", encloseStringInQuotations(field.getName()), encloseStringInQuotations(escapeSystemVariables(
    field.getTarget())));
}

private String buildFieldTargetList(List<Field> fields) {
  List<String> fieldStrings = new ArrayList<>();
  fields.forEach(field -> {
    fieldStrings.add(buildSingleFieldTarget(field));
  });
  return Joiner.on(",").skipNulls().join(fieldStrings);
}

@Override
public String getTarget() {
  // TODO Auto-generated method stub
  return String.format("{%s}", buildFieldTargetList(fields));
}


@Override
public boolean isAliased() {
  return true;
}

}

    protected class NestedProjection implements AggregationExpression {

private List<Field> projectedFields;

public NestedProjection(List<Field> projectedFields) {
  this.projectedFields = projectedFields;

}@Override
public DBObject toDbObject(AggregationOperationContext context) {
  DBObject projectionExpression = new BasicDBObject();
  for(Field f : projectedFields) {
    //this is necessary because if we just put f.getTarget(), spring-mongo will attempt to JSON-escape the string
    DBObject target = (DBObject) com.mongodb.util.JSON.parse(f.getTarget());
      projectionExpression.put(f.getName(), target);


  }
  return projectionExpression;
}

}

Comments