Mysql 通用任意单表查询API设计

应用场景

1/数据库任意单表查询的重复劳动
2/数据库任意单表多业务需要不同字段的重复劳动

#说明

1/黑名单设计,解决敏感表和敏感字段拦截
2/防SQL注入处理,对key,value的合法性进行验证
3/对于表名暴露给前端,有两种处理方式,一种由node层来传参,一种服务器添加一个表名映射,实现逻辑表与物理表的联系
4/为了性能考虑,fields与filter为必填项
5/可扩展:加入分页查询,可以实现任意表的分页查询

使用场景:
1/根据ID查询,返回id,name字段
{
	"table":"t_base_user",
	"fields":["id","name"],
	"filter":{
		"id":"1111"
	}
}
2/in查询
{
	"table":"t_base_user",
	"fields":["id","name"],
	"filter":{
		"id":["1",2]
	}
}
3/like查询
{
	"table":"t_base_user",
	"fields":["id","name"],
	"filter":{
		"name":{"$like":"%富贵%"}
	}
}
4/范围查询 大于,小于
{
	"table":"t_base_user",
	"fields":["id","name"],
	"filter":{
		"start_time":{
			"$gte":"2020-03-13 00:00:00",
			"$lte":"2020-03-20 00:00:00"
		}
	}
}
5/反向查询,不等于
{
	"table":"t_base_user",
	"fields":["id","name"],
	"filter":{
		"type":{"$ne":1}
	}
}
6/嵌套逻辑范围查询,$and,$or
{
	"table":"t_base_user",
	"fields":["id","name"],
	"filter":{
		"group_account":"001",
		"$or":{
			"type":{"$ne":1},
			"name":{"$like":"富贵%"}
		}
	}
}
where group_account='001' and (type !=1 or name like '富贵%')

7/根据关键字模糊匹配,倒序前5条记录
{
	"table":"t_base_user",
	"fields":["id","name"],
	"filter":{
		"name":{"$like":"%宝贵%"}
	},
	"sort":{"time":"desc"},
	"limit":5
}

#实体对象

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class QueryReq implements Serializable {

    /**
     * 数据源
     */
    private String table;

    /**
     * 返回字段-as别名
     * eg:["id","head_img as headImg"]
     */
    @NotNull(message = "'fields' 不能为空")
    private List<String> fields;

    /**
     * 过滤条件
     * eg:
     * {
     * 		"org_account":"ZJXHJT",
     * 		"start_time":{
     * 			"$gte":"2020-03-13 00:00:00",
     * 		    "$gt":"2020-03-13 00:00:00",
     * 			"$lte":"2020-03-20 00:00:00",
     * 		    "$lt":"2020-03-20 00:00:00"
     *      },
     * 		"activity_type":{"$ne":2},
     * 	    "activity_name":{"$like":"%hello%"},
     * 		"$and":{
     * 			"activity_status":3,
     * 			"project_account":"001"
     *        },
     * 		"$or":{
     * 			"activity_status":3,
     * 			"project_account":"001"
     *        },
     * 		"prize_configure":["1111","0001","1001"]* 	}
     */
    @NotNull(message = "'filter' 不能为空")
    private Map<String,Object> filter;

    /**
     * 返回类型 list | obj
     */
    private String resultType = "list";

    /**
     * 返回记录数
     */
    private Integer limit;

    /**
     * 排序,eg:{"id":"desc","name":"asc"}
     */
    private LinkedHashMap<String,String> sort;

    /**
     * 是否显示SQL
     */
    private boolean showSql = false;
}

#解析器,转SQL语句

@Service
@Slf4j
public class CommonService {

    @Autowired
    private CommonMapper commonMapper;

    private final List<String> scope = Arrays.asList("$or","$and");
    private final List<String> variables = Arrays.asList("$gt","$gte","$lt","$lte","$ne","$like");
    private final List<String> sortEnum = Arrays.asList("asc","desc");
    private static final Map<String,List<String>> blackTableFieldList = new HashMap<>();


    private final Pattern keyRegexPat = Pattern.compile("[^(a-zA-Z 0-9_\\-\\$)]+");
    private final Pattern valueRegexPat = Pattern.compile("[']+");
    static
    {
        //黑名单字段描述
        //blackTableFieldList.put("t_user", Arrays.asList("*"));
        //blackTableFieldList.put("t_user", Arrays.asList("password"));
    }

