关于Spring中MongoTemplate.aggregate的一个奇异bug

Posted Derrick_gu

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了关于Spring中MongoTemplate.aggregate的一个奇异bug相关的知识,希望对你有一定的参考价值。

昨天在完成公司一个项目的时候用了mongoTemplate的aggregate,在使用Aggretaion.match(criteria)来筛选数据,其中criteria语句是Criteria.where("id").ne(xxxId),结果程序在执行的时候该条件一直没有起效果,但是其他的find和update等语句都是可以执行的,抱着满脑袋的疑惑翻看了它的源码实现后发现这里有一个很大的坑,具体是这个样子的:

在其他语句中,criteria查询条件最终都会包到Query对象里面,而template驱动在对Query进行解析的时候,以updateFirst为例,它的解析过程是这个样子的:

public WriteResult updateFirst(Query query, Update update, Class<?> entityClass, String collectionName) 
   return doUpdate(collectionName, query, update, entityClass, false, false);

首先经过updateFirst方法,然后调用doUpdate:

protected WriteResult doUpdate(final String collectionName, final Query query, final Update update,
      final Class<?> entityClass, final boolean upsert, final boolean multi) 

   return execute(collectionName, new CollectionCallback<WriteResult>() 
      public WriteResult doInCollection(DBCollection collection) throws MongoException, DataAccessException 

         MongoPersistentEntity<?> entity = entityClass == null ? null : getPersistentEntity(entityClass);

         increaseVersionForUpdateIfNecessary(entity, update);

         DBObject queryObj = query == null ? new BasicDBObject() : queryMapper.getMappedObject(query.getQueryObject(),
               entity);
         DBObject updateObj = update == null ? new BasicDBObject() : updateMapper.getMappedObject(
               update.getUpdateObject(), entity);

         if (LOGGER.isDebugEnabled()) 
            LOGGER.debug("Calling update using query: " + queryObj + " and update: " + updateObj + " in collection: "
                  + collectionName);
         

         MongoAction mongoAction = new MongoAction(writeConcern, MongoActionOperation.UPDATE, collectionName,
               entityClass, updateObj, queryObj);
         WriteConcern writeConcernToUse = prepareWriteConcern(mongoAction);
         WriteResult writeResult = writeConcernToUse == null ? collection.update(queryObj, updateObj, upsert, multi)
               : collection.update(queryObj, updateObj, upsert, multi, writeConcernToUse);

         if (entity != null && entity.hasVersionProperty() && !multi) 
            if (writeResult.getN() == 0 && dbObjectContainsVersionProperty(queryObj, entity)) 
               throw new OptimisticLockingFailureException("Optimistic lock exception on saving entity: "
                     + updateObj.toMap().toString() + " to collection " + collectionName);
            
         

         handleAnyWriteResultErrors(writeResult, queryObj, MongoActionOperation.UPDATE);
         return writeResult;
      
   );

在这一步中,它对query的解析是这句:

DBObject queryObj = query == null ? new BasicDBObject() : queryMapper.getMappedObject(query.getQueryObject(),

我们跟入getMappedObject方法中,它的实现是:

public DBObject getMappedObject(DBObject query, MongoPersistentEntity<?> entity) 

   if (isNestedKeyword(query)) 
      return getMappedKeyword(new Keyword(query), entity);
   

   DBObject result = new BasicDBObject();

   for (String key : query.keySet()) 

      // TODO: remove one once QueryMapper can work with Query instances directly
      if (Query.isRestrictedTypeKey(key)) 

         @SuppressWarnings("unchecked")
         Set<Class<?>> restrictedTypes = (Set<Class<?>>) query.get(key);
         this.converter.getTypeMapper().writeTypeRestrictions(result, restrictedTypes);

         continue;
      

      if (isKeyword(key)) 
         result.putAll(getMappedKeyword(new Keyword(query, key), entity));
         continue;
      

      Field field = createPropertyField(entity, key, mappingContext);
      Entry<String, Object> entry = getMappedObjectForField(field, query.get(key));

      result.put(entry.getKey(), entry.getValue());
   

   return result;

在一个个地翻看每一条语句的源码之后,发现其对key进行解析的时候,getMappedObjectForfield方法负责对每一个约束key和查询值进行一一对应转换,具体实现是:

protected Entry<String, Object> getMappedObjectForField(Field field, Object rawValue) 

   String key = field.getMappedKey();
   Object value;

   if (isNestedKeyword(rawValue) && !field.isIdField()) 
      Keyword keyword = new Keyword((DBObject) rawValue);
      value = getMappedKeyword(field, keyword);
    else 
      value = getMappedValue(field, rawValue);
   

   return createMapEntry(key, value);

到了这儿的时候,其中一个条件引起了我的注意,就是field.isIdField,难道是这里对id进行了转换?

进一步跟进之后发现:

public boolean isIdField() 
   return ID_KEY.equals(name);
private static final String ID_KEY = "_id";

原来其在此处对_id进行了特殊处理,既然有做特殊处理,那就得看看是怎么特殊处理的了,于是进一步跟进getMappedKeyword方法,发现其中没有进一步特殊处理,于是返回上一个方法,从第一个方法跟进,打开getMappedKey:

public String getMappedKey() 
   return isIdField() ? ID_KEY : name;

原来在这里进行了特殊处理,随后进行了进一步查看之后发现template在Aggregation.match中没有对此进行转换,结果导致mongo查询条件一直没有正常工作,没办法,只能自行处理了,于是查询条件改为Criteria.where("_id").ne(new ObjectId(xxxId)),至此搞定。这个坑埋得有点深啊。

以上是关于关于Spring中MongoTemplate.aggregate的一个奇异bug的主要内容,如果未能解决你的问题,请参考以下文章

关于spring配置文件中username取值的问题

关于Spring MVC分页

关于加密密码的 Spring Security 问题

关于 Spring bean 容器中的作用域和垃圾回收

关于 Spring 中 getBean 的全流程源码解析

关于spring boot java中实体的问题