cms查询系统(二)json形式参数的设计与解析

本文涉及的产品
云数据库 MongoDB,独享型 2核8GB
推荐场景:
构建全方位客户视图
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
简介:

1 前言

本篇文章主要来说明下代码模块的设计。像我们这种菜鸟级别,只有平时多读读源码,多研究和探讨其中的设计才可能提升自己,写出高质量的代码。

没有最好的设计,只有更好的设计,所以在发表我自己的愚见的同时,希望小伙伴们相互探讨更好的设计,有探讨才有更大的进步。

2 题目及分析

我们维护了一个数据中心,对外提供查询API,如何能让用户随意的添加查询条件,而不用修改后台的查询代码呢?用户如何配置查询条件,从而达到如下的sql效果呢?:

a.name='lg' or b.age>12
b.id in (12,34,45)
c.updateTime>'2015-3-28' and (b.id=2 or d.age<23)
e.age>f.age

2.1 查询参数的传递方式

我们作为API设计者,该如何让用户方便的传递他们任意的查询需求呢?这是我们要思考的地方。

目前来看比较好的方式莫过于:用户通过json来表达他们的查询需求。

2.2 查询的本质分析

从上面的查询来看,我们可以总结出来查询条件无非就是某个字段满足什么样的条件。这里有三个对象:

  • 查询的字段 如 b.age
  • 条件值 如 12
  • 怎样满足条件值 如 >

2.3 查询的配置分析

这样我们就可以清晰明了了,一个查询条件无非就是三个内容,所以可以如下配置:

{
    "columns":"b.age",
    "oper":">",
    "value":12
}

很显然,上面的确很麻烦,我们无非是要表达这三个内容,所以就要简化配置:

{
    "b.age":12,
    "oper":">"
}

还是不够简化,如何把操作符 > 也放置进去呢?如下

{
    "b.age@>":12
}

这样我们就可以把三个对象表达清楚了,将查询的字段和操作符合并起来作为key,并使用分隔符@分割两者,条件值作为value。这样就做到了,非常简化的查询配置。

接下来又面临一个问题,如何表达查询条件之间的and or 关系呢?即如何表达下面的内容呢?

c.age>14 and (b.id=2 or d.age<23)

借鉴mongodb的查询方案,可以如下配置:

{
    "c.age@>":14,
    "$or":{
        "b.id@=":2,
        "d.age@<":23
    }
}

通过配置一个$or作为key表明里面的几个查询条件是or的关系,如果是$and表明里面的查询条件之间是and的关系,外层默认是and的关系。

同时我们再回顾下,mongodb所作出的查询设计,也是通过用户配置json形式来表达查询意图,但是我们来看下它是如何查询

a.age>12 对应的mongodb的查询方式为:
{
    a.age : {
        $gt : 22
    }
}

我们的查询方式是

{
    "a.age@>":22
}

虽然看似我们的更加简单,mongodb的更加繁琐,主要是mongodb认为对于一个字段,可以有多个查询条件的,为了支持更加复杂的查询,如下:

{   
    a.age : {
        $lt :24,
        $gt : 17
    }
}

然而我们也可以对此进行拆分,如下,同样满足:

{
    "a.age@>":17"a.age@<":24,
}

各有各的好处和缺点,我就是我,颜色不一样的烟火。哈哈。。。

3 代码设计与实现

3.1 解析器接口的设计

对题目进行分析完了之后,就要考虑如何实现这样的json配置到sql的转化。实现起来不难,最重要的是如何做出一个高扩展性的实现?

再来看下下面的例子:

{
    "a.name@=":"lg",
    "b.age@>":12,
    "c.id@in":[12,13,14],
    "d.time@time>":"2015-3-1"
}

其实就是针对每个自定义的操作符进行相应的处理,所以就有了解析器接口:

public interface SqlParamsParser {

    //这里表示该解析器是否支持对应的操作符
    public boolean support(String oper);

    public String getParams(String key,Object value,String oper);

    public SqlParamsParseItemResult getParamsResult(String key,Object value,String oper);

}

