数据表格 搜索条件 样板代码 简化

业务场景:  一个数据表格,  n 个搜索框,点击搜索按钮后,刷新数据表格。 查询条件可能很多, 也可能会变动。

为了减少  这部分 样板代码  的开发工作,通过 拦截器生成 sql where 条件。

例如:

<input name='search_EQ_username'  value = 'chuang' >
<input name='search_LIKE_phone'  value = '152' >
<input name='search_GTE_age'  value = '25' >
...  更多搜索框

拦截器生成 where 语句, where username = 'chuang' and phone like '%152%' and age >= 25 ,

前端页面增加 变动 搜索框,不改后端代码。
// 普通 controller  

@Before(SearchSql.class)    
public void query() {  
   // 分页参数  
   int pageNumber = getAttr("pageNumber");    
   int pageSize = getAttr("pageSize");    
   
   // where 条件
   String where = getAttr(Constant.SEARCH_SQL);    
   
   // 查询数据
   Page<SysUser> sysUserPage = SysUser.dao.page(pageNumber, pageSize, where);    
   renderDatagrid(sysUserPage);    
}
 /**
 *  拦截器, 可扩展更多 sql 生成逻辑
 *  只使用 mysql,  其它数据库自行修改 生成sql 语句逻辑
 *
 *  @author 佚名
 */
public class SearchSql implements Interceptor {

    public void intercept(Invocation ai) {
        Controller c = ai.getController();

        // 自定义 查询字段前缀
        String prefix = "search_";
        
        Map<String, Object> searchParams = getParametersStartingWith(c.getRequest(), prefix);
        Map<String, SearchFilter> filters = SearchFilter.parse(searchParams);
        String whereSql = buildFilter(filters.values());
        c.setAttr(Constant.SEARCH_SQL, whereSql);

        // 自定义分页字段名
        int pageNumber = c.getParaToInt("page", 1);
        int pageSize = c.getParaToInt("rows", 1);

        c.setAttr("pageNumber", pageNumber);
        c.setAttr("pageSize", pageSize);
        ai.invoke();
    }

    /**
     * 取得带相同前缀的Request Parameters, copy from spring WebUtils.
     * 返回的结果的Parameter名已去除前缀.
     */
    private Map<String, Object> getParametersStartingWith(
            HttpServletRequest request, String prefix) {
        Enumeration<String> paramNames = request.getParameterNames();
        Map<String, Object> params = new TreeMap<>();
        if (prefix == null) {
            prefix = "";
        }
        while (paramNames != null && paramNames.hasMoreElements()) {
            String paramName = paramNames.nextElement();
            if ("".equals(prefix) || paramName.startsWith(prefix)) {
                String unprefixed = paramName.substring(prefix.length());
                String[] values = request.getParameterValues(paramName);
                //noinspection StatementWithEmptyBody
                if (values == null || values.length == 0) {
                    // Do nothing, no values found at all.
                } else if (values.length > 1) {
                    params.put(unprefixed, values);
                } else {
                    params.put(unprefixed, values[0]);
                }
            }
        }
        return params;
    }

    /**
     * 按属性条件列表创建查询字句
     */
    private String buildFilter(final Collection<SearchFilter> filters) {
        StringBuilder sb = new StringBuilder();
        if (null != filters && filters.size() > 0) {
            for (SearchFilter filter : filters) {
                if (sb.length() > 0) {
                    sb.append(" and ");
                }
                sb.append(filter.fieldName);

                // 此处 可能要根据数据库类型 修改
                switch (filter.operator) {
                    case EQ:
                        // 相等 条件
                        sb.append(" ='").append(filter.value).append("'");
                        break;
                    case LIKE:
                        // 模糊查询,首尾添加  %
                        sb.append(" like ").append("'%").append(filter.value).append("%'");
                        break;
                    case LIKECUST:
                        // 模糊匹配,* 替换为 %
                        sb.append(" like ").append("'").append(filter.value.toString().replaceAll("\\*","%")).append("'");
                        break;
                    case GT:
                        sb.append(" >'").append(filter.value).append("'");
                        break;
                    case LT:
                        sb.append(" <'").append(filter.value).append("'");
                        break;
                    case GTE:
                        sb.append(" >='").append(filter.value).append("'");
                        break;
                    case LTE:
                        sb.append(" <='").append(filter.value).append("'");
                        break;
                    case NEQ:
                        // 不等于
                        sb.append(" !='").append(filter.value).append("'");
                        break;
                    case INN:
                        // in 数字
                        sb.append(" in (").append(filter.value).append("')");
                        break;
                    case INS:
                        // in 字符串
                        sb.append(" in ('").append(filter.value.toString().replaceAll(",", "','")).append("')");
                        break;
                    case IS:
                        // is null
                        sb.append("  is null ");
                        break;
                    case ISNOT:
                        // is not null
                        sb.append("  is not null ");
                        break;
                }
            }
        }
        return sb.toString();
    }
}

