1. 程式人生 > >對於springframework的mongoTemplate擴充套件自定義的分享

對於springframework的mongoTemplate擴充套件自定義的分享

之前對於spring的mongoTemplate真的是有點又愛又恨,由於它對mongodb的驅動做了一層封裝,使得在開發的時候方便了許多,但是它的語法和mongo的原生js有很大不同,有時候在mongo官方文件裡的API介面很多時候在mongoTemplate中的使用完全不一樣,導致有些時候用的很彆扭,而且一些語句完全不知道怎麼去轉換為template的語法。不過最近的兩次使用經歷使得我對mongoTemplate有了一些改觀。

第一個就是mongoTemplate自身的criteria.where沒有>和<的操作,也就是對mongo的一條記錄自身的兩個欄位進行比較。mongo語句如下:

db.whereColl.find({$where: "this.b > this.a"})

當時研究和許久,最後實在沒招了,扒原始碼找出了它的Criteria實現,下面是它的原始碼實現:

/**
 * Creates a criterion using the {@literal $elemMatch} operator
 * 
 * @see http://docs.mongodb.org/manual/reference/operator/query/elemMatch/
 * @param c
* @return
*/
public Criteria elemMatch(Criteria c) {
   criteria.put("$elemMatch", c.getCriteriaObject());
return this; }
/**
 * Creates an 'and' criteria using the $and operator for all of the provided criteria.
 * <p>
* Note that mongodb doesn't support an $and operator to be wrapped in a $not operator.
 * <p>
* 
 * @throws IllegalArgumentException if {@link #andOperator(Criteria...)} follows a not() call directly.
* @param criteria */ public Criteria andOperator(Criteria... criteria) { BasicDBList bsonList = createCriteriaList(criteria); return registerCriteriaChainElement(new Criteria("$and").is(bsonList)); }
private BasicDBList createCriteriaList(Criteria[] criteria) {
   BasicDBList bsonList = new BasicDBList();
   for (Criteria c : criteria) {
      bsonList.add(c.getCriteriaObject());
}
   return bsonList;
}
public DBObject getCriteriaObject() {

   if (this.criteriaChain.size() == 1) {
      return criteriaChain.get(0).getSingleCriteriaObject();
} else if (CollectionUtils.isEmpty(this.criteriaChain) && !CollectionUtils.isEmpty(this.criteria)) {
      return getSingleCriteriaObject();
} else {
      DBObject criteriaObject = new BasicDBObject();
      for (Criteria c : this.criteriaChain) {
         DBObject dbo = c.getSingleCriteriaObject();
         for (String k : dbo.keySet()) {
            setValue(criteriaObject, k, dbo.get(k));
}
      }
      return criteriaObject;
}
}
protected DBObject getSingleCriteriaObject() {

   DBObject dbo = new BasicDBObject();
   boolean not = false;
   for (String k : this.criteria.keySet()) {
      Object value = this.criteria.get(k);
      if (not) {
         DBObject notDbo = new BasicDBObject();
notDbo.put(k, value);
dbo.put("$not", notDbo);
not = false;
} else {
         if ("$not".equals(k) && value == null) {
            not = true;
} else {
            dbo.put(k, value);
}
      }
   }

   if (!StringUtils.hasText(this.key)) {
      if (not) {
         return new BasicDBObject("$not", dbo);
}
      return dbo;
}

   DBObject queryCriteria = new BasicDBObject();
   if (!NOT_SET.equals(isValue)) {
      queryCriteria.put(this.key, this.isValue);
queryCriteria.putAll(dbo);
} else {
      queryCriteria.put(this.key, dbo);
}

   return queryCriteria;
}
private void setValue(DBObject dbo, String key, Object value) {
   Object existing = dbo.get(key);
   if (existing == null) {
      dbo.put(key, value);
} else {
      throw new InvalidMongoDbApiUsageException("Due to limitations of the com.mongodb.BasicDBObject, "
+ "you can't add a second '" + key + "' expression specified as '" + key + " : " + value + "'. "
+ "Criteria already contains '" + key + " : " + existing + "'.");
}
}
通過上面的原始碼可以知道,mongoTemplate的語法轉換是通過構建一個DBObject,然後將查詢語法轉換為mongo的Java驅動的語法,也是接近於原生的語法。

