1. 背景
對於用戶來講查詢功能按易用性分三個層次:
1)最簡單查詢操作是一個輸入框,全文檢索,如百度,后台實現技術使用搜索引擎,需要設計和建立索引,技術較為復雜,適用於文檔和信息數據庫檢索,但是結果很難精確控制。
2)其次是定義字段查詢,很多企業信息系統大多用的是這種查詢,針對模塊特定字段的查詢,有針對性、使用門坎低,適用於企業內部信息管理系統模塊定制。
3)最后一種是專門針對數據模型靈活的查詢編輯器,使用難度最高,但是查詢結果可以靈活和精確的控制,適用於有一定IT知識並對數據相當了解的用戶,同時又可以避免直接將數據庫暴露給用戶帶來的不安全隱患。
大家不難發現一個好的系統軟件的查詢基本會涵蓋上述三種類型的查詢功能
2. jeecg實現原理
Jeecg系統中模塊主要使用第二種方式的查詢功能,使用hibernate的QBC來封裝前端查詢條件,針對字段的定制過濾條件,最后轉換為sql執行數據庫查詢。
現有方案優點:
- 默認無須復雜開發即可實現字段檢索,支持范圍、模糊、精確匹配查詢
- 內置簡單表達式支持:!*,實現非、模糊、數組等
現有方案缺點:
- 不支持or條件
- 不支持字段間操作field1=field2
- 不支持非hibernate關聯表聯合查詢
- 單一字段條件只能出現一次
- 不支持sql嵌套
- 要支持上述功能需要額外開發定制工作
某些特定場景下,用戶想要通過模塊獲得相關信息必須借助於第三方報表模塊功能或求助於開發人員,無形中對報表模塊開發帶來一定壓力。比如,用戶要從模塊中查詢特殊的部分數據進行操作,現有查詢功能無法做到,報表模塊又不能操作編輯,這個時候就是高級查詢器定制查詢功能派上用場的時候了。
3. 類似應用舉例
我們來看看Outlook郵件查詢設計:
1)全文檢索型
2)字段定制型
3)高級查找
微軟Team Foundation Server查詢編輯器:
4. Jeecg查詢器設計與實現
UI設計:首先要實現高級查詢,必須要對數據表元信息metadata進行封裝才可以通用化,我們利用datagrid標簽中的columnList來自動生成字段下拉列表。作為高級查詢,從易用的性和使用頻率的角度功能所占UI比例不能太多,采用彈出窗口實現,如圖:
這里的表格我使用了treegrid,因為更適合表達sql語法樹,但沒有實現樹形結構,條件只加了大於、小於、包含等常用操作符,有待后續擴展sql嵌套。
右側有查詢歷史,將每次查詢的json條件保存在一個數組中實現快速重查,這個歷史查詢記錄使用HTML5的localstorage保存在客戶端緩存中,下次登錄仍然有效,對於不支持localstorage的瀏覽器將使用cookie存儲。
前后端交互:從到向前兼容性和對框架升級改動最小的因素考慮,將查詢器組裝的所有內容封裝到一個json字符串,作為一個參數_sqlbuidler傳遞到后台,由后台控制條件組裝,更加安全,防止sql注入等漏洞
json格式示例:
- [{“id”:101,”field”:”user_name”,”condition”:”like”,”value”:”%王%”,”relation”:”and”},{“id”:101,”field”:”user_name”,”condition”:”like”,”value”:”%王%”,”relation”:”and”}]
后台封裝處理java對象QueryCondition
- package org.jeecgframework.web.demo.entity.test;
- import java.util.List;
- public class QueryCondition {
- String field;
- String type;
- String condition;
- String value;
- String relation;
- List<QueryCondition> children;
- public List<QueryCondition> getChildren() {
- return children;
- }
- public void setChildren(List<QueryCondition> children) {
- this.children = children;
- }
- public String getField() {
- return field;
- }
- public void setField(String field) {
- this.field = field;
- }
- public String getType() {
- return type;
- }
- public void setType(String type) {
- this.type = type;
- }
- public String getCondition() {
- return condition;
- }
- public void setCondition(String condition) {
- this.condition = condition;
- }
- public String getValue() {
- return value;
- }
- public void setValue(String value) {
- this.value = value;
- }
- public String getRelation() {
- return relation;
- }
- public void setRelation(String relation) {
- this.relation = relation;
- }
- public String toString(){
- StringBuffer sb =new StringBuffer();
- sb.append(this.relation).append(" ");
- sb.append(this.field).append(" ")
- .append(this.condition).append(" ");
- if("java.util.Date".equals(this.type)){
- sb.append("to_date('").append(this.value).append("','yyyy-MM-dd')");
- }else if("java.lang.Number".equals(this.type)
- ||this.condition.indexOf("in")>0){//TODO 需要按類型處理
- sb.append(this.value);
- }else{
- sb.append("'").append(this.value).append("'");//TODO 需要處理特殊字符
- }
- return sb.toString();
- }
- }
后台解析處理代碼,修改org.jeecgframework.core.extend.hqlsearch.HqlGenerateUtil:
- public static void installHql(CriteriaQuery cq, Object searchObj,
- Map<String, String[]> parameterMap) {
- installHqlJoinAlias(cq, searchObj, getRuleMap(), parameterMap, "");
- // --增加一個特殊sql參數處理----------------------------------
- try{ if(StringUtil.isNotEmpty(parameterMap.get("_sqlbuilder"))){
- List<QueryCondition> list = JSONHelper.toList(
- parameterMap.get("_sqlbuilder")[0]
- , QueryCondition.class);
- String sql=getSql(list,"");
- System.out.println("DEBUG sqlbuilder:"+sql);
- cq.add(Restrictions.sqlRestriction(sql));
- }
- }catch(Exception e){
- e.printStackTrace();
- }
- // --增加一個特殊sql參數處理----------------------------------
- cq.add();
- }
5. 實現約束
- 只支持標准命名的表名,因為是通過java駝峰命名轉換帶下划線的數據庫表名,如果表名不是這個規則字段名會轉換錯誤;
- Sql前端界面在輸入時並沒有做太多約束和控制,因此非專業用戶使用時會出現非法的語句而查詢無果,建議在懂sql的人員指導下使用不要直接交給用戶。
6. 標簽使用
Datagrid標簽中已經對高級查詢器做了封裝,js變量也根據每個模塊的id做了命名處理防止沖突。
使用時只需在datagrid標簽加一個屬性queryBuilder="true",例如
- <t:datagrid name="jeecgDemoList" title="DEMO示例列表" autoLoadData="true" actionUrl="jeecgDemoController.do?datagrid" sortName="userName" fitColumns="true"
- idField="id" fit="true" queryMode="group" checkbox="true" queryBuilder="true">
模塊UI效果如下,重置后面會多一個按鈕:
7. TODO
UI方面:易用性還可以提升,比如選中某字段和條件=時,值自動根據數據庫表中該列實際值枚舉一部分候選項;字段為date類型時,值編輯框變成date控件;字段為整形時,值編輯框有校驗或只能輸入整數的spinbox來防呆
功能:List<QueryCondition>對象轉換為sql getSql()函數中僅僅實現拼裝原生sql,這塊還有很大的改進空間,可以增加字段類型(int,string,date)的識別和處理、操作符(正則匹配)、內置表達式和函數(類似TFS)等擴展。
安全:模塊按鈕還沒有跟權限綁定,只是通過標簽屬性來開關,應該加一個動態權限由系統配置來控制