简介
DataQL(Data Query Language)DataQL 是一种查询语言。旨在通过提供直观、灵活的语法来描述客户端应用程序的数据需求和交互。
数据的存储根据其业务形式通常是较为简单的,并不适合直接在页面上进行展示。因此开发页面的前端工程师需要为此做大量的工作,这就是 DataQL 极力解决的问题。
请注意 DataQL 不是一门编程语言,它是查询语言。它对逻辑的处理仅限于简单场景。DataQL 的解决问题的重点集中在:数据的聚合和转换以及过程中的简单加工处理。
特性
- 层次结构:多数产品都涉及数据的层次结构,为了保证结构的一致性 DataQL 结果也是分层的。
- 数据为中心:前端工程是一个比较典型的场景,但是 DataQL 不局限于此(后端友好性)。
- 弱类型定义:语言中不会要求声明任何形式的类型结构。
- 简单逻辑:具备简单逻辑处理能力:表达式计算、对象取值、条件分支、lambda和函数。
- 编译运行:查询的执行是基于编译结果的。
- 混合语言:允许查询中混合任意的其它语言代码,典型的场景是查询中混合 SQL 查询语句。
- 类 JS 语法:类JS语法设计,学习成本极低。
语法手册
词法记号
-
注释支持://、/*…*/
-
支持任意空格,提高可读性,支持\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,…) 为执行这个代码时传入的参数列表。如果不需要定义任何参数可以是 ()
- 在 <% 和 %> 之间编写的是 目标语言的代码片段。
// 在 MySQL 中插入一条数据,并返回自增的ID var saveData = @@sql(data) <% insert into my_option ( `key`, `value`, `desc` ) values ( #{data.key}, #{data.value}, #{data.desc} ); select LAST_INSERT_ID(); %> return saveData(${root});
数据模型
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);
- 1.解析DataQL查询:把 DataQL 查询字符串通过解析器解码为 AST(抽象语法树)
-
查询接口(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 { @Inject private 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数组。
// 例子数据 var testData = [ { "name" : "马一", "age" : 26, "status" : 0 }, { "name" : "马二", "age" : 26, "status" : 0 }, { "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);
-
执行结果拆包
拆包是指将只返回一行一列的数据如count(*),拆解为int类型。
有三种模式,默认为column:
- off:不拆包,严格返回一个对象数组。
- row:最小粒度到行,多条记录时正常返回,返回0或1条记录时,返回一个Object。
- column:最小粒度到列,当返回结果只有一行一列时,返回具体值。
拆包模式可以通过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 } ]
-
结果列名拼写转换
是指从数据库查询返回的列名信息,按照某一规则统一处理,如所有key转为驼峰。可以使返回的列信息具有很高的可读性。
hint FRAGMENT_SQL_COLUMN_CASE = "hump"
几个可供配置的值:
- default:保持原样,这是个默认设置
- upper:全部转大写
- lower:全部转小写
- hump:转换成驼峰
-
分页查询
默认关闭,通过
hint FRAGMENT_SQL_QUERY_BY_PAGE = true
打开。打开分页后经过3个步骤:
- 定义分页SQL
- 创建分页查询对象
- 设置分页信息
- 执行分页查询
// SQL 执行器切换为分页模式 hint FRAGMENT_SQL_QUERY_BY_PAGE = true // 定义查询SQL var dataSet = @@sql() <% select * from category %> // 创建分页查询对象 var pageQuery = dataSet();//从数据库查出来的是一个对象,有多种属性,而不是仅有数据 // 设置分页信息 run pageQuery.setPageInfo({ "pageSize" : 10, // 页大小 "currentPage" : 3 // 第3页 }); // 执行分页查询 var result = pageQuery.data(); // 获取分页信息 var info = pageQuery.pageInfo();
由于大部分前端是以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_OFFSET
Hint,可以设置让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)); // 数据源A apiBinder.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来控制,例如:保留每一条结果。
Mybatis执行器
在4.1.8版本后加入了@@Mybatis执行器,这是对@@sql执行器的扩展,继承了@@sql的能力,并提供了Mybatis的配置方式,提供了动态SQL的能力。
-
对比@@sql的优势
- 继承 @@sql 全部能力
- 提供动态SQL能力,提供 SQL 层面的 if 和 for
- 类似 mybatis 的工作方式,比起 DataQL 拼接字符串注入更加安全可靠。
var dimSQL = @@mybatis(userName)<% <select> select * from user_info where `name` like concat('%',#{userName},'%') order by id asc </select> %>;
-
提供的标签
-
select
-
update
-
insert
-
delete
-
foreach:循环拼接SQL
- collection:集合,必填
- item:item,必填
- open:起始,选填
- close:结束,选填
- separator:分隔符,选填
<foreach collection="userIds.split(',')" item="userId" open="(" close=")" separator=","> #{userId} </foreach>
-
if:判断条件,成立时拼接标签内内容
- test:判断条件,必填
<if test="userId != null and userId != ''"> and user_id = #{userId} </if>
-
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) -> { // 构造 value return { "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