class SearchFilter {

    // 查询字段名
    public final String fieldName;
    // 查询字段值
    public final Object value;
    // 查询条件
    public final Operator operator;

    public SearchFilter(String fieldName, Operator operator, Object value) {
        this.fieldName = fieldName;
        this.value = value;
        this.operator = operator;
    }

    /**
     * searchParams中key的格式为OPERATOR_FIELDNAME
     */
    public static Map<String, SearchFilter> parse(Map<String, Object> searchParams) {
        Map<String, SearchFilter> filters = new HashMap<>();

        for (Map.Entry<String, Object> entry : searchParams.entrySet()) {
            // 过滤掉空值
            String key = entry.getKey();
            Object value = entry.getValue();
            if (StrKit.isBlank((String) value)) {
                continue;
            }

            // 拆分operator与field

            String[] names = key.split("_");
            if (names.length < 2) {
                throw new IllegalArgumentException(key + " is not a valid search filter name");
            }

            // field 中可能有查询条件
            String filedName;
            StringBuilder filedNameTemp = new StringBuilder();
            for (int i = 1; i < names.length; i++) {
                filedNameTemp.append(names[i]).append("_");
            }
            if (filedNameTemp.substring(filedNameTemp.length() - 1).equals("_")) {
                filedNameTemp = new StringBuilder(filedNameTemp.substring(0, filedNameTemp.length() - 1));
            }
            filedName = filedNameTemp.toString();

            // 查询条件
            Operator operator = Operator.valueOf(names[0]);

            // 创建searchFilter
            SearchFilter filter = new SearchFilter(filedName, operator, value);

            filters.put(key, filter);
        }

        return filters;
    }

    public enum Operator {
        EQ, LIKE,LIKECUST, GT, LT, GTE, LTE, NEQ, INS, INN, IS, ISNOT
    }
}



评论区

alien_yuan

2019-09-26 16:04

怎么这么复杂

JFinal

2019-09-26 17:31

封装的方向非常好,我以前有个项目也是类似封装,用的一个 SearchInter 拦截器:
@Before(SearchInter.class)

然后在 SearchInter 中用一个 Cond.java 封装所有查询条件,然后里面有再添加两个方法:
String getSql()
Object getParas()

controller 的代码就减少到了如下程度:
Cond cond = getAttr("cond");
List ret = Db.find(cond.getSql(), cond.getParas());
renderJson(ret);

JFinal

2019-09-26 17:32

由于用的是 Db.find, 所以可以解决所有后台管理搜索需求

blog

2019-09-27 17:37

会不会存在sql注入的风险呢?

JFinal

2019-09-27 18:03

@blog sql 中是问号占位字符,底层用的是 preparestatement,天然防 sql

不要将参数拼接在 sql 中就不会出现 sql 注入

EATI001

2019-10-06 18:47

@JFinal 老大分享下你封装的通用查询代码也来学习参考下的呢,这块如果封装的好确实省时省力。

苏伟伟

2019-10-07 13:42

多表联合查询怎么办呢?

zhangchuang

2019-10-07 19:25

@苏伟伟 表单 的 name 写成 search_EQ_a.username , 会 解析成 where a.username = 'xx' 这样的,a是 多表中某表别名,加上 自己 在 dao 中写的方法,便能实现 多表联合查询了,这个拦截器只是 解析出 where 条件。

blog

2019-10-11 19:17

@JFinal 好的,感谢回答。

热门分享

扫码入社