mongodbspring-boot

mongodb aggregate a hierarchy to string in spring boot


Is there any simple solution to build a directiry path string from a hierarchy with aggregation in SpringBoot and MongoDB?

Directory.java looks like

@Getter
@Setter
@EqualsAndHashCode
@Document
public class Directory {
    @Id
    private String id;
    private String path;
    private Date createdAt;
    @DBRef
    private Directory parentDirectory;
}

(I use lombok)

The code that I tried is this:

public String buildDirectoryHierarchy(Directory directory) {
        GraphLookupOperation graphLookup = Aggregation.graphLookup("directory")
                .startWith("$_id")
                .connectFrom("parentDirectory")
                .connectTo("_id")
                .as("parents");

        ProjectionOperation projectionOperation = Aggregation.project()
                .andExpression("'$parents'").as("parents");

        Aggregation aggregation = Aggregation.newAggregation(
                Aggregation.match(Criteria.where("_id").is(directory.getId())),
                graphLookup,
                projectionOperation,
                Aggregation.project()
                        .andExpression(
                                "{$reduce: { input: '$parents', initialValue: '', in: {$concat: ['$$value', '/', '$$this.path'] } } }")
                        .as("fullPath"));
        AggregationResults<Map> results = mongoTemplate.aggregate(aggregation, "directory", Map.class);

        if (results.getMappedResults().isEmpty()) {
            return "/";
        }
        return (String) results.getMappedResults().get(0).get("fullPath");
    }

But I get the following exception:

Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception 
[Request processing failed: org.springframework.expression.spel.SpelParseException: 

Expression [{$reduce: { input: '$parents', initialValue: '', in: {$concat: ['$$value', '/', '$$this.path'] } } }] @73: EL1043E: Unexpected token. Expected 'rsquare(])' but was 'comma(,)'] with root cause

I'm not perfectly understand the code. Thanks for suggestions!


Solution

  • I created a working code:

        public String buildDirectoryHierarchy(Directory directory) {
            MatchOperation matchStage = Aggregation.match(Criteria.where("_id").is(directory.getId()));
    
            GraphLookupOperation graphLookupStage = Aggregation.graphLookup("directory")
                    .startWith("$parentDirectory.$id")
                    .connectFrom("parentDirectory.$id")
                    .connectTo("_id")
                    .depthField("level")
                    .as("parents");
    
            Sort sortByLevelDesc = Sort.by(Sort.Direction.DESC, "level");
            SortArray sortArrayParents = SortArray.sortArray("$parents").by(sortByLevelDesc);
            AddFieldsOperation addFieldsStage = Aggregation.addFields().addField("parents").withValue(sortArrayParents).build();
    
            Cond reduceSeparatorCond = Cond.when(Eq.valueOf("$$value").equalToValue(""))
                                                                .then("")
                                                                .otherwise("/");
            Concat reduceConcatExpr = Concat.valueOf("$$value")
                                                            .concatValueOf(reduceSeparatorCond)
                                                            .concat("$$this");
            Reduce parentPathReduceExpr = Reduce.arrayOf("$parents.path")
                                                                    .withInitialValue("")
                                                                    .reduce(reduceConcatExpr);
    
            ExpressionVariable parentPathVar = ExpressionVariable.newVariable("parentPathCalc").forExpression(parentPathReduceExpr);
    
            ConditionalOperators.Cond finalSeparatorCond = Cond.when(Eq.valueOf("$$parentPathCalc").equalToValue(""))
                                                               .then("") // No separator if parent path is empty
                                                               .otherwise("/");
    
            StringOperators.Concat finalConcatExpr = Concat.valueOf("$$parentPathCalc")
                                                            .concatValueOf(finalSeparatorCond)
                                                            .concatValueOf("$path");
    
            Let letExpr = Let.define(parentPathVar).andApply(finalConcatExpr);
    
            ProjectionOperation projectStage = Aggregation.project()
                    .andExclude("_id")
                    .and(letExpr).as("fullPath");
    
    
            Aggregation aggregation = Aggregation.newAggregation(
                    matchStage,
                    graphLookupStage,
                    addFieldsStage,
                    projectStage 
            );
    
            AggregationResults<FullPathResult> results = mongoTemplate.aggregate(
                    aggregation, "directory", FullPathResult.class
            );
    
            FullPathResult uniqueResult = results.getUniqueMappedResult();
    
            return Optional.ofNullable(uniqueResult)
                    .map(FullPathResult::getFullPath)
                    .orElse(null);
        }