前言
在MongoDB中,“$”符號是有特殊意義的,一般用來表示採取一些系統預定義的一些操作,比如比較操作。但是如果在記錄文檔中的key中出現“$”符號,會怎麼樣呢?
MongoDB的方案
經測試,在MongoDB的命令行中,使用帶“$”符號的key進行數據添加修改和其它聚合操作都沒有問題。
Spring Data MongoDB 聚合的使用
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<TagCount> results = mongoTemplate.aggregate(agg, "tags", TagCount.class);
List<TagCount> 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命令不一致結果的情況,都可以通過上述方法解決。