根治Spring中使用Mongo时报错InvalidMongoDbApiUsageException

And Or

很多时候都需要进行逻辑的与或操作,但是spring当中自带的操作并不好用,于是做了相关的改进,首先来看原本的操作:

// or操作
Criteria criteriaA = new Criteria();
criteriaA.orOperator(
        Criteria.where("name").is("wang"),
        Criteria.where("age").is("18")
);

// and操作 ,这里为了图省事,直接用了一样的条件,只有方法不同
Criteria criteriaB = new Criteria();
criteriaB.andOperator( // 这里跟上面不一样,用的是andOperator
        Criteria.where("name").is("wang"),
        Criteria.where("age").is("18")
);

迷惑

这里存在一个问题,就是当我们试图将这两个Criteria合并起来查询的时候,就会报错:

Query query = new Query();
query.addCriteria(criteriaA);
query.addCriteria(criteriaB);
System.out.println(query);

/* 报错如下
org.springframework.data.mongodb.InvalidMongoDbApiUsageException: Due to limitations of the com.mongodb.BasicDocument, you can't add a second 'null' criteria. Query already contains '{ "$or" : [{ "name" : "wang"}, { "age" : "18"}]}'
*/

这就非常迷惑了,报错内容是我们不能添加两个键值为null的criteria,可是我们添加的明明是$or和$and。要解释这个问题要深入解析一下Criteria的构成。

原因

Criteria其中有四个属性

private String key;
private List<Criteria> criteriaChain;
private LinkedHashMap<String, Object> criteria = new LinkedHashMap();
private Object isValue;
  • key 表示键值
  • criteriaChain 是一个链表,存储了其他筛选的条件,当执行criteria.and().is()操作的时候,就会添加在其中
  • criteria 是一个Map,存储了除了is以外的筛选条件,例如gt/ne之类的
  • isValue 存放了criteria.is()方法设置的值
    这里,如果我们采用无参构造方法new Criteria()
public Criteria() {
    this.isValue = NOT_SET;
    this.criteriaChain = new ArrayList();
}

就会造成key值为空null,当我们将两个Criteria合并起来查询的时候,就会报错了。
解决方法
最简单的解决方法,就是只使用一次new Criteria()方法:

Criteria criteriaA = new Criteria();
criteriaA.orOperator(
        Criteria.where("name").is("wang"),
        Criteria.where("age").is("18")
);
criteriaA.andOperator(
        Criteria.where("name").is("wang"),
        Criteria.where("age").is("18")
);
Query query = new Query();
query.addCriteria(criteriaA);
System.out.println(query);

如果是逻辑不复杂的话,那么这样就OK了。但是,这里仍然埋了一个雷,那就是criteriaA的键值仍然为空,很有可能在其他地方报错。让我们冷静分析一下,这里问题的根源是没有一个静态方法可以直接创造一个and/or类型的Criteria对象,OK,下面来继续深入探索一下为什么没有静态方法呢。
我们点开orOperator方法,看看他怎么实现的

public Criteria orOperator(Collection<Criteria> criteria) {
    Assert.notNull(criteria, "Criteria must not be null!");
    BasicDBList bsonList = this.createCriteriaList(criteria);
    return this.registerCriteriaChainElement((new Criteria("$or")).is(bsonList));
}

我们会发现一个大无语事件,这东西就是单纯地将"$or"作为键值重新构造了一个Criteria对象,然后续到了原有criteriaChain的后面,所以从技术上来说完全没有问题啊?开发者就是懒得没写$or/$and的静态方法,那我们自己来写好了:

public class CriteriaUtil{
   
    public static Criteria and(Criteria... criteria){
        return and(Arrays.asList(criteria));
    }
    public static Criteria and(Collection<Criteria> criteria) {
        BasicDBList bsonList = createCriteriaList(criteria);
        return new Criteria("$and").is(bsonList);
    }
    public static Criteria or(Criteria... criteria){
        return or(Arrays.asList(criteria));
    }
    public static Criteria or(Collection<Criteria> criteria){
        BasicDBList bsonList = createCriteriaList(criteria);
        return new Criteria("$or").is(bsonList);
    }
    private static BasicDBList createCriteriaList(Collection<Criteria> criteria) {
        BasicDBList bsonList = new BasicDBList();

        for (Criteria c : criteria) {
            bsonList.add(c.getCriteriaObject());
        }

        return bsonList;
    }
}