其中SqlParamsParseItemResult,则是把解析后的结果分别存储起来,而不是直接拼接成一个字符串,主要为了直接拼接字符串式的sql注入,它的内容如下:

public class SqlParamsParseItemResult {
    private String key;
    private String oper;
    private Object value;
}

上面的key oper value 则是解析后的内容。下面举例说明

以"b.age@>“:12 为例,其中getParams方法中的 key就是b.age, value就是12, oper就是> 而这个方法的返回的字符串结果为:

b.age>12

返回的SqlParamsParseItemResult存储的内容为分别为 key=b.age ; oper=> ; value=12

以"c.id@in”:[12,13,14]为例,其中getParams方法中的 key就是c.id,value就是一个List集合,oper就是in ,这个方法的返回结果为:

c.id in (12,13,14)

返回的SqlParamsParseItemResult存储的内容为分别为 key=c.id ; oper=in ; value=(12,13,14)

以"d.time@time>“:“2015-3-1"为例,其中getParams方法中的 key就是c.id,value就是一个List集合,oper就是in,这个方法的返回结果为:

unix_timestamp(d.time) > 1425139200     (2015-3-1对应的秒数)

返回的SqlParamsParseItemResult存储的内容为分别为 key=unix_timestamp(d.time) ; oper=> ; value=1425139200

3.2 解析器接口的抽象类

解析器有很多相同的地方,这就需要我们进行抽象,抽出共性部分,留给子类去实现不同的部分。所以有了抽象类AbstractSqlParamsParser

有哪些共性部分和非共性部分呢?

  • 共性部分: 就是support方法。每个解析器支持某几种操作符,所以判断该解析器是否支持当前的操作符的逻辑是共同的,所以如下:

    public abstract class AbstractSqlParamsParser implements SqlParamsParser{
    
        private String[] opers; 
        private boolean ignoreCase=true;
    
        protected void setOpers(String[] opers){
            this.opers=opers;
        }   
        protected void setIgnoreCase(boolean ignoreCase){
            this.ignoreCase=ignoreCase;
        }
        @Override
        public boolean support(String oper) {
            if(opers!=null && oper!=null){
                for(String operItem:opers){
                    if(ignoreCase){
                        operItem=operItem.toLowerCase();
                        oper=oper.toLowerCase();
                    }
                    if(operItem.equals(oper)){
                        return true;
                    }
                }
            }
            return false;
        }
    }
    

    opers属性表示当前解析器所支持的所有操作符。ignoreCase表示在匹配操作符的时候是否忽略大小写。这两个属性都设置成private,然后对子类开放了protected类型的set方法,用于子类来设置这两个属性。

  • 非共性部分:留出了doParams方法供子类来具体实现

    @Override
    public SqlParamsParseItemResult getParamsResult(String key, Object value,
            String oper) {
        return doParams(key, processStringValue(value), oper);
    }
    
    protected abstract SqlParamsParseItemResult doParams(String key, Object value, String oper);
    

3.3 解析器接口的实现类

目前内置了几个常用的解析器实现,类图如下: 
SqlParamsParser接口类图

以TimeSqlParamsParser为例来简单说明下:

它主要是用于解析如下形式的:

{
    "d.time@time>":"2015-3-1"
}

最终想达到的效果是:

unix_timestamp(d.time) > 1425139200

它的解析过程如下:

/**
 * 以d.time@time>'2015-3-1'为例
 * 初始参数 key=d.time; value='2015-3-1'; oper=time>
 * 解析后的key=unix_timestamp(d.time); value=1425139200('2015-3-1'对应的秒数); oper=>
 */
@Override
protected SqlParamsParseItemResult doParams(String key, Object value, String oper) {
    String timeKey="unix_timestamp("+key+")";
    String realOper=oper.substring(4+fullTimeFlag.length());
    if(value instanceof String){
        String tmp=(String)value;
        Assert.isLarger(tmp.length(),2,"时间参数不合法");
        //默认进行了字符串处理,即加上了'',现在要去掉,然后解析成时间的秒数
        value=tmp.substring(1,tmp.length()-1);
        try {
            SimpleDateFormat format=new SimpleDateFormat(timeFormat);
            Date date=format.parse((String)value);
            value=date.getTime()/1000;
        } catch (ParseException e) {
            e.printStackTrace();
            throw new IllegalArgumentException("timeFormat为"+timeFormat+";value="+value+";出现了解析异常");
        }
    }else{
        Assert.isInstanceof(value,Number.class,"时间参数必须为时间的秒数");
    }
    return new SqlParamsParseItemResult(timeKey,realOper,value);
}