知道了這個,稍微對com.mongodb包有些經驗的都應該知道要怎麼去操作了,我們通過複寫getCriteriaObject方法來實現自定義查詢語句,突破mongoTemplate的限制,我的實現如下:

    public ListResponse<AObject> loadAObjectList(String aId, String bId, int start, int limit) {
        ListResponse<AObject> AObjectListResp = new ListResponse<AObject>();
        Criteria criteria = new Criteria() {
            @Override
            public DBObject getCriteriaObject() {
                DBObject obj = new BasicDBObject();
                obj.put("$where", "this.groupNum > this.joinedNum");
                return obj;
            }
        };
        Query query = Query.query(criteria);
        query.addCriteria(Criteria.where("members").nin(bId).and("aId").is(aId)).with(new Sort(Sort.Direction.DESC, "gmtCreated")).skip((start-1) * limit).limit(limit);
        List<AObject> AObjectList = template.find(query, AObject.class);
        long count = template.count(new Query(criteria).addCriteria(.where("members").nin(bId).and("aId").is(aId)), AObject.class);
        return AObjectListResp.fill(ResponseCode.SUCCESS, "success", AObjectList, count, count > start * limit);
    }

上面我們通過複寫相關方法來自定義Criteria語法,而另一個事件是今天的一個專案,需要從mongo中隨機取出一定數量的文件,我們知道mongo在3.2版本之後對此加入了一個官方的api,我們可以使用aggregate管道的$sample來讀取檔案,mongo語法如下:
db.users.aggregate(
   [ { $sample: { size: 3 } } ]
)
我看了mongotemple的aggregate相關API,發現它的API只有以下幾個,即便是升級到最新的1.10.0-RELEASE版本也是如此,最後我想起上面的自定義擴充套件,於是,又去吭哧吭哧地翻看原始碼,首先:
template.aggregate(Aggregation.newAggregation(Aggregation.match(Criteria),.....),....);
我發現它的語法條件都是通過Aggregation.XXX來操作的,於是,我開啟Aggregation.XXX的原始碼:
/**
 * Creates a new {@link MatchOperation} using the given {@link Criteria}.
 * 
 * @param criteria must not be {@literal null}.
 * @return
*/
public static MatchOperation match(Criteria criteria) {
   return new MatchOperation(criteria);
}
/**
 * Creates a new {@link LimitOperation} limiting the result to the given number of elements.
 * 
 * @param maxElements must not be less than zero.
 * @return
*/
public static LimitOperation limit(long maxElements) {
   return new LimitOperation(maxElements);
}
我發現它是構建不同的XXXOperation,於是,我開啟MatchOperation和LimitOperation:
public class MatchOperation implements AggregationOperation {

   private final CriteriaDefinition criteriaDefinition;
/**
    * Creates a new {@link MatchOperation} for the given {@link CriteriaDefinition}.
    * 
    * @param criteriaDefinition must not be {@literal null}.
    */
public MatchOperation(CriteriaDefinition criteriaDefinition) {

      Assert.notNull(criteriaDefinition, "Criteria must not be null!");
      this.criteriaDefinition = criteriaDefinition;
}

   /* 
    * (non-Javadoc)
    * @see org.springframework.data.mongodb.core.aggregation.AggregationOperation#toDBObject(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext)
    */
@Override
public DBObject toDBObject(AggregationOperationContext context) {
      return new BasicDBObject("$match", context.getMappedObject(criteriaDefinition.getCriteriaObject()));
}
}
public class LimitOperation implements AggregationOperation {

   private final long maxElements;
/**
    * @param maxElements Number of documents to consider.
    */
public LimitOperation(long maxElements) {

      Assert.isTrue(maxElements >= 0, "Maximum number of elements must be greater or equal to zero!");
      this.maxElements = maxElements;
}

   /* 
    * (non-Javadoc)
    * @see org.springframework.data.mongodb.core.aggregation.AggregationOperation#toDBObject(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext)
    */
@Override
public DBObject toDBObject(AggregationOperationContext context) {
      return new BasicDBObject("$limit", maxElements);
}
}
到這兒,我發現最終,它的操作也是落到了BasicDBObject上面,並且都是通過繼承AggregationOperation來實現的,於是我照著葫蘆畫瓢,自己自定義了一個SampleOperation類:
public class SampleOperation implements AggregationOperation {


    private final long maxElements;

    /**
     * @param maxElements Number of documents to consider.
     */
    public SampleOperation(long maxElements) {

        Assert.isTrue(maxElements >= 0, "Maximum number of elements must be greater or equal to zero!");
        this.maxElements = maxElements;
    }

    /*
    * (non-Javadoc)
    * @see org.springframework.data.mongodb.core.aggregation.AggregationOperation#toDBObject(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext)
    */
    @Override
    public DBObject toDBObject(AggregationOperationContext context) {
        return new BasicDBObject("$sample",  new BasicDBObject("size", maxElements));
    }
}
通過這兩次經歷,我發現mongoTemplate雖然語法上和原生的語法有些不同之處,但是它在設計之初充分地考慮到了開發的自定義擴充套件。

上面是我個人的一些小小的心(慘)得(通)經(教)驗(訓),希望能夠對大家有些幫助。