在MongoDB中,“$”符号是有特殊意义的,一般用来表示采取一些系统预定义的一些操作,比如比较操作。但是如果在记录文档中的key中出现“$”符号,会怎么样呢?
经测试,在MongoDB的命令行中,使用带“$”符号的key进行数据添加修改和其它聚合操作都没有问题。
Spring Data MongoDB 使用的是org.springframework.data.mongodb.core.aggregation包中的类进行聚合操作,代码如下:
import static org.springframework.data.mongodb.core.aggregation.Aggregation.*;
Aggregation agg = newAggregation(
project("tags"),
unwind("tags"),
group("tags").count().as("n"),
project("n").and("tag").previousOperation(),
sort(DESC, "n")
);
AggregationResults results = mongoTemplate.aggregate(agg, "tags", TagCount.class);
List tagCount = results.getMappedResults();
在使用过程中,如果需要group操作的字段没有包含“$”字符就不会出现问题。如果需要group的字段中包含“$”字符,则只会返回一条“_id”为null的记录,这不是正确的结果。
经调试和查看源码,发现所有聚合操作都使用了Fields.AggregationField去封装文档的key,在初始化的过程中,都会对文档的key执行其中的cleanUp方法,代码如下:
private static final String cleanUp(String source) {
if (source == null) {
return source;
}
if (Aggregation.SystemVariable.isReferingToSystemVariable(source)) {
return source;
}
int dollarIndex = source.lastIndexOf('$');
return dollarIndex == -1 ? source : source.substring(dollarIndex + 1);
}
经过这个方法处理后,所有包含“$”的属性,都变成了“$”后的字符串表示需要操作的key。也就是说,使用Spring Data MongoDB提供的默认聚合操作方案,不能正确处理带“$”的key。
后面对Spring Data MongoDB中聚合操作进一步深挖,发现在构建Aggregation对象时,其参数与Fields.AggregationField无关,只需要实现AggregationOperation接口即可,代码如下:
/**
* Creates a new {@link Aggregation} from the given {@link AggregationOperation}s.
*
* @param operations must not be {@literal null} or empty.
*/
public static Aggregation newAggregation(List extends AggregationOperation> operations) {
return newAggregation(operations.toArray(new AggregationOperation[operations.size()]));
}
/**
* Creates a new {@link Aggregation} from the given {@link AggregationOperation}s.
*
* @param operations must not be {@literal null} or empty.
*/
public static Aggregation newAggregation(AggregationOperation... operations) {
return new Aggregation(operations);
}
而AggregationOperation接口只有一个方法:
public interface AggregationOperation {
/**
* Turns the {@link AggregationOperation} into a {@link DBObject} by using the given
* {@link AggregationOperationContext}.
*
* @return the DBObject
*/
DBObject toDBObject(AggregationOperationContext context);
}
看到这里,那么问题就好解决了,只要实现AggregationOperation接口,并避免使用Fields.AggregationField去处理需要进行聚合的字段就行了。并且AggregationOperation接口中只有一个toDBObject方法,而AggregationOperationContext接口中是有一个getMappedObject方法返回DBObject对象的,代码如下:
public interface AggregationOperationContext {
/**
* Returns the mapped {@link DBObject}, potentially converting the source considering mapping metadata etc.
*
* @param dbObject will never be {@literal null}.
* @return must not be {@literal null}.
*/
DBObject getMappedObject(DBObject dbObject);
/**
* Returns a {@link FieldReference} for the given field or {@literal null} if the context does not expose the given
* field.
*
* @param field must not be {@literal null}.
* @return
*/
FieldReference getReference(Field field);
/**
* Returns the {@link FieldReference} for the field with the given name or {@literal null} if the context does not
* expose a field with the given name.
*
* @param name must not be {@literal null} or empty.
* @return
*/
FieldReference getReference(String name);
}
实现AggregationOperation接口就相当简单了,直接用DBObject就好了,如下:
public class BaseOperation implements AggregationOperation {
private DBObject operation;
public StartGroupOperation(DBObject operation) {
this.operation = operation;
}
@Override
public DBObject toDBObject(AggregationOperationContext context) {
return context.getMappedObject(operation);
}
}
用法如MongoDB命令一样,将相应的聚合操作语句放入DBObject里面,然后构造Aggregation就可以了。
类似的,用Aggregation中的方法不能解决或结果与用MongoDB命令不一致结果的情况,都可以通过上述方法解决。