当前位置: 首页 > 工具软件 > DataQL > 使用案例 >

DataQL

苏宾鸿
2023-12-01

简介

DataQL(Data Query Language)DataQL 是一种查询语言。旨在通过提供直观、灵活的语法来描述客户端应用程序的数据需求和交互。

数据的存储根据其业务形式通常是较为简单的,并不适合直接在页面上进行展示。因此开发页面的前端工程师需要为此做大量的工作,这就是 DataQL 极力解决的问题。

请注意 DataQL 不是一门编程语言,它是查询语言。它对逻辑的处理仅限于简单场景。DataQL 的解决问题的重点集中在:数据的聚合和转换以及过程中的简单加工处理。

特性

  1. 层次结构:多数产品都涉及数据的层次结构,为了保证结构的一致性 DataQL 结果也是分层的。
  2. 数据为中心:前端工程是一个比较典型的场景,但是 DataQL 不局限于此(后端友好性)。
  3. 弱类型定义:语言中不会要求声明任何形式的类型结构。
  4. 简单逻辑:具备简单逻辑处理能力:表达式计算、对象取值、条件分支、lambda和函数。
  5. 编译运行:查询的执行是基于编译结果的。
  6. 混合语言:允许查询中混合任意的其它语言代码,典型的场景是查询中混合 SQL 查询语句。
  7. 类 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`。

  • 分隔符:()、{}、[]、,、:、;(非必须)

  • 运算符:

    1. 数学运算:+,-,*,/,\(整除),%
    2. 位运算:&,|,!,^(异或),<<(左位移),>>(有符号右位移),>>>(无符号右位移)
    3. 比较运算:>,>=,<,<=,==,!=
类型系统

DataQL 是弱类型定义的查询语言,在DataQL 中所有数据都会被归结到有限的几种类型上。无需定义数据类型结构,在弱类型系统中编写查询会非常方便,它去掉了繁杂的类型定义。

数据类型表示方式详情
布尔truefalse表示真假值
数值负无穷大0正无穷大浮点数、整数、科学计数法表示的数
字符串’ 或 “字符串 或 单个字符
空值nullNULL空值
集合[…] 数组 或 多维数组带有顺序的多组数据的集合
对象{‘key’:…} 具有键值对的数据体DataQL 的对象不支持方法,但是可以具备 Udf 类型的属性。
UDFlambda 函数 或 一个外部的 Udf一个外部的 net.hasor.dataql.Udf 接口函数定义。
DataQL 中书写的 lambda 函数也被称作为 UDF。
一个扩展代码片段的定义,也属于 UDF 的范畴。
  • 数值类型

    • 二进制表示法:0b010101011000B01010101100
    • 十进制表示法:0o12345670O1234567
    • 八进制表示法:-0000234123
    • 十六进制表示法:0x123450X12345
    • 科学计数法: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:

    1. 变换要出现数组,则以[]包裹,对象则{}包裹,只有[]没有{}的是字符串数组。在变换中能自由改名并使用表达式组合结果,并且可以一个元素多次使用,但是同名属性只以第一次出现为准。
    2. 对数组的循环操作可以通过,在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>
  1. 通过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);
            }
    
  2. 通过JSR223使用DataQL:我这里略

  3. 基于底层接口使用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);
  4. 查询接口(Query)
    无论使用何种方式查询都会通过DataQL的查询接口发出查询指令。

    查询接口提供了三种不同参数类型的查询重载,所有入参数最后都被转换成为 Map 结构然后统一变换成为 CustomizeScope 数据域形式。

  5. 查询结果(QueryResult)

    发出DataQL查询后,如果顺利执行完查询,结果会以QueryResult接口形式返回。

    /** 执行结果是否通过 EXIT 形式返回的 */
    public boolean isExit();
     
    /** 获得退出码。如果未指定退出码,则默认值为 0 */
    public int getCode();
     
    /** 获得返回值 */
    public DataModel getData();
     
    /** 获得本次执行耗时 */
    public long executionTime();
    

    DataQL 的所有返回值都会包装成 DataModel 接口类型。如果想拿到 Map/List 结构数据,只需要调用 unwrap 方法即可。

全局变量

添加全局变量有两种方式:

  1. 在QueryModule中初始化环节添加

    AppContext appContext = Hasor.create().build((QueryModule) apiBinder -> {
        apiBinder.addShareVarInstance("global_var", "g1");
    });
    
  2. 通过DataQL接口添加

    DataQL dataQL = appContext.getInstance(DataQL.class);
    dataQL.addShareVarInstance("global_var", "g2");
    
  3. 获取全局变量

    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

    1. off:不拆包,严格返回一个对象数组。
    2. row:最小粒度到行,多条记录时正常返回,返回0或1条记录时,返回一个Object。
    3. 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_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)); // 数据源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
 类似资料:

相关阅读

相关文章

相关问答