顺便一提,上面的createCriteriaList是从源码里面抄过来的,以及源码这个方法没有写成静态的,还写成私有的了

Criteria criteriaA = CriteriaUtil.or(
        Criteria.where("name").is("wang"),
        Criteria.where("age").is("18")
);
Criteria criteriaB = CriteriaUtil.and(
        Criteria.where("name").is("wang"),
        Criteria.where("age").is("18")
);
Query query = new Query();
query.addCriteria(criteriaA);
query.addCriteria(criteriaB);
System.out.println(query);

至此,问题得以优雅地解决。多说一句为什么要采用这么复杂的解决方法呢,因为我的项目当中有很多criteriaA/B/C/D,并且中间代码跨度很大,修改Criteria本身反倒简单。
下面通过反射机制来重写Criteria。

告别InvalidMongoDbApiUsageException

问题

在绝大部分情况下,如果我们对同一个字段设置了不同的条件,我们都是希望这些条件同时要满足,比如一下例子:

Criteria criteria = Criteria.where("A").gt(100);
criteria.and("B").is("b");
... ...

criteria.and("A").lt(200);

首先我们设置了“A>100”的条件,经过一些判断之后,我们又需要添加“A<200”的条件,但这样是会运行报错的
InvalidMongoDbApiUsageException: Due to limitations of the org.bson.Document, you can’t add a second ‘A’ expression specified as ‘A : Document{{KaTeX parse error: Expected 'EOF', got '}' at position 7: lt=200}̲}'. Criteria al…gt=100}}’.

简单解决

最简单的解决办法就是合并两个条件,在代码当中写到一起:

Criteria criteria = Criteria.where("A").gt(100).lt(200); 
criteria.and("B").is("b");
System.out.println(Query.query(criteria));

结果如下:

Query: { "A" : { "$gt" : 100, "$lt" : 200}, "B" : "b"}, Fields: {}, Sort: {}

但这样调整了我们条件的顺序,要求我们必须将对同一字段的筛选条件写在一起,不能分开。这是因为在Spring框架当中使用MongoDB时,对于某个字段的条件筛选只能设置一次,设置完成之后才能设置其他字段的条件。并且,这种情况对于$and/$or也是一样的:
比如两个$or:

Criteria criteria = new Criteria();
criteria.orOperator(
        Criteria.where("A").is("a"),
        Criteria.where("B").is("b")
);
criteria.orOperator(
        Criteria.where("C").is("c"),
        Criteria.where("D").is("d")
);
System.out.println(Query.query(criteria));

对于这种问题的简单解决办法是用and将两个or操作包起来:

Criteria criteriaA = new Criteria();
criteriaA.orOperator(
        Criteria.where("A").is("a"),
        Criteria.where("B").is("b")
);

Criteria criteriaB = new Criteria();
criteriaB.orOperator(
        Criteria.where("C").is("c"),
        Criteria.where("D").is("d")
);
Criteria criteria = new Criteria();
criteria.andOperator(criteriaA,criteriaB);
System.out.println(Query.query(criteria));

根本解决

简单的解决办法其实就是把不同的条件写到一起,或者用and包起来。但实际情况当中,我们很可能需要在已经生成的Criteria上面继续添加条件,这时如果之前对某个字段设置过条件,就无法再次添加条件了,或者将二者用很多$and包起来。为了优雅的处理这种情况,我们需要深入源码来探究一下。
Criteria对象是通过getCriteriaObject方法转化为Document的,来生成MongoDB当中可执行的语句:

public Document getCriteriaObject() {
    if (this.criteriaChain.size() == 1) {
        return ((Criteria)this.criteriaChain.get(0)).getSingleCriteriaObject();
    } else if (CollectionUtils.isEmpty(this.criteriaChain) && !CollectionUtils.isEmpty(this.criteria)) {
        return this.getSingleCriteriaObject();
    } else {
        Document criteriaObject = new Document();
        Iterator var2 = this.criteriaChain.iterator();

        while(var2.hasNext()) {
            Criteria c = (Criteria)var2.next();
            Document document = c.getSingleCriteriaObject();
            Iterator var5 = document.keySet().iterator();

            while(var5.hasNext()) {
                String k = (String)var5.next();
                this.setValue(criteriaObject, k, document.get(k));
            }
        }

        return criteriaObject;
    }
}

上面这个函数不用细看,其中的setValue方法是抛出异常的关键:

