DataQL
簡介
DataQL(Data Query Language)DataQL 是一種查詢語言。旨在通過提供直觀、靈活的語法來描述客戶端應用程序的數據需求和交互。
數據的存儲根據其業務形式通常是較為簡單的,并不適合直接在頁面上進行展示。因此開發頁面的前端工程師需要為此做大量的工作,這就是 DataQL 極力解決的問題。
請注意 DataQL 不是一門編程語言,它是查詢語言。它對邏輯的處理僅限于簡單場景。DataQL 的解決問題的重點集中在:數據的聚合和轉換以及過程中的簡單加工處理。
特性
語法手冊
詞法記號
-
注釋支持://、/*…*/
-
支持任意空格,提高可讀性,支持\t,\n,\r,\f
-
關鍵字:
關鍵字含義 if 條件語句的引導詞 else 用在條件語句中,表明當條件不成立時的分支 return 三大退出指令之一,終止當前過程的執行并正常退出到上一個執行過程中 throw 三大退出指令之一,終止所有后續指令的執行并拋出異常 exit 三大退出指令之一,終止所有后續指令的執行并正常退出 var 執行一個查詢動作,并把查詢結果保存到臨時變量中 run 僅僅執行查詢動作,不保留查詢的結果 hint 寫在 DataQL 查詢語句的最前面,用于設置一些執行選項參數 import 將另外一個 DataQL 查詢導入并作為一個 Udf 形式存在、或直接導入一個 Udf 到當前查詢中 as 與 import 關鍵字配合使用,用作將導入的 Udf 命名為一個本地變量名 true 基礎類型之一,表示 Boolean 的:真值 false 基礎類型之一,表示 Boolean 的:假值 null 基礎類型之一,表示 NULL 值 -
標識符:表示查詢中的一些實體,如變量名參數名。必須滿足正則表達式:[_a-zA-Z][_0-9a-zA-Z]*,一些對象的key可能會超出范圍,可以用反引號`xxx`。
-
分隔符:()、{}、[]、,、:、;(非必須)
-
運算符:
- 數學運算:+,-,*,/,\(整除),%
- 位運算:&,|,!,^(異或),<<(左位移),>>(有符號右位移),>>>(無符號右位移)
- 比較運算:>,>=,<,<=,==,!=
類型系統
DataQL 是弱類型定義的查詢語言,在DataQL 中所有數據都會被歸結到有限的幾種類型上。無需定義數據類型結構,在弱類型系統中編寫查詢會非常方便,它去掉了繁雜的類型定義。
| 布爾 | true 或 false | 表示真假值 |
| 數值 | 負無窮大 或 0 或 正無窮大 | 浮點數、整數、科學計數法表示的數 |
| 字符串 | ‘…’ 或 “…” | 字符串 或 單個字符 |
| 空值 | null 或 NULL | 空值 |
| 集合 | […] 數組 或 多維數組 | 帶有順序的多組數據的集合 |
| 對象 | {‘key’:…} 具有鍵值對的數據體 | DataQL 的對象不支持方法,但是可以具備 Udf 類型的屬性。 |
| UDF | lambda 函數 或 一個外部的 Udf | 一個外部的 net.hasor.dataql.Udf 接口函數定義。 |
| DataQL 中書寫的 lambda 函數也被稱作為 UDF。 | ||
| 一個擴展代碼片段的定義,也屬于 UDF 的范疇。 |
-
數值類型
- 二進制表示法:0b01010101100 或 0B01010101100
- 十進制表示法:0o1234567 或 0O1234567
- 八進制表示法:-0000234 或 123
- 十六進制表示法:0x12345 或 0X12345
- 科學計數法:a * 10的n 次冪的形式,其中 1 < a < 10
- 關于負數:目前只有十進制表示法中提供了負數的表示能力。
-
UDF
- 外部Udf:外部的 Udf 被引入之后,通常以標識符形式表示它。
- DataQL 中書寫的 lambda 表達方式為:var foo = () -> { /* 代碼塊 */ }
- 外部代碼片段:var a = @@xxx() <% /* 外部代碼塊 */ %>
-
JSON
- DataQL 可以直接表達 Json 數據(Json 的 Key 必須通過雙引號或單引號形式包裹起來)
-
當兩類型不匹配時能自動類型提升。
數值
-
支持:byte、short、int、long、float、double、BigInt、Decimal
-
數值寬度默認是int和double
-
浮點數計算時默認保留20位小數,多余的四舍五入。修改精度使用hint MAX_DECIMAL_DIGITS=20,更換舍入規則hint NUMBER_ROUNDING = 'HALF_EBEN'。
語句
-
import
- import “<函數類或函數包類>” as <別名>
- import @"<資源地址>" as <別名>
- 別名必須滿足 標識符
- 必須放在整個查詢最開始的地方,被導入的資源會以var的方式定義。
-
var
- var <變量名> = <表達式 or 值對象 or 函數定義>
- 定義變量、執行并存儲表達式的值
-
run
- run <表達式 or 值對象 or 函數定義>
-
return、throw、exit
- return <狀態碼>, <表達式 or 值對象 or 函數定義>
- 還可以不指定狀態碼
- return <表達式 or 值對象 or 函數定義>
- 三者除了行為不同,用法完全一樣。
-
if
-
令DataQL變得靈活的存在
- if` `(boolean_expression) {``/* 如果布爾表達式為真將執行的語句 */ } ``else` `{``/* 如果布爾表達式為假將執行的語句 */ }
-
-
hint
- hint <選項名稱> = <選項值>
- 作用是設置一些執行查詢使用的選項參數??梢詤⒖歼@里
表達式
- 一元運算:!(對Boolean取反),-(對數值取相反數)
- 二元運算:主要是面向number,但是也支持字符串拼接。
- 三元運算:testExpr ? expr1 : expr2
訪問符
-
取值域:
- $:值域符A
- #:值域符B
- @:值域符C
- 應用場景:1. 獲取程序傳來的參數。2. 表達式中的訪問符。
-
獲取程序傳來的參數:<訪問符>{<參數名>} ,如${abc}
-
表達式中的訪問符:$(環境棧根),#(環境棧頂),@(整個環境棧(數組形態))
-
DataQL 查詢過程中一般情況下環境棧始終是空的,當遇到 => 操作時。
DataQL 會把 => 符左邊的表達式值放入環境棧,當轉換結束時 DataQL 會把表達式值從環境棧中刪掉。
如果在轉換過程中遇到第二次 => 操作,那么會在環境棧頂中放入新的數據。
-
取值與賦值
-
對象取值:return userInfo.username;
-
函數結果取值:return userByID({'id': 4}).username;
-
數組中取值:return userList()[0].username;
-
下標取值:return userInfo['username'];
-
連續下標取值:return userList[0]['username'];
-
數組下標取值:
- 正向索引:list[3] = 3,
- 反向索引:list[-3] = 7,
- 索引溢出:三種處理方式:throw,null,near(默認)
-
下標變量:使用變量代替下標
// 定義一個變量,變量表示要取值的字段名。 var columnKey = 'username'; // 通過下標變量方式來取值 return userInfo[columnKey]; -
賦值
- 在DataQL中數據是不可被修改的,只能重新生成數據集或者在結果轉換中對局部數據進行修改。
- 重定義: 在任何時候都可以通過var來重新定義一個已經存在的變量。
結果轉換
-
結果轉換是DataQL的核心能力,可以大體歸納為:組裝(憑空構造)和變換。
-
組裝:
-
case1:
var userName = "馬三"; // 姓名 var userAge = 23; // 年齡// 返回一個對象數據,將用戶名稱和年齡組裝到一個對象中 return {"name" : userName,"age" : userAge }; -
case2:
var data1 = 123; // 值1 var data2 = 456; // 值2// 返回2個元素的數組 return [data1, data2 ]; -
case3:
var data = [123, 456];//它組裝了兩個字段。但是兩個字段分別來自于同一個數組數據的不同元素 return {"element_0" : data[0], // 123"element_1" : data[1] // 456 };
-
-
數組的變換(是一個一個元素依次按照規則進行轉換的)
//首先我們有一個對象數組 var data = [{"userID" : 1234567891,"age" : 31,"name" : "this is name1.","nick" : "my name is nick1.","sex" : "F","status" : true },{"userID" : 1234567892,"age" : 32,"name" : "this is name2.","nick" : "my name is nick2.","sex" : "F","status" : true },{"userID" : 1234567893,"age" : 33,"name" : "this is name3.","nick" : "my name is nick3.","sex" : "M","status" : true } ]-
case1:
//得到一個新的數據集,只包含name和age字段 return data => [{"name", "age"} ]; -
case2:
//只包含name和age字段,同時修改一下字段名 return data => [{"userName" : name, // 取 name 字段的值作為 userName 的值"userAge" : age} ]; -
case3:
//返回所有用戶名的列表,得到字符串數組 return data => [ name ]; -
case4:
//將 一組值類型 變換成 一組對象 var data = ["馬三", "馬四"]; return data => [{"name" : # // 符號 "#" 表示在對每個元素進行轉換的過程中的那個元素本身。} ]; //得到的就是 [ {"name":"馬三"}, {"name":"馬四"} ] -
case5:
//為二維數組的每個值都加上一個字符串前綴 var data = [[1,2,3],[4,5,6],[7,8,9] ] return data => [# => [ // 在結果轉換中對當前元素進行二次轉換"值:" + #] ] //查詢結果: [["值:1","值:2","值:3"],["值:4","值:5","值:6"],["值:7","值:8","值:9"] ]
-
-
對象的變換
//首先我們有一個對象 var data = {"userID" : 1234567890,"age" : 31,"name" : "this is name.","nick" : "my name is nick.","sex" : "F","status" : true }-
case1:
//通過變換而非組裝的方式將其轉換為一個數組,內容是一個對象 return data => [ # ]; //---------結果--------- [{"userID": 1234567890,"age": 31,"name": "this is name.","nick": "my name is nick.","sex": "F","status": true} ] -
case2:
//對象的變換通常都是結構上的變化。對象可以是數組形式的一個元素,數組中可以疊加對象的變換 return data => {"name","info" : {"age","sex"} } //查詢結果 {"name": "this is name.","info": {"age": 31,"sex": "F"} }
-
-
使用表達式
-
case1:
//對結果變換過程中通過表達式來對字段重新計算 return data => {"name","age" : age + "歲","old" : age, // 這是有效的,age能夠使用多次"old" : name, // 這是無效的,"old"只會顯示上面那個"sex" : (sex == 'F') ? '男' : '女' }
-
-
通過Lambda模擬for循環
import 'net.hasor.dataql.fx.basic.CollectionUdfSource' as collect;var map = {"a" : 123,"b" : 321 } var data = [{"name" : "馬三","type" : "a"},{"name" : "n2","type" : "b"} ]var appendData = (dat) -> {//這個dat是data中的一個對象。lambda表達式只用處理一個元素即可,具體的整個數組的調用邏輯在return的時候。var newMap = collect.newMap(dat);run newMap.put('type',map[dat.type])//給這個newMap中設置'type'屬性,值是這樣取出來的(從map取出(key是(dat.type的value)的鍵值對)的值)。return newMap.data() };return data => [appendData(#) ] // 變量appendData定義為一個函數,這個函數有一個參數。 // 對data進行變換時,變換規則是之前定義的函數appendData, // 在這個函數中參數為當前data數組中的一個對象元素, // 所以在函數中是對一個對象進行操作,不用考慮數組的問題。 // 數組是在return的時候對person進行變換,對每個元素#調用定義的lambda函數appendData實現的。//執行結果 [{"name": "馬三","type": 123},{"name": "n2","type": 321} ] -
Tips:
- 變換要出現數組,則以[]包裹,對象則{}包裹,只有[]沒有{}的是字符串數組。在變換中能自由改名并使用表達式組合結果,并且可以一個元素多次使用,但是同名屬性只以第一次出現為準。
- 對數組的循環操作可以通過,在return中對數組取#實現對每個元素的更改。
函數
-
定義函數:可以直接用DataQL語言定義一個函數,然后在后續查詢中使用它。
var convertSex = (sex) -> {//定義一個函數return (sex == 'F') ? '男' : '女' };var data = {"userID" : 1234567890,"age" : 31,"name" : "this is name.","nick" : "my name is nick.","sex" : "F","status" : true };return data => {"name","age" : age + "歲","sex" : convertSex(sex)//使用函數得到值 } -
外部函數:DataQL具有官方標準函數庫,可以通過import語句導入。
//通過時間函數庫獲取時間 import 'net.hasor.dataql.fx.basic.DateTimeUdfSource' as time;return time.now();//通過json函數庫來生成JSON數據 import 'net.hasor.dataql.fx.basic.JsonUdfSource' as json;return json.toJson([0,1,2])// "[0,1,2]"- 開發者還可以自己編寫函數(編寫UDF)
-
Lambda寫法
import 'net.hasor.dataql.fx.basic.CollectionUdfSource' as collect; // 數據 var dataList = [{"name" : "馬一" , "age" : 18 },{"name" : "馬二" , "age" : 28 },{"name" : "馬三" , "age" : 30 },{"name" : "馬四" , "age" : 25 } ]//只保留年齡大于20歲的數據// 使用非Lambda的寫法---------------- // 年齡過濾邏輯 var filterAge = (dat) -> {return return dat.age > 20; }; // 調用 filter 函數 return collect.filter(dataList, filterAge);//使用Lambda的寫法----------------------省略了一個函數定義 var result = collect.filter(dataList, (dat) -> { // lambda 寫法return dat.age > 20;// 年齡過濾條件 });
混合其他語言
? 在DataQL中混合其他語言一起協同處理DataQL查詢,需要定義一個片段執行器。
-
典型的場景是把SQL語句混合在DataQL中。
var dataSet = @@sql(item_code) <%select * from category where co_code = #{item_code} %>return dataSet() => [{ "id","name","code","body" } ] // @@sql 是 FunctionX 擴展包中提供的一組片段執行器,這個片段執行器相當于讓 DataQL 有能力執行數據庫的 SQL 語句。 -
定義:定義一個片段執行器需要,實現 net.hasor.dataql.FragmentProcess 接口(更多信息請參考開發手冊)并且將其注冊到 DataQL 環境中
// 方式一:通過Dataql接口 FragmentProcess process = ... AppContext = appContext = ...DataQL dataQL = appContext.getInstance(DataQL.class);//獲取 DataQL 接口 dataQL.addFragmentProcess("sql", process); //注冊片段執行器// 方式二:通過QueryModule FragmentProcess process = ...public class MyQueryModule implements QueryModule {public void loadModule(QueryApiBinder apiBinder) {dataQL.addFragmentProcess("sql", process); //注冊片段執行器} } -
使用:
定義一個片段執行器需要使用 @@xxxx(arg1,arg2,arg3,…)<% … %> 語法,其中:
- xxxx 為片段執行器注冊的名稱。
- (arg1,arg2,arg3,…) 為執行這個代碼時傳入的參數列表。如果不需要定義任何參數可以是 ()
- 在 <% 和 %> 之間編寫的是 目標語言的代碼片段。
數據模型
DataQL的數據模型是通過net.hasor.dataql.domain.DataModel接口表示的,共計4個實現類:ValueModel,ListModel,ObjectModel,UdfModel。
還有一個非常重要的unwrap方法,能解除DataModel形態的封裝,直接變成Map/List結構,注意UdfModel類型解開是Udf接口。
- ValueModel:用于表示String、Number、Boolean、Null四種基本數據類型。有isXxx()方法用于判斷類型,asXxx()方法用于獲取對應值。
- ListModel:表示一個列表或集合的數據,相比較 DataModel 多了一組根據元素位置判斷對應類型的接口方法。
- ObjectModel:表示一個列表或集合的數據,相比較 DataModel 多了一組根據元素 Key 判斷對應類型的接口方法。
- UdfModel:當 DataQL 查詢返回一個 Udf 函數或者 Lambda 函數時,就會得到一個 UdfModel。而它事實上就是一個 Udf
開發手冊
執行查詢
? 引入依賴
<dependency><groupId>net.hasor</groupId><artifactId>hasor-dataql</artifactId><version>4.2.1</version> </dependency>通過Hasor使用DataQL
//由于 AppContext 有自身的聲明周期特性,因此需要做一個單例模式來創建 DataQL 接口。 public class DataQueryContext {private static AppContext appContext = null;private static DataQL dataQL = null;public static DataQL getDataQL() {if (appContext == null) {appContext = Hasor.create().build();dataQL = appContext.getInstance(DataQL.class);}return dataQL;} }//然后在Test中執行查詢 HashMap<String, Object> tempData = new HashMap<String, Object>() {{put("uid", "uid is 123");put("sid", "sid is 456"); }};DataQL dataQL = DataQueryContext.getDataQL(); Query dataQuery = dataQL.createQuery("return [${uid},${sid}]"); QueryResult queryResult = dataQuery.execute(tempData); DataModel dataModel = queryResult.getData(); List list = (List)dataModel.unwrap();for (Object o : list) {System.out.println(o);}通過JSR223使用DataQL:我這里略
基于底層接口使用DataQL
DataQL 的運行基于三個步驟:
- 1.解析DataQL查詢:把 DataQL 查詢字符串通過解析器解碼為 AST(抽象語法樹)QueryModel queryModel = QueryHelper.queryParser(query1);
- 2.編譯查詢:將DataQL 的 AST(抽象語法樹) 編譯為 QIL 指令序列。QIL qil = QueryHelper.queryCompiler(queryModel, ``null``, Finder.DEFAULT);
- 3.執行查詢:最后在根據 QIL 創建對應的 Query 接口即可。Query dataQuery = QueryHelper.createQuery(qil, Finder.DEFAULT);
查詢接口(Query)
無論使用何種方式查詢都會通過DataQL的查詢接口發出查詢指令。
查詢接口提供了三種不同參數類型的查詢重載,所有入參數最后都被轉換成為 Map 結構然后統一變換成為 CustomizeScope 數據域形式。
查詢結果(QueryResult)
發出DataQL查詢后,如果順利執行完查詢,結果會以QueryResult接口形式返回。
/** 執行結果是否通過 EXIT 形式返回的 */ public boolean isExit();/** 獲得退出碼。如果未指定退出碼,則默認值為 0 */ public int getCode();/** 獲得返回值 */ public DataModel getData();/** 獲得本次執行耗時 */ public long executionTime();DataQL 的所有返回值都會包裝成 DataModel 接口類型。如果想拿到 Map/List 結構數據,只需要調用 unwrap 方法即可。
全局變量
添加全局變量有兩種方式:
在QueryModule中初始化環節添加
AppContext appContext = Hasor.create().build((QueryModule) apiBinder -> {apiBinder.addShareVarInstance("global_var", "g1"); });通過DataQL接口添加
DataQL dataQL = appContext.getInstance(DataQL.class); dataQL.addShareVarInstance("global_var", "g2");獲取全局變量
return global_var;函數
-
開發Udf
一個Udf必須是實現了net.hasor.dataql.Udf接口,注冊Udf的方式和添加全局變量相同。
public class UserByIdUdf implements Udf {private UserManager userManager;public Object call(Hints readOnly, Object[] params) {return userManager.findById(params[0]);} } -
參數中的Udf
DataQL 允許在執行查詢時通過參數形式提供 Udf ,這種方式傳入的 Udf 在調用時也需要使用 ${…} 來獲取
HashMap<String, Object> tempData = new HashMap<String, Object>() {{put("findUserById", new UserByIdUdf()); }};AppContext appContext = Hasor.create().build(); DataQL dataQL = appContext.getInstance(DataQL.class);//得到 DataQL接口 Query dataQuery = dataQL.createQuery("return ${findUserById}(1) => { 'name','sex' }"); // 創建查詢 QueryResult queryResult = dataQuery.execute(tempData); DataModel dataModel = queryResult.getData(); -
函數包(UdfSource)
UdfSource 是一個函數包接口,接口中只有一個 getUdfResource 方法,用于返回函數包中的所有 Udf(Map形式返回)但是一般情況下更推薦使用 UdfSourceAssembly 接口。
使用函數包的好處是可以像平常開發一樣編寫 Udf,無需考慮 Udf 接口的細節。裝配器會自動幫助進行參數和結果的轉換。
public class DateTimeUdfSource implements UdfSourceAssembly {/** 返回當前時間戳 long 格式 */public long now() { ... }/** 返回當前系統時區的:年 */public int year(long time) { ... }/** 返回當前系統時區的:月 */public int month(long time) { ... }/** 返回當前系統時區的:日 */public int day(long time) { ... }... } // 最后在查詢中通過 <函數包名>.<函數> 的形式調用函數包。 -
inport導入(函數/函數包)
如果 Classpath 中已經存在某個 Udf 類,還可以通過 import 語句導入使用。
import 'net.xxxx.foo.udfs.UserByIdUdf' as findUserById; return findUserById(1) => { 'name','sex' };函數包的導入語句相同,只是在調用函數包中函數的時需要指明函數包
import 'net.xxxx.foo.udfs.DateTimeUdfSource' as timeUtil; return timeUtil.now(); -
使用注解批量注冊
通過 @DimUdf 注解可以快速的聲明函數
@DimUdf("findUserById") public class UserByIdUdf implements Udf {private UserManager userManager;public Object call(Hints readOnly, Object[] params) {return userManager.findById(params[0]);} }通過 @DimUdfSource 注解可以快速的聲明函數包:
@DimUdfSource("time_util") public class DateTimeUdfSource implements UdfSourceAssembly {... }然后在初始化時掃描加載它們
AppContext appContext = Hasor.create().build(apiBinder -> {QueryApiBinder queryBinder = apiBinder.tryCast(QueryApiBinder.class);queryBinder.loadUdf(queryBinder.findClass(DimUdf.class));queryBinder.loadUdfSource(queryBinder.findClass(DimUdfSource.class)); });
外部代碼片段
-
外部代碼執行器
外部代碼片段是 DataQL 特有能力,它允許在 DataQL 查詢中混合其它語言的腳本。并將引入的外部語言腳本轉換為 Udf 形式進行調用。使用這一特性時需要擴展 FragmentProcess 接口,并注冊對應的外部代碼執行器。
//外部代碼執行器,接收<% %>包裹的代碼,然后調用jdbcTemplate的query方法執行具體的SQL查詢 @DimFragment("sql") public class SqlQueryFragment implements FragmentProcess {@Injectprivate JdbcTemplate jdbcTemplate;public Object runFragment(Hints hint, Map<String, Object> paramMap, String fragmentString) throws Throwable {return this.jdbcTemplate.queryForList(fragmentString, paramMap);} }//在初始化階段注冊這個代碼執行器,就可以在查詢時使用了這個外部代碼片段了 public class MyFragment implements QueryModule {public void loadModule(QueryApiBinder apiBinder) {//掃描所有標記了@DimFragment注解的類并加載它apiBinder.loadFragment(queryBinder.findClass(DimFragment.class));} }//DataQL語句,通過@@指令開啟了一段外部代碼的定義,執行器的名字是sql var dataSet = @@sql(item_code) <%select * from category where co_code = #{item_code} %>return dataSet() => [{ "id","name","code","body" } ] -
資源加載器(Finder)
資源加載器是net.hasor.dataql.FInder,其主要負責import語句導入資源/對象的加載。通常不會接觸到它。
import ``'userBean'` `as ub;``//userBean 是 Bean 的名字 return` `ub().name;
SQL執行器
SQL 執行器是 DataQL 的一個 FragmentProcess 擴展,其作用是讓 DataQL 可以執行 SQL。執行器的實現是 FunctionX 擴展包提供的。使用執行器需要引入擴展包。
<dependency><groupId>net.hasor</groupId><artifactId>hasor-dataql-fx</artifactId><version>4.2.1</version> </dependency>功能與特性
- 支持兩種模式:簡單模式、分頁模式
- 簡單模式下,使用原生SQL。100% 兼容所有數據庫
- 分頁模式下,自動改寫分頁SQL。并兼容多種數據庫
- 支持參數化 SQL,更安全
- 支持 SQL 注入,更靈活
- 支持批量 CURD
配置和方言
-
配置數據源
//普通方式配置數據源,在Hasor中初始化數據源即可 public class ExampleModule implements Module {public void loadModule(ApiBinder apiBinder) throws Throwable {// .創建數據源DataSource dataSource = null;// .初始化Hasor Jdbc 模塊,并配置數據源apiBinder.installModule(new JdbcModule(Level.Full, this.dataSource));} } -
方言
配置方言使用 hint HASOR_DATAQL_FX_PAGE_DIALECT = mysql,即可設置方言。支持Mysql,Oracle,SqlServer2012,PostgreSQL,DB2,Infomix。
執行SQL
-
執行SQL
// 聲明一個 SQL var dataSet = @@sql() <%select * from category limit 10; %>// 執行這個 SQL,并返回結果 return dataSet(); -
SQL參數化
// 聲明一個 SQL var dataSet = @@sql(itemCode) <%select * from category where co_code = #{itemCode} limit 10; %>// 執行這個 SQL,并返回結果 return dataSet(${itemCode}); -
SQL注入
//SQL注入是為了一些特殊場景需要拼接SQL而準備的,如:動態排序字段和排序規則 // 使用 DataQL 拼接字符串 var orderBy = ${orderField} + " " + ${orderType};// 聲明一個可以注入的 SQL var dataSet = @@sql(itemCode,orderString) <%select * from category where co_code = #{itemCode} order by ${orderString} limit 10; %>// 執行這個 SQL,并返回結果 return dataSet(${itemCode}, orderBy); -
Ognl表達式
//同Mybatis一樣,SQL執行器可以將一個對象作為參數傳入 // 例子數據 var testData = {"name" : "馬三","age" : 26,"status" : 0 }// insert語句模版 var insertSQL = @@sql(userInfo) <%insert into user_info (name,age,status,create_time) values (#{userInfo.name},#{userInfo.age},#{userInfo.status},now()) %>// 插入數據 return insertSQL(testData); -
批量操作
DataQL 的 SQL 執行器支持批量 Insert\Update\Delete\Select 操作,最常見的場景是批量插入數據。批量操作必須滿足下列幾點要求:
- 入參必須是 List
- 如果有多個入參。所有參數都必須是 List 并且長度必須一致。
- @@sql()<% … %> 寫法升級為批量寫法 @@sql[]()<% … %>
- 如果批量操作的 SQL 中存在 SQL注入,那么批量操作會自動退化為:循環遍歷模式
- 由于批量操作底層執行SQL使用java.sql.Statement.executeBatch方法,因此insertSQL的返回值是int數組。
-
執行結果拆包
拆包是指將只返回一行一列的數據如count(*),拆解為int類型。
有三種模式,默認為column:
- off:不拆包,嚴格返回一個對象數組。
- row:最小粒度到行,多條記錄時正常返回,返回0或1條記錄時,返回一個Object。
- column:最小粒度到列,當返回結果只有一行一列時,返回具體值。
-
結果列名拼寫轉換
是指從數據庫查詢返回的列名信息,按照某一規則統一處理,如所有key轉為駝峰。可以使返回的列信息具有很高的可讀性。
hint FRAGMENT_SQL_COLUMN_CASE = "hump"
幾個可供配置的值:
- default:保持原樣,這是個默認設置
- upper:全部轉大寫
- lower:全部轉小寫
- hump:轉換成駝峰
-
分頁查詢
默認關閉,通過hint FRAGMENT_SQL_QUERY_BY_PAGE = true打開。
打開分頁后經過3個步驟:
- 定義分頁SQL
- 創建分頁查詢對象
- 設置分頁信息
- 執行分頁查詢
由于大部分前端是以1為第一頁,而默認情況下SQL執行器是以0為第一頁的,所以需要-1,如果是GET方式發布的話,還需要使用轉換函數。
import 'net.hasor.dataql.fx.basic.ConvertUdfSource' as convert; hint FRAGMENT_SQL_QUERY_BY_PAGE = true ... run queryPage.setPageInfo({"pageSize" : 5, // 頁大小"currentPage" : (convert.toInt(${pageNumber}) -1) });還有第二種方式,DataQL在4.1.8版本中加入FRAGMENT_SQL_QUERY_BY_PAGE_NUMBER_OFFSETHint,可以設置讓SQL執行器以1作為開始。
-
數據庫事務
SQL執行器本身并不支持事務,需要借助事務函數來實現。
事務函數還可以嵌套使用。
import 'net.hasor.dataql.fx.db.TransactionUdfSource' as tran; //引入事務函數 ... return tran.required(() -> {... // 事務return ... }); ...支持完整的7個傳播屬性:
類型說明用法 REQUIRED 加入已有事務 tran.required(() -> { … }); REQUIRES_NEW 獨立事務 tran.requiresNew(() -> { … }); NESTED 嵌套事務 tran.nested(() -> { … }); SUPPORTS 跟隨環境 tran.supports(() -> { … }); NOT_SUPPORTED 非事務方式 tran.notSupported(() -> { … }); NEVER 排除事務 tran.never(() -> { … }); MANDATORY 要求環境中存在事務 tran.tranMandatory(() -> { … }); -
多數據源
SQL執行器在4.1.4版本之后提供了通過hint來切換數據源的能力
public class MyModule implements Module {public void loadModule(ApiBinder apiBinder) throws Throwable {DataSource defaultDs = ...;DataSource dsA = ...;DataSource dsB = ...;apiBinder.installModule(new JdbcModule(Level.Full, defaultDs)); // 默認數據源apiBinder.installModule(new JdbcModule(Level.Full, "ds_A", dsA)); // 數據源AapiBinder.installModule(new JdbcModule(Level.Full, "ds_B", dsB)); // 數據源B} } // 如果不設置 FRAGMENT_SQL_DATA_SOURCE 使用的是 defaultDs 數據源。 // - 設置值為 "ds_A" ,使用的是 dsA 數據源。 // - 設置值為 "ds_B" ,使用的是 dsB 數據源。 hint FRAGMENT_SQL_DATA_SOURCE = "ds_A"// 聲明一個 SQL var dataSet = @@sql() <% select * from category limit 10; %> // 使用 特定數據源來執行SQL。 return dataSet(); -
多條查詢
是指一次SQL執行的過程中,包含了一個以上的SQL語句。
var dataSet = @@sql() <%set character_set_connection = 'utf8';select * from my_option; %>return dataSet(); // 默認返回最后一個SQL語句的結果。 // 可以通過 FRAGMENT_SQL_MUTIPLE_QUERIES hint來控制,例如:保留每一條結果。
拆包模式可以通過hint改變,hint FRAGMENT_SQL_OPEN_PACKAGE = 'row'
//hint FRAGMENT_SQL_OPEN_PACKAGE = "off" var dataSet = @@sql() <% select count(*) as cnt from category; %> var result = dataSet(); // 不指定 hint 的情況下,會返回 category 表的總記錄數,返回值為:10。 // 拆包模式變更為 row ,返回值為: { "cnt" : 10 } // 關閉拆包,返回值為標準的 List/Map: [ { "cnt" : 10 } ]Mybatis執行器
在4.1.8版本后加入了@@Mybatis執行器,這是對@@sql執行器的擴展,繼承了@@sql的能力,并提供了Mybatis的配置方式,提供了動態SQL的能力。
-
對比@@sql的優勢
- 繼承 @@sql 全部能力
- 提供動態SQL能力,提供 SQL 層面的 if 和 for
- 類似 mybatis 的工作方式,比起 DataQL 拼接字符串注入更加安全可靠。
-
提供的標簽
-
select
-
update
-
insert
-
delete
-
foreach:循環拼接SQL
- collection:集合,必填
- item:item,必填
- open:起始,選填
- close:結束,選填
- separator:分隔符,選填
-
if:判斷條件,成立時拼接標簽內內容
- test:判斷條件,必填
-
FunctionX庫函數
依賴:
<dependency><groupId>net.hasor</groupId><artifactId>hasor-dataql-fx</artifactId><version>4.2.1</version> </dependency>轉換函數庫
引入轉換函數庫:import 'net.hasor.dataql.fx.basic.ConvertUdfSource' as convert;
- toInt(target):將Object轉換為Number,0x12->18,""->0,“abc”->throw error
- toString(target):將Object轉換為String,null->“null”,[1,2,3,4]->"[1, 2, 3, 4]",{“test”:123}->"{test=123}"
- toBoolean(target):將Object轉換為Boolean,支持on,off
- byteToHex(target):將二進制數據轉換為十六進制字符串,將List<Byte>轉換為String。
- hexToByte(target):將十六進制字符串轉換為二進制數據,將String轉換為List<Byte>
- stringToByte(target, charset):將字符串轉換為二進制數據,將String轉換為List<Byte>,charset是String類型的字符集名稱,如’utf-8’
- ByteToString(target, charset):將二進制數據轉換為字符串
集合函數庫
引入集合函數庫:import 'net.hasor.dataql.fx.basic.CollectionUdfSource' as collect;
-
isEmpty:注意:collect.isEmpty(null) = false// 不支持的基本類型會返回 false
-
size
-
merge:返回 List
-
mergeMap
-
filter(dataList, filterUDF):根據一個規則來對集合進行過濾。filterUDF類型:Udf/Lambda
var result = collect.filter(dataList, (dat) -> {return dat.age > 20; }); -
filterMap(dataMap, keyFilterUDF)
-
limit(dataList, start, limit):截取List的一部分,返回一個List
-
newList
-
newMap(target):將一個map創建為帶狀態的Map,具有put(),putAll(),data(),size()方法
-
mapJoin(data_1,data_2,joinMapping):將兩個Map/List進行左連接,joinMaping是Map類型,表示兩表的join關系。
import 'net.hasor.dataql.fx.basic.CollectionUdfSource' as collect;var year2019 = [{ "pt":2019, "item_code":"code_1", "sum_price":2234 },{ "pt":2019, "item_code":"code_2", "sum_price":234 },{ "pt":2019, "item_code":"code_3", "sum_price":12340 },{ "pt":2019, "item_code":"code_4", "sum_price":2344 } ]; var year2018 = [{ "pt":2018, "item_code":"code_1", "sum_price":1234.0 },{ "pt":2018, "item_code":"code_2", "sum_price":1234.0 },{ "pt":2018, "item_code":"code_3", "sum_price":1234.0 },{ "pt":2018, "item_code":"code_4", "sum_price":1234.0 } ];var result = collect.mapJoin(year2019,year2018, { "item_code":"item_code" }) => [{"商品Code": data1.item_code,"去年同期": data2.sum_price,"今年總額": data1.sum_price,"環比去年增長": ((data1.sum_price - data2.sum_price) / data2.sum_price * 100) + "%"} ] return result; -
mapKeyToLowerCase:將Map的Key全部轉為小寫,如果Key沖突會產生覆蓋
-
mapKeyToUpperCase:將Map的Key全部轉為大寫,如果Key沖突會產生覆蓋
-
mapKeyToHumpCase:將Map的Key中下劃線轉為駝峰,如果Key沖突會產生覆蓋
-
mapKeys:提取Map的Key,返回List
-
mapValues:提取Map的Values,返回List
-
mapKeyReplace(dataMap, replaceKey):循環遍歷每一個Map元素,并對Map的Key進行替換,replaceKey是用于生成新key的函數。
var data = {"key1":1, "key2":2, "key3":3 }; var result = collect.mapKeyReplace(data, (oldKey,value) -> {return "new_" + oldKey }); // result = {"new_key1":1, "new_key2":2, "new_key3":3 } -
mapValueReplace:同上,不過是對值的處理
-
list2map(listData, dataKey, convertUDF):將List轉為Map,dataKey是鍵的名字,可以用字符串,也可以直接獲取,convertUDF是轉換的函數,可以不寫。
//通過字符串指明Key字段 var yearData = [{ "pt":2018, "item_code":"code_1", "sum_price":12.0 },{ "pt":2018, "item_code":"code_2", "sum_price":23.0 },{ "pt":2018, "item_code":"code_3", "sum_price":34.0 },{ "pt":2018, "item_code":"code_4", "sum_price":45.0 } ];var result = collect.list2map(yearData, "item_code"); // result = { // "code_1": { "pt":2018, "item_code":"code_1", "sum_price":12.0 }, // "code_2": { "pt":2018, "item_code":"code_2", "sum_price":23.0 }, // "code_3": { "pt":2018, "item_code":"code_3", "sum_price":34.0 }, // "code_4": { "pt":2018, "item_code":"code_4", "sum_price":45.0 } // };//使用提取出來的值作為key var yearData = [ 1,2,3,4,5]; var result = collect.list2map(yearData, (idx,dat)-> {// Key 提取函數,直接把數組的數字元素內容作為 key 返回return dat; },(idx,dat) -> {// 構造 valuereturn { "index": idx, "value": dat }; });// result = { // "1": { "index": 0, "value": 1 }, // "2": { "index": 1, "value": 2 }, // "3": { "index": 2, "value": 3 }, // "4": { "index": 3, "value": 4 }, // "5": { "index": 4, "value": 5 } // } -
map2list(dataMap, convert):將List轉換為Map,convert是轉換函數。
// 不指定轉換函數 var data = {"key1":1, "key2":2, "key3":3 }; var result = collect.map2list(data); // result = [ // { "key": "key1", "value": 1}, // { "key": "key2", "value": 2}, // { "key": "key3", "value": 3} // ]// 指定轉換函數 var data = {"key1":1, "key2":2, "key3":3 }; var result = collect.map2list(data, (key,value) -> {return { "k" : key, "v" : value }; }); // result = [ // { "k": "key1", "v": 1}, // { "k": "key2", "v": 2}, // { "k": "key3", "v": 3} // ] -
map2string(dataMap, joinStr, convert):將Map轉換成字符串,通常在生成Url參數時用到,joinStr表示連接符。
var data = {"key1":1, "key2":2, "key3":3 }; var result = collect.map2string(data,"&",(key,value) -> {return key + "=" + value; }); // result = "key1=1&key2=2&key 3=3" -
mapSort(dataMap, sortUdf):DataQL中的Map是有序的,因此可以排序。
-
listSort(dataList, sortUdf):對List進行排序
-
groupBy(dataList, groupByKey):根據公共字段對數據進行分組。groupByKey是String是要分組的字段名。數據集中需要有一個公共字段。
-
uniqueBy(dataList, uniqueByKey):根據公共字段去重,只返回第一次出現的。數據集中需要有一個公共字段。
時間日歷函數庫
引入函數庫:import 'net.hasor.dataql.fx.basic.DateTimeUdfSource' as time;
- now:返回當前時間戳
- year(time):返回時間戳中的年份,獲取當前年份:time.year(time.now())
- month(time):返回時間戳中的月份。
- day、dayOfMonth:返回時間戳中的日期是這個月的第幾天。
- hour
- minute
- second
- dayOfYear(time):返回時間戳中的日期是全年的第幾天。
- dayOfWeek(time):返回時間戳中的日期是這周的第幾天,SUNDAY=1。
- format(time, pattern):對時間戳進行時間日期格式化,底層使用的是java.text.SimpleDateFormat
- parser(time, pattern):對時間按照格式進行解析,解析為時間戳。
Json函數庫
引入函數庫:import 'net.hasor.dataql.fx.basic.JsonUdfSource' as json;
- toJson(target):返回String,把對象JSON序列化。
- toFmtJson(target):返回String,把對象JSON序列化(帶格式)。
- fromJson(jsonString):把JSON格式的字符串解析成對象。
字符串函數庫
引入函數庫:import 'net.hasor.dataql.fx.basic.StringUdfSource' as string;
- startsWith(str, prifix):是否以xxx開頭
- startsWithIgnoreCase
- endsWith(str, prifix):是否以xxx結尾
- endsWithIgnoreCase
- lineToHump(str):下劃線轉駝峰
- humpToLine
- firstCharToUpperCase(str):首字母大寫
- firstCharToLowerCase
- toUpperCase
- toLowerCase
- indexOf(str, searchStr):查找第一次出現的位置
- indexOfWithStart(str, searchStr, startPos):從某一位置開始,查找之后第一次出現的位置
- indexOfIgnoreCase
- indexOfIgnoreCaseWithStart
- lastIndexOf
- lastIndexOfWithStart
- lastIndexOfIgnoreCase
- lastIndexOfIgnoreCaseWithStart
- contains(str, searchStr):是否包含字符串。
- containsIgnoreCase
- containsAny(str, searchStrArray):是否包含,指定List中的值。
- containsAnyIgnoreCase
- trim
- sub(str, start, end):獲取指定位置的子串。
- left(str, len):獲取最左邊的指定長度的串。
- right
- alignRight:右對齊,不足的向右填充
- alignLeft
- alignCenter
- compareString:比較兩個字符串大小
- compareStringIgnoreCase
- split
- join(array, separator):用separator將array拼裝成字符串
- isEmpty
- equalsIgnoreCase
狀態函數庫
引入函數庫:import 'net.hasor.dataql.fx.basic.StateUdfSource' as state;
- decNumber(initValue):返回一個 Udf,每次調用這個 UDF,都會返回一個 Number。Number 值較上一次會自增 1。
- incNumber(initValue):返回一個 Udf,每次調用這個 UDF,都會返回一個 Number。Number 值較上一次會自減 1。
- uuid():返回一個完整格式的UUID字符串
- uuidToShort():返回一個不含’-'的UUID字符串
Web函數庫
引入函數庫:import 'net.hasor.dataql.fx.web.WebUdfSource' as webData;
- cookieMap
- cookieArrayMap
- getCookie
- getCookieArray
- tempCookie
- tempCookieAll
- storeCookie
- removeCookie
- headerMap
- headerArrayMap
- getHeader
- getHeaderArray
- setHeaderAll
- addHeader
- addHeaderAll
- sessionKeys
- getSession
- setSession
- removeSession
- cleanSession
- sessionInvalidate
- sessionId
- sessionLastAccessedTime
簽名/編碼函數庫
引入函數庫:import 'net.hasor.dataql.fx.encryt.CodecUdfSource' as codec;
- encodeString
- decodeString
- encodeBytes
- decodeBytes
- urlEncode
- urlEncodeBy
- urlDecode
- urlDecodeBy
- digestBytes
- digestString
- hmacBytes
- hmacString
總結
- 上一篇: QT运行时的Debug、Release、
- 下一篇: gpu浮点计算能力floaps_认识GP