MongoDB JavaApi 简单包装成sql查询

2019-12-14  本文已影响0人  virct

背景

最近项目经理让我着手做一些报表,前台基于React+echarts+antd ,后台spring boot,报表差不多有七八个,而且前后台我一个人写,心累。
要制图的数据都是存储在MongoDB,而MongoDB查询又不支持传统的sql方式查询。

例如 一个简单的查询语句

select d , sum(f) from a where b = 'xx' and c = 'xx' group by d order by e

写成MongoDB的文档查询方式的话就是下面这样

db.getCollection("a").aggregate(
    [
        { 
            "$match" : {
                "b" : "xx", 
                "c" : "xx"
            }
        }, 
        { 
            "$group" : {
                "_id" : {
                    "d" : "$d"
                }, 
                "SUM(f)" : {
                    "$sum" : "$f"
                }
            }
        }, 
        { 
            "$project" : {
                "d" : "$_id.d", 
                "SUM(f)" : "$SUM(f)", 
                "e" : "$_id.e", 
                "_id" : 0
            }
        }, 
        { 
            "$sort" : {
                "e" : 1
            }
        }, 
        { 
            "$project" : {
                "_id" : 0, 
                "d" : "$d", 
                "SUM(f)" : "$SUM(f)"
            }
        }
    ]
);

而项目中的查询只可能比这个复杂(条件更多),因此每个报表的查询单纯写查询语句都要琢磨半天,因此简单封装了下。满足我当前需要的一些查询操作。

思路

采用类Builder模式的方式构建整个查询语句如 Builder<Type>().select(field).from(table).where(condition).groupBy(field).orderBy(another).build();

要注意MongoDB聚合操作中pipeline执行的顺序,它是依据你构建的查询文档的顺序执行的,也就是说如果你的match在group by 操作之后的话,他是会先取执行group by操作再执行match操作,因此会出现意想不到的效果。

因此采用一个TreeMap存储查询语句,排序各个操作的优先级,最终转换成pipeline,保证与想要执行的操作顺序一致。

代码

package com.xxx.risk.report.util.mongo;

import com.google.common.base.Strings;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoDatabase;
import org.bson.Document;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.Arrays;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.xxx.risk.report.common.Dict.MongoSupp.AGG;
import static com.xxx.risk.report.common.Dict.MongoSupp._ID;
import static com.xxx.risk.report.common.Dict.Symbol.USD;
import static com.xxx.risk.report.util.mongo.Action.*;

@Component
public class MongoUtil {

    private final Pattern pat = Pattern.compile("^(sum|avg)\\((\\w+)\\)");

    @Autowired
    private MongoDatabase database;

    private final Logger log = LoggerFactory.getLogger(this.getClass());

    public MongoCollection<Document> getCollection(String collectionName) {
        return database.getCollection(collectionName);
    }

    public <T> MongoCollection<T> getCollection(String collectionName, Class<T> type) {
        return database.getCollection(collectionName, type);
    }

    public static String sum(String field){
        checkNotNull(field,"to sum field cannot be null");
        return "sum("+field+")";
    }

    public static String avg(String field){
        checkNotNull(field,"to avg field cannot be null");
        return "avg("+field+")";
    }


    public <T> Querys<T> Querys(){
        return new Querys<>();
    }

    public class Querys<T>{
        private TreeMap<Action,Object> sql = new TreeMap<>();//use default compare
        private String table ;
        private Class<T> type;
        boolean sort = false;


        public Querys<T> select(String...filed){
            if (filed.length==0)
                return this;
            Document select =  (Document) sql.getOrDefault(Select,new Document());
            Arrays.stream(filed).forEach(x->{
                Matcher mat = pat.matcher(x);
                if (mat.find()){
                    Document group = (Document) sql.getOrDefault(GroupBy,new Document(_ID,new Document()));
                    group.append(x,new Document(USD+mat.group(1),USD+mat.group(2)));
                    sql.put(GroupBy,group);
                    select.append(x,USD+x);
                }
                else select.append(x,AGG+x);
            });
            sql.put(Select,select);
            return this;
        }

        public Querys<T> from(String table,Class<T> type){
            this.table = table;
            this.type = type;
            return this;
        }

        public Querys<T> where(){
            return this;
        }

        public Querys<T> where(Document where){
            if (where!=null&&!where.isEmpty())
                sql.put(Where,where);
            return this;
        }