解析过程其实就是对key value oper 进行了不同程度的转换。

同时TimeSqlParamsParser还支持其他时间形式的解析,如"2015-3-1 12:23:12”,只需如下方式创建一个解析器:

new TimeSqlParamsParser("yyyy-MM-dd HH:mm:ss","full_")

然后他就能够解析下面的形式:

{
    "d.time@full_time>":"2015-3-1 12:23:12"
}

同时又能保留原有的形式,两者互不干扰。

4 DefaultSqlParamsHandler使用解析器

有了解析器的一系列实现,下面就需要一个综合的类来使用这些解析器。这就是DefaultSqlParamsHandler:

4.1 注册使用解析器

public class DefaultSqlParamsHandler {

private List<SqlParamsParser> sqlParamsParsers;

public DefaultSqlParamsHandler(){
    sqlParamsParsers=new ArrayList<SqlParamsParser>();
    sqlParamsParsers.add(new DefaultSqlParamsParser());
    sqlParamsParsers.add(new InSqlParamsParser());
    sqlParamsParsers.add(new TimeSqlParamsParser());
    sqlParamsParsers.add(new TimeSqlParamsParser("yyyy-MM-dd HH:mm:ss","full_"));
    sqlParamsParsers.add(new DefaultColumnSqlParamsParser());
}

内部已经注册了几个解析器。同时需要对外留出注册自定义解析器的方法:

public void registerSqlParamsHandler(SqlParamsParser sqlParamsParser){
    if(sqlParamsParser!=null){
        sqlParamsParsers.add(sqlParamsParser);
    }
}

public void registerSqlParamsHandler(List<SqlParamsParser> sqlParamsParsers){
    if(sqlParamsParsers!=null){
        for(SqlParamsParser sqlParamsParser:sqlParamsParsers){
            registerSqlParamsHandler(sqlParamsParser);
        }
    }
}

4.2 解析过程

这个过程不仅需要使用已经注册的解析器来解析,还包含对解析条件之间的and or 关系的递归处理。代码如下,不再详细说明:

private SqlParamsParseResult getSqlWhereParamsResultByAndOr(Map<String,Object> params,String andOr,
        boolean isPlaceHolder,SqlParamsParseResult sqlParamsParseResult){
    if(params!=null){
        String andOrDelititer=" "+andOr+" ";
        for(String key:params.keySet()){
            Object value=params.get(key);
            if(value instanceof Map){
                //这里需要进行递归处理嵌套的查询条件
                SqlParamsParseResult SqlParamsParseResultModel=null;
                if(key.equals(andKey)){
                    SqlParamsParseResultModel=processModelSqlWhereParams((Map<String,Object>)value,AND,isPlaceHolder);
                }else if(key.equals(orKey)){
                    SqlParamsParseResultModel=processModelSqlWhereParams((Map<String,Object>)value,OR,isPlaceHolder);
                }
                if(SqlParamsParseResultModel!=null && StringUtils.isNotEmpty(SqlParamsParseResultModel.getBaseWhereSql())){
                    sqlParamsParseResult.addSqlModel(andOrDelititer);
                    sqlParamsParseResult.addSqlModel("("+SqlParamsParseResultModel.getBaseWhereSql()+")");
                    sqlParamsParseResult.addArguments(SqlParamsParseResultModel.getArguments());
                }
            }else{
                //这里才是使用已经注册的解析器进行解析
                SqlParamsParseItemResult sqlParamsParseItemResult=processNormalSqlWhereParams(key,value,isPlaceHolder);
                if(sqlParamsParseItemResult!=null){
                    sqlParamsParseResult.addSqlModel(andOrDelititer);
                    sqlParamsParseResult.addSqlModel(sqlParamsParseItemResult.getSqlModel(isPlaceHolder,PLACE_HOLDER));
                    sqlParamsParseResult.addArgument(sqlParamsParseItemResult.getValue());
                }
            }
        }
        StringBuilder baseWhereSql=sqlParamsParseResult.getBaseWhereSql();
        if(StringUtils.isNotEmpty(baseWhereSql)){
            sqlParamsParseResult.setBaseWhereSql(new StringBuilder(baseWhereSql.substring(andOrDelititer.length())));
        }
    }
    return sqlParamsParseResult;
}

这里进行了递归调用,主要用于处理 $and $or 的嵌套查询,getSqlWhereParamsResultByAndOr可能内部调用了processModelSqlWhereParams,processModelSqlWhereParams内部又调用了getSqlWhereParamsResultByAndOr

private SqlParamsParseResult processModelSqlWhereParams(Map<String,Object> params,String andOr,boolean isPlaceHolder){
    return getSqlWhereParamsResultByAndOr(params,andOr,isPlaceHolder,new SqlParamsParseResult());
}

这里就是使用解析器进行解析的过程,先遍历每个解析器是否支持当前的操作符,如果支持则进行相应的解析

private SqlParamsParseItemResult processNormalSqlWhereParams(String key,Object value,boolean isPlaceHolder) {
    SqlParamsParseItemResult sqlParamsParseItemResult=null;
    String[] parts=key.split(separatorFlag);
    if(parts.length==2){
        for(SqlParamsParser sqlParamsParser:sqlParamsParsers){
            if(sqlParamsParser.support(parts[1])){
                sqlParamsParseItemResult=sqlParamsParser.getParamsResult(parts[0],value,parts[1]);
                break;
            }
        }
    }else{
        sqlParamsParseItemResult=new SqlParamsParseItemResult(key,"=",SqlStringUtils.processString(value));
    }
    return sqlParamsParseItemResult;
}

4.3 对外留出的扩展

{
    "c.age@>":14,
    "$or":{
        "b.id@=":2,
        "d.age@<":23
    }
}

这里面的@ $or 以及 $and 都是可以自己设定的,默认值是上述形式。

5 工程项目

这个小项目已经发布到osc上,见 osc的search-sqlparams项目

相关文章
|
1天前
|
JSON 前端开发 搜索推荐
关于商品详情 API 接口 JSON 格式返回数据解析的示例
本文介绍商品详情API接口返回的JSON数据解析。最外层为`product`对象,包含商品基本信息(如id、name、price)、分类信息(category)、图片(images)、属性(attributes)、用户评价(reviews)、库存(stock)和卖家信息(seller)。每个字段详细描述了商品的不同方面,帮助开发者准确提取和展示数据。具体结构和字段含义需结合实际业务需求和API文档理解。
|
8天前
|
存储 运维 负载均衡
Hologres 查询队列全面解析
Hologres V3.0引入查询队列功能,实现请求有序处理、负载均衡和资源管理,特别适用于高并发场景。该功能通过智能分类和调度,确保复杂查询不会垄断资源,保障系统稳定性和响应效率。在电商等实时业务中,查询队列优化了数据写入和查询处理,支持高效批量任务,并具备自动流控、隔离与熔断机制,确保核心业务不受干扰,提升整体性能。
32 9
|
14天前
|
JSON JavaScript 前端开发
一次采集JSON解析错误的修复
两段采集来的JSON格式数据存在格式问题,直接使用PHP的`json_decode`会报错。解决思路包括:1) 手动格式化并逐行排查错误;2) 使用PHP-V8JS扩展在JavaScript环境中解析。具体方案一是通过正则表达式和字符串替换修复格式,方案二是利用V8Js引擎执行JS代码并返回JSON字符串,最终实现正确解析。 简介: 两段采集的JSON数据因掺杂JavaScript代码导致PHP解析失败。解决方案包括手动格式化修复和使用PHP-V8JS扩展在JavaScript环境中解析,确保JSON数据能被正确处理。
|
13天前
|
存储 数据库 对象存储
新版本发布:查询更快,兼容更强,TDengine 3.3.4.3 功能解析
经过 TDengine 研发团队的精心打磨,TDengine 3.3.4.3 版本正式发布。作为时序数据库领域的领先产品,TDengine 一直致力于为用户提供高效、稳定、易用的解决方案。本次版本更新延续了一贯的高标准,为用户带来了多项实用的新特性,并对系统性能进行了深度优化。
23 3
|
17天前
|
存储 监控 算法
企业内网监控系统中基于哈希表的 C# 算法解析
在企业内网监控系统中,哈希表作为一种高效的数据结构,能够快速处理大量网络连接和用户操作记录,确保网络安全与效率。通过C#代码示例展示了如何使用哈希表存储和管理用户的登录时间、访问IP及操作行为等信息,实现快速的查找、插入和删除操作。哈希表的应用显著提升了系统的实时性和准确性,尽管存在哈希冲突等问题,但通过合理设计哈希函数和冲突解决策略,可以确保系统稳定运行,为企业提供有力的安全保障。
|
26天前
|
JSON 自然语言处理 Java
OpenAI API深度解析:参数、Token、计费与多种调用方式
随着人工智能技术的飞速发展,OpenAI API已成为许多开发者和企业的得力助手。本文将深入探讨OpenAI API的参数、Token、计费方式,以及如何通过Rest API(以Postman为例)、Java API调用、工具调用等方式实现与OpenAI的交互,并特别关注调用具有视觉功能的GPT-4o使用本地图片的功能。此外,本文还将介绍JSON模式、可重现输出的seed机制、使用代码统计Token数量、开发控制台循环聊天,以及基于最大Token数量的消息列表限制和会话长度管理的控制台循环聊天。
201 7
|
1月前
|
安全 前端开发 Android开发
探索移动应用与系统:从开发到操作系统的深度解析
在数字化时代的浪潮中,移动应用和操作系统成为了我们日常生活的重要组成部分。本文将深入探讨移动应用的开发流程、关键技术和最佳实践,同时分析移动操作系统的核心功能、架构和安全性。通过实际案例和代码示例,我们将揭示如何构建高效、安全且用户友好的移动应用,并理解不同操作系统之间的差异及其对应用开发的影响。无论你是开发者还是对移动技术感兴趣的读者,这篇文章都将为你提供宝贵的见解和知识。
|
1月前
|
负载均衡 网络协议 算法
Docker容器环境中服务发现与负载均衡的技术与方法,涵盖环境变量、DNS、集中式服务发现系统等方式
本文探讨了Docker容器环境中服务发现与负载均衡的技术与方法,涵盖环境变量、DNS、集中式服务发现系统等方式,以及软件负载均衡器、云服务负载均衡、容器编排工具等实现手段,强调两者结合的重要性及面临挑战的应对措施。
90 3
|
21天前
|
安全 搜索推荐 数据挖掘
陪玩系统源码开发流程解析,成品陪玩系统源码的优点
我们自主开发的多客陪玩系统源码,整合了市面上主流陪玩APP功能,支持二次开发。该系统适用于线上游戏陪玩、语音视频聊天、心理咨询等场景,提供用户注册管理、陪玩者资料库、预约匹配、实时通讯、支付结算、安全隐私保护、客户服务及数据分析等功能,打造综合性社交平台。随着互联网技术发展,陪玩系统正成为游戏爱好者的新宠,改变游戏体验并带来新的商业模式。
|
2月前
|
机器学习/深度学习 人工智能 数据处理
【AI系统】NV Switch 深度解析
英伟达的NVSwitch技术是高性能计算领域的重大突破,旨在解决多GPU系统中数据传输的瓶颈问题。通过提供比PCIe高10倍的带宽,NVLink实现了GPU间的直接数据交换,减少了延迟,提高了吞吐量。NVSwitch则进一步推动了这一技术的发展,支持更多NVLink接口,实现无阻塞的全互联GPU系统,极大提升了数据交换效率和系统灵活性,为构建强大的计算集群奠定了基础。
106 3

热门文章

最新文章

推荐镜像

更多