private void setValue(Document document, String key, Object value) {
    Object existing = document.get(key);
    if (existing == null) {
        document.put(key, value);
    } else {
        throw new InvalidMongoDbApiUsageException("Due to limitations of the org.bson.Document, you can't add a second '" + key + "' expression specified as '" + key + " : " + value + "'. Criteria already contains '" + key + " : " + existing + "'.");
    }
}

逻辑非常简单了,首先判断Document当中有没有key值,没有的话就插入<key,value>,有的话就报错。那么解决的方法也就有了,如果往Document中插入时起了冲突,那么就用$and将二者进行合并。

修改源码

修改完成的setValue函数如下:

private static void setValue(Document document, String key, Object value) {
    Object existing = document.get(key);
    if (existing == null) {
        document.put(key, value);
    } else {
        if(key.equals("$and")){
            if(value.getClass() != BasicDBList.class || existing.getClass() != BasicDBList.class){
                throw new InvalidMongoDbApiUsageException("error: $and meet unknown type "+value.getClass()+" "+existing.getClass());
            }
            BasicDBList basicDBList = new BasicDBList();
            basicDBList.addAll((BasicDBList) existing);
            basicDBList.addAll((BasicDBList) value);
            document.put(key,basicDBList);
        }else{
            document.remove(key);
            Document left = new Document(key,existing);
            Document right = new Document(key,value);
            BasicDBList basicDBList = new BasicDBList();
            basicDBList.add(left);
            basicDBList.add(right);
            setValue(document,"$and",basicDBList);
        }
    }
}

但是我们不能直接修改包当中的源码,这里我采用的方法是新建一个类CriteriaSub,并拷贝了Criteria的部分方法,用Java的反射机制将Criteria中的私有变量读取了出来。最终实现了将Criteria生成Document的方法,并保证不会报错InvalidMongoDbApiUsageException,然后新写一个方法将Document再次还原成Criteria以便后续使用。

代码(省流,可以直接看这里)

最终所有代码如下,建立一个工具类CriteriaUtil
对于原本出错的语句,加上这一行即可

criteria = CriteriaUtil.reform(criteria);
import com.mongodb.BasicDBList;
import lombok.extern.slf4j.Slf4j;
import org.bson.Document;
import org.springframework.data.mongodb.InvalidMongoDbApiUsageException;
import org.springframework.data.mongodb.core.geo.GeoJson;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.GeoCommand;
import org.springframework.lang.Nullable;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

import java.lang.reflect.Field;
import java.util.*;

/**
 * @Description Criteria重构
 */
public class CriteriaUtil{
    private static BasicDBList createCriteriaList(Collection<Criteria> criteria) {
        BasicDBList bsonList = new BasicDBList();

        for (Criteria c : criteria) {
            bsonList.add(c.getCriteriaObject());
        }

        return bsonList;
    }
    public static Criteria and(Criteria... criteria){
        return and(Arrays.asList(criteria));
    }
    public static Criteria and(Collection<Criteria> criteria) {
        BasicDBList bsonList = createCriteriaList(criteria);
        return new Criteria("$and").is(bsonList);
    }
    public static Criteria or(Criteria... criteria){
        return or(Arrays.asList(criteria));
    }
    public static Criteria or(Collection<Criteria> criteria){
        BasicDBList bsonList = createCriteriaList(criteria);
        return new Criteria("$or").is(bsonList);
    }

    /**
     * @Description: 重组criteria,解决InvalidMongoDbApiUsageException
     */
    public static Criteria reform(Criteria criteria){
        try {
            criteria.getCriteriaObject();
            return criteria;
        } catch (InvalidMongoDbApiUsageException e) {
            Document document = new CriteriaSub(criteria).getCriteriaObject();
            return from(document);
        }
    }

    /**
     * @Description: Document 转化成 Criteria
     */
    public static Criteria from(Document document) {
        Criteria c = new Criteria();

        try {

            Field _criteria = c.getClass().getDeclaredField("criteria");
            _criteria.setAccessible(true);

            @SuppressWarnings("unchecked")
            LinkedHashMap<String, Object> criteria = (LinkedHashMap<String, Object>) _criteria.get(c);

            for (Map.Entry<String, Object> set : document.entrySet()) {
                criteria.put(set.getKey(), set.getValue());
            }

            Field _criteriaChain = c.getClass().getDeclaredField("criteriaChain");
            _criteriaChain.setAccessible(true);

            @SuppressWarnings("unchecked")
            List<Criteria> criteriaChain = (List<Criteria>) _criteriaChain.get(c);
            criteriaChain.add(c);

        } catch (Exception e) {
            // Ignore
        }

        return c;
    }
    private static class CriteriaSub {
        private static Object NOT_SET = new Object();
        private static final int[] FLAG_LOOKUP = new int['\uffff'];
        @Nullable
        private String key;
        private List<Criteria> criteriaChain;
        private LinkedHashMap<String, Object> criteria = new LinkedHashMap();
        @Nullable
        private Object isValue;