        public Querys<T> groupBy(String...filed){
            if (filed.length==0)
                return this;
            Document group =  (Document) sql.getOrDefault(GroupBy,new Document(_ID,new Document()));
            Document id = (Document) group.get(_ID);
            Arrays.stream(filed).forEach(x->id.append(x,USD+x));
            group.put(_ID,id);
            sql.put(GroupBy,group);
            return this;
        }

        public Querys<T> orderBy(Sort...sorts){
            if (sorts.length==0)
                return this;
            Document sort = new Document();
            Arrays.stream(sorts).forEach(x->sort.append(x.getField(),x.order.toMongoOrder()));
            sql.put(OrderBy,sort);
            this.sort = true;
            return this;
        }

        public Querys<T> range(int skip,int limit){
            if (skip<=0||limit<=0)
                return this;
            sql.put(Skip,skip);
            sql.put(Limit,limit);
            return this;
        }

        public Querys<T> skip(int skip){
            if (skip<=0)
                return this;
            sql.put(Skip,skip);
            return this;
        }

        public Querys<T> limit(int limit){
            if (limit<=0)
                return this;
            sql.put(Limit,limit);
            return this;
        }

        public Stream<T> exec(){
            checkArgument(!Strings.isNullOrEmpty(table),"the table name cannot be null or empty .");
            log.debug("sql : {}",sql);
            MongoCollection<T> collection = getCollection(table,type);
            return StreamSupport.stream(collection.aggregate(
                    sql.entrySet()
                    .stream()
                            .map(i->new Document(i.getKey().toMongoField(),i.getValue()))
                            .collect(Collectors.toList()))
                    .spliterator(),!sort);
        }
    }
}




能够支持的mongo聚合操作

public enum Action{
    Where,GroupBy,Select,OrderBy,Skip,Limit;
    public String toMongoField(){
        switch (this){
            case Where:return MATCH;
            case GroupBy:return GROUP;
            case Select:return PROJECT;
            case OrderBy:return SORT;
            case Skip:return SKIP;
            default: return LIMIT;
        }
    }
}

排序字段转换

public enum Order{
    Asc,Desc;
    public int toMongoOrder(){
        return this==Asc?1:-1;
    }
}
public class Sort {
    private String field;

    Order order;

    private Sort(){}

    public Sort(String field, Order order){
        this.field = field;
        this.order = order;
    }

    public String getField() {
        return field;
    }

    public void setField(String field) {
        this.field = field;
    }

    public Order getOrder() {
        return order;
    }

    public void setOrder(Order order) {
        this.order = order;
    }
}

相关字典

public class Dict {

    public static class Symbol{
        public static final char COMMA = ',';
        public static final char MINUS = '-';
        public static final char USD = '$';
        public static final char POINT = '.';
    }

    public static class MongoSupp{
        public static final String GROUP = "$group";
        public static final String MATCH = "$match";
        public static final String PROJECT = "$project";
        public static final String SORT = "$sort";
        public static final String SKIP = "$skip";
        public static final String LIMIT = "$limit";
        public static final String SUM = "$sum";
        public static final String AVG = "$avg";
        public static final String _ID = "_id";
        public static final String AGG = USD+_ID+POINT;
    }

}

使用方式

    @Test
    public void test14(){
        util.<Document>Querys()
                .select("sum(position)")
                .from(NameDefine.TB_InvestorPosition, Document.class)
                .groupBy("investorid")
                .orderBy(new Sort("sum(position)",Order.Desc))
                .exec()
                .collect(Collectors.toList())
                .forEach(System.out::println);
    }



"Inside of a method": {
        //拿到展示字段
        String field = req.getVolumeType().toRiskField();
        //拿到表名称
        String table = req.getVolumeType().deriveTableName();
        //拿到group by字段
        String group = req.getSelectType().deriveGroupByField();

        return util.<Document>Querys()
                .select(group,sum(field))
                .from(table,Document.class)
                .where(ands)
                .groupBy(group)
                .exec()
                .map(x->new PieResp(x.getString(group),x.get(sum(field))))//将查询出来两列的Document转换成想要的类型
                .collect(Collectors.toList());
}
...

最后

公司项目闭源,因此只能贴出一小部分供大家参考。

功能可能并不完美,github上还有一些直接标准sql转Mongo查询语句的库,比我这个强大很多,但是对于我这个简单的服务来说并不需要如此强大的功能。当然还有很复杂操作是不支持的,例如lookup等关联操作,count等操作。可以根据个人需求拓展。代码也有很多不严谨的地方,还望多多指正。

上一篇下一篇

猜你喜欢

热点阅读