    public Object query(QueryReq queryReq) {
        validateKey(queryReq.getTable());


        List<String> blankFieldList = blackTableFieldList.get(queryReq.getTable());
        if (blankFieldList !=null && blankFieldList.contains("*")){
            throw new BizException(ErrorCode.SERVER_ERROR.getCode(),"无权限访问此数据源任意字段");
        }

        queryReq.getFields().forEach(item->{
            validateKey(item);
            if (blankFieldList !=null && blankFieldList.contains(item)){
                throw new BizException(ErrorCode.SERVER_ERROR.getCode(),"无权限访问此数据源,'"+item+"'字段");
            }
        });

        if (queryReq.getTable() == null){
            throw new BizException(ErrorCode.SERVER_ERROR.getCode(),"未知数据源");
        }

        StringBuilder sql = new StringBuilder("select ");
        sql.append(String.join(",",queryReq.getFields()));
        sql.append(" from ");
        sql.append(queryReq.getTable());
        sql.append(" where ");
        sql.append(buildWhere("$and",queryReq.getFilter()));
        sql.append(buildSort(queryReq.getSort()));
        sql.append(!"list".equals(queryReq.getResultType())?" limit 1 ":queryReq.getLimit() == null?"":" limit "+queryReq.getLimit());
        log.debug("generator sql:{}",sql.toString());
        if (queryReq.isShowSql()){
            return sql.toString();
        }
        try {
            List<Object> resultList = commonMapper.query(sql.toString());
            if (!"list".equals(queryReq.getResultType())){
                return resultList.size()==0?Collections.EMPTY_MAP:resultList.get(0);
            }
            return resultList;
        }catch (Exception e){
            throw new BizException(ErrorCode.SERVER_ERROR.getCode(),e.getCause().getMessage());
        }
    }

    private void validateKey(String item) {
        Matcher matcher = keyRegexPat.matcher(item);
        if(matcher.find()){
            String subStr = scopeStr(item,matcher.start(0)-10,matcher.end(0)+10);
            throw new BizException(ErrorCode.FORMAT_ERROR.getCode(),"包含特殊符号:'"+subStr+"'");
        }
    }

    private void validateValue(String item) {
        Matcher matcher = valueRegexPat.matcher(item);
        if(matcher.find()){
            String subStr = scopeStr(item,matcher.start(0)-10,matcher.end(0)+10);
            throw new BizException(ErrorCode.FORMAT_ERROR.getCode(),"包含特殊符号:'"+subStr+"'");
        }
    }

    private String scopeStr(String item, int start, int end) {
        start = start<0?0:start;
        end = end>item.length()?item.length():end;
        return item.substring(start,end);
    }

    private String buildSort(LinkedHashMap<String,String> sort) {
        if (CollectionUtils.isEmpty(sort)){
            return "";
        }
        StringBuilder valStr = new StringBuilder(" order by ");
        boolean isFirst = true;
        for(String item : sort.keySet()){
            validateKey(item);
            if (!sortEnum.contains(sort.get(item))){
                throw new BizException(ErrorCode.FORMAT_ERROR.getCode(),"包含未知匹配符:"+sort.get(item));
            }
            if (!isFirst){
                valStr.append(",");
            }
            isFirst = false;
            valStr.append(item).append(" ").append(sort.get(item)).append(" ");
        }
        return valStr.toString();
    }

    private String buildWhere(String connector,Map<String, Object> filter) {
        connector = connector.replace("$","");
        StringBuilder valStr = new StringBuilder();
        boolean isFirst = true;

        for(String key:filter.keySet()){
            validateKey(key);
            if (!isFirst){
                valStr.append(" ").append(connector).append(" ");
            }
            isFirst = false;

            if (scope.contains(key)){
                valStr.append("(").append(buildWhere(key, (Map<String, Object>) filter.get(key))).append(")");
            }else if(filter.get(key) instanceof Map){
                valStr.append(buildVariableValue(key,(Map<String, Object>) filter.get(key)));
            }else {
                valStr.append(key).append(buildValue(filter.get(key)));
            }


        }

        return valStr.toString();
    }

    private String buildVariableValue(String key, Map<String, Object> filter) {
        StringBuilder valStr = new StringBuilder();
        boolean isFirst = true;
        for(String item :filter.keySet()){
            if (!variables.contains(item)){
                throw new BizException(ErrorCode.FORMAT_ERROR.getCode(),"包含未知匹配符:"+item);
            }
            String connector = "";
            switch (item){
                case "$gte":connector=" >= ";break;
                case "$gt":connector=" > ";break;
                case "$lte":connector=" <= ";break;
                case "$lt":connector=" < ";break;
                case "$ne":connector=" != ";break;
                case "$like":connector=" like ";break;
            }
            if (!isFirst){
                valStr.append(" and ");
            }
            isFirst = false;
            valStr.append(key).append(connector).append(buildStrValue(filter.get(item)));
        }
        return valStr.toString();
    }

    private String buildValue(Object val) {
        StringBuilder valStr = new StringBuilder();
        if (val instanceof Collection){
            valStr.append(((Collection)val).stream().map(item->buildStrValue(item)).collect(Collectors.joining(","," in (",")")));
        }else {
            valStr.append(" = ").append(buildStrValue(val));
        }
        return valStr.toString();
    }

    private String buildStrValue(Object val){
        if (val instanceof Number){
            return val.toString();
        }
        validateValue(val.toString());
        return "'"+val.toString()+"'";
    }
}

评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值