        public CriteriaSub(Criteria criteria){
            Class<?> clazz = criteria.getClass();
            Field field ;
            try {
                field = clazz.getDeclaredField("NOT_SET");
                field.setAccessible(true);//压制java检查机制
                NOT_SET = field.get(criteria);

                field = clazz.getDeclaredField("key");
                field.setAccessible(true);//压制java检查机制
                key = (String) field.get(criteria);

                field = clazz.getDeclaredField("criteriaChain");
                field.setAccessible(true);//压制java检查机制
                criteriaChain = (List<Criteria>) field.get(criteria);

                field = clazz.getDeclaredField("criteria");
                field.setAccessible(true);//压制java检查机制
                this.criteria = (LinkedHashMap<String, Object>) field.get(criteria);

                field = clazz.getDeclaredField("isValue");
                field.setAccessible(true);//压制java检查机制
                isValue = field.get(criteria);
            } catch (NoSuchFieldException | IllegalAccessException e) {
                // ignore
            }
        }

        public Document getCriteriaObject() {
            if (this.criteriaChain.size() == 1) {
                return new CriteriaSub(this.criteriaChain.get(0)).getSingleCriteriaObject();
            } else if (CollectionUtils.isEmpty(this.criteriaChain) && !CollectionUtils.isEmpty(this.criteria)) {
                return this.getSingleCriteriaObject();
            } else {
                Document criteriaObject = new Document();

                for (Criteria value : this.criteriaChain) {
                    CriteriaSub c = new CriteriaSub(value);
                    Document document = c.getSingleCriteriaObject();

                    for (String k : document.keySet()) {
                        setValue(criteriaObject, k, document.get(k));
                    }
                }

                return criteriaObject;
            }
        }

        protected Document getSingleCriteriaObject() {
            Document document = new Document();
            boolean not = false;

            for (Map.Entry<String, Object> stringObjectEntry : this.criteria.entrySet()) {
                String key = stringObjectEntry.getKey();
                Object value = stringObjectEntry.getValue();
                if (requiresGeoJsonFormat(value)) {
                    value = new Document("$geometry", value);
                }

                if (not) {
                    Document notDocument = new Document();
                    notDocument.put(key, value);
                    document.put("$not", notDocument);
                    not = false;
                } else if ("$not".equals(key) && value == null) {
                    not = true;
                } else {
                    document.put(key, value);
                }
            }

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

                return document;
            }

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

            return queryCriteria;
        }

        private static boolean requiresGeoJsonFormat(Object value) {
            return value instanceof GeoJson || value instanceof GeoCommand && ((GeoCommand)value).getShape() instanceof GeoJson;
        }


        private static void setValue(Document document, String key, Object value) {
            Object existing = document.get(key);
            if (existing == null) {
                document.put(key, value);
            } else {
                if(key.equals("$and")){
                    System.out.println("merge $and "+value+" "+ existing);
                    if(value.getClass() != BasicDBList.class || existing.getClass() != BasicDBList.class){
                        throw new InvalidMongoDbApiUsageException("error: $and meet unknown type "+value.getClass()+" "+existing.getClass());
                    }
                    BasicDBList basicDBList = new BasicDBList();
                    basicDBList.addAll((BasicDBList) existing);
                    basicDBList.addAll((BasicDBList) value);
                    document.put(key,basicDBList);
                }else{
                    System.out.println("merge "+key+" "+value+" "+ existing);
                    document.remove(key);
                    Document left = new Document(key,existing);
                    Document right = new Document(key,value);
                    BasicDBList basicDBList = new BasicDBList();
                    basicDBList.add(left);
                    basicDBList.add(right);
                    setValue(document,"$and",basicDBList);
//                  throw new InvalidMongoDbApiUsageException("Due to limitations of the org.bson.Document, you can't add a second '" + key + "' expression specified as '" + key + " : " + value + "'. Criteria already contains '" + key + " : " + existing + "'.");
                }
            }
        }
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值