前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >使用 QueryBuilder 构造复杂的数据筛选语句

使用 QueryBuilder 构造复杂的数据筛选语句

原创
作者头像
windseeker
发布2021-12-01 14:36:55
6K0
发布2021-12-01 14:36:55
举报

QueryBuilder 是一个常用的过滤器的 UI 组件,本文从前后端和数据库查询的角度总结了一些使用经验,包括一些踩坑的心得。

QueryBuilder 是什么?

引用 jQuery QueryBuilder 的定义

QueryBuilder 是一个用于创建查询和过滤器的 UI 组件。

  • 它可以用于高级搜索的引擎页面、管理端等。
  • 它是高度可定制的,并可插入许多小部件,如 sliders 滑块和日期选择器。
  • 它输出一个结构化的 JSON 规则,可以很容易地解析来创建 SQL/NoSQL/ 任何查询。
  • 它还附带了一些插件,并有一个完整的事件系统来支持更多的功能。
query-builder
query-builder

QueryBuilder 组件一般多用于数据筛选,它以 AND OR NOT 的嵌套组合,让非专业的人也能构造复杂的数据查询语句。在问卷系统中,就有不少的地方需要使用到这个组件,本文就从最开始的技术选型到上线总结一下其中的一些关键技术点。

需求场景

一般来说,一个专业的问卷系统都需要满足大量的数据筛选和清洗的工作,而 QueryBuilder 正是交互的第一步。在问卷的回收过程中,我们需要直接根据用户设置的条件进行答案的过滤,如下图:

数据筛选
数据筛选

在答题者提交问卷之后,便会直接在后台根据 QueryBuilder 生成的规则进行 运算,并且标记该份答案是 "有效/无效",一般多用于根据答题者的答题认真程度进行发奖、招募等场景。因为这种筛选是在 api 侧实时运算的,需要直接根据答案的值解析 QueryBuilder 规则。

而数据清洗的功能则是在管理端异步任务中计算的,一般用于生成报表或者批量导出部分数据使用,它是针对所有回收的问卷进行清洗,所以需要将 QueryBuilder 规则转换成相应的查询语句,比如我们主要的分析工具是 es ,那么就要转换成 es 对应的 DSL 语句。

技术调研

通过需求场景可以看出,虽然是同样的交互,但是不同的使用场景,底层需要做的事情是完全不一样的,所以我们技术调研时需要考虑的核心点就是扩展性,其一是 UI 组件是否能方便扩展新的规则(例如问卷中需要计算2个数组交集、字符串长度等等); 其二是 QueryBuilder 规则存储的数据结构能否便捷的转换成对应的语法,如 mongo、es 等;最后还有非常重要的一点就是,是否有后端解析库的支持,比如支持在我们使用的主要语言 go 中直接计算出结果。

综上,最终我们确定使用的是 react-awesome-query-builder,它不仅能通过简单配置扩展 UI 规则,还内置了很多转换器,可以直接将 UI 组件的数据转换成 mysql/mongo/es 的查询语句。而且还可以将 QueryBuilder 规则转换成 jsonLogic,这是一种用 json 构造的语法树,最主要优势是语言无关、前后端通用,jsonLogic 虽然不支持复杂的语法:setters、循环、函数等,但对于 QueryBuilder 这种主要为 AND OR NOT 的语法完全够用了,看看它的语法:

代码语言:txt
复制
var rules = { "and" : [
  {"<" : [ { "var" : "temp" }, 110 ]},
  {"==" : [ { "var" : "pie.filling" }, "apple" ] }
] };

var data = { "temp" : 100, "pie" : { "filling" : "apple" } };

jsonLogic.apply(rules, data); // true

非常简单明了,jsonLogic 官方有 js/php/python/ruby 对应的解析库,只可惜没有 go 的。

难点解决

在实际的开发过程中,我们还是遇到了不少的问题。

go 解析 jsonLogic 规则

因为 jsonlogc 官方并没有相应的 go 版本,最开始我打算自己实现,在调研过程中,发现 github 上确实也有几个不错的开源项目,其中 https://github.com/diegoholiveira/jsonlogic 入参和返回值的设计最符合我们的使用场景,能减少很多的开发量。唯一遗憾的是它不支持自定义操作符,这对于我们的需求是必须实现的,好在作者比较活跃,我提了一个 PR implement AddOperator method,他很快的合入了版本。

最终,我在项目中引用了该库的最新版,并增加了字符长度比较(用于填空题)、数组是否存在交集(用于多选题)。

benchmark:

代码语言:txt
复制
# 跑了 1==1 和 reduce 函数的benchmark
goos: darwin
goarch: amd64
pkg: git.code.oa.com/mur-survey/survey-api-v2/internal/pkg/json-logic
cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
BenchmarkSampleRule-12         4209722           289.0 ns/op         896 B/op           4 allocs/op
BenchmarkReduceFunc-12         3978484           292.7 ns/op         896 B/op           4 allocs/op
QueryBuilder 扩展 es 的转换规则和语法,优雅修改 node_modules

react-awesome-query-builder 虽然内置了 es 语法的转换,但是只支持比较常见的一小部分,之前有提到,我们有不少自定义的规则。举个例子:gte_strlen(文本长度大于),像这个操作符就需要新增转换规则。react-awesome-query-builder 为 mongo 提供了 mongoFormatOp 这样的函数,可以针对特殊的操作符配置不同的语法,比如:

代码语言:txt
复制
{
  equal: {
    label: 'equals',
    mongoFormatOp: (field, op, value) => ({ [field]: { '$eq': value } }),
  },
}

但是,没有为 es 提供相应的函数,而且我又不想提 PR(没时间,还得写test,还得等作者合并),所以直接修改 node_modules 的代码是最快的方式。那么,如何优雅的修改呢?

patch-package 可能是最好的方式,patch-package 可以在修改完 node_modules 文件之后,根据当前库的版本生成一份补丁,在其他人 npm install 之后,执行一下 patch-package 就可以应用到修改后的代码,非常方便和安全。

所以,我修改了 react-awesome-query-builder 转换函数中的源码,让其可以支持这样配置:

代码语言:txt
复制
{
    gte_strlen: {
      label: '文本长度大于',
      labelForFormat: 'gte_strlen',
      esFormatOp: (field, op, values) => ({
        script: {
          script: `doc['${field}_text.keyword'].value.length() > ${values[0] ?? 0}`,
        },
      }),
    }
}

例子中,将 gte_strlen 转换成了 es 的 painless 脚本,让其支持查询文本的长度是否大于某值。

vue2 兼容 react 组件

虽然 react-awesome-query-builder 这个库很完善很好用,但是我们的问卷管理端是早期使用 vue2 搭建的,所以重点还需要解决如何在 vue2 中使用 react 组件的问题。其实理论上,build 之后的代码都只是原生的创建 UI 的函数,已经框架无关了,只是像 props/event 这种需要手动处理,vuera 就提供了这样的 react/vue 相互转换的 wrapper,让我们可以混用 Vue 和 React 的组件,不需要额外的脚本配置(webpack/babel等),是一个改造成本比较低的方式,如下:

代码语言:txt
复制
<template>
   <my-react-component :passedProps="passedProps"></my-react-component>
</template>

<script>
  import { ReactWrapper } from 'vuera'
  import MyReactComponent from './MyReactComponent.jsx'

  export default {
    data () {
      message: 'Hello from React!',
    },
    computed: {
      passedProps () {
        return {
          message: this.message,
          reset: this.reset,
        }
      },
    },
    components: { 'my-react-component': MyReactComponent },
  }
</script>
减少包的体积

可以想象,不仅除了组件的代码还有 react 源码,所以整体包的体积 gzip 之后增加了 1.34 mb,虽然是管理端,但作为有追求的前端,我还是希望能将包的体积减少一些。首先,我移除了 react-awesome-query-builder 所有依赖的 UI 库(它适配了 antd/material),其次最核心的是使用只有 3kb 的 preact 替代 react。

代码语言:txt
复制
// vue.config.js
config.resolve.alias.set('react', resolve('node_modules/preact/compat'))
config.resolve.alias.set('react-dom', resolve('node_modules/preact/compat'))

最终体积从 1.34 mb 变成了 0.29mb ,减少了 78%的大小,效果非常明显,就在我准备开开心心提交代码的时候,发现了一个严重的问题,使用 preact 之后,子组件不渲染了。更准确的说,是在 Group.jsx 中的这行代码没有生效:

代码语言:txt
复制
renderChildren() {
    const {children1} = this.props;
    return children1 ? children1.map(this.renderItem.bind(this)).toList() : null;
}

主要的原因是 react 支持 iterator 的遍历,比如 immutable.js 中的 List 类型,但 preact 没有支持,虽然有人提过相关的 PR,Add support for all iterable children, not just Arrays,但由于体积的考虑,最终没有 merged,所以我又修改了对应的源码,使用 Array.from 转换成 js 原生数组:

代码语言:txt
复制
renderChildren() {
    const {children1} = this.props;
    return children1 ? Array.from(children1.map(this.renderItem.bind(this)).toList()) : null;
}

总结

其实,类似的组件有一些设计、文档比较好的,都是需要收费的,比如 Essential JS 2,在开源项目中 react-awesome-query-builder 只能说相对而言是比较不错的,在看源码过程中,只能说中规中矩,当然它最大优点就是功能齐全,帮助我们减少了很多的开发时间。如果让我重新设计,我可能更多会考虑 UI 无关的部分,先从数据结构,树的变换算法开始做一个由纯数据驱动的库,然后再考虑上层 UI ,跟 vue/react 等适配,这也是我们之前重构问卷系统所思考的方式。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • QueryBuilder 是什么?
  • 需求场景
  • 技术调研
  • 难点解决
    • go 解析 jsonLogic 规则
      • QueryBuilder 扩展 es 的转换规则和语法,优雅修改 node_modules
        • vue2 兼容 react 组件
          • 减少包的体积
          • 总结
          相关产品与服务
          腾讯云 BI
          腾讯云 BI(Business Intelligence,BI)提供从数据源接入、数据建模到数据可视化分析全流程的BI能力,帮助经营者快速获取决策数据依据。系统采用敏捷自助式设计,使用者仅需通过简单拖拽即可完成原本复杂的报表开发过程,并支持报表的分享、推送等企业协作场景。
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档