#1 前言git
本篇文章主要来讲明下代码模块的设计。像咱们这种菜鸟级别,只有平时多读读源码,多研究和探讨其中的设计才可能提高本身,写出高质量的代码。sql
没有最好的设计,只有更好的设计,因此在发表我本身的愚见的同时,但愿小伙伴们相互探讨更好的设计,有探讨才有更大的进步。mongodb
#2 题目及分析json
咱们维护了一个数据中心,对外提供查询API,如何能让用户随意的添加查询条件,而不用修改后台的查询代码呢?用户如何配置查询条件,从而达到以下的sql效果呢?:ide
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 查询参数的传递方式ui
咱们做为API设计者,该如何让用户方便的传递他们任意的查询需求呢?这是咱们要思考的地方。this
目前来看比较好的方式莫过于:用户经过json来表达他们的查询需求。.net
##2.2 查询的本质分析设计
从上面的查询来看,咱们能够总结出来查询条件无非就是某个字段知足什么样的条件。这里有三个对象:unix
##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](http://my.oschina.net/u/126678)>":"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 解析器接口的实现类
目前内置了几个经常使用的解析器实现,类图以下:
以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项目