1.10.珷玞:Jooq

“故美玉蕴於碔砆。”——《四子讲德论》

    Zero的支持表格如下:

版本
Zero 0.6.2
Zero 0.7.x

Vert.x

3.9.9

4.1.x

Jooq

3.10.8

3.15.x

vertx-jooq

2.4.1

6.3.x

新版都是支持最新的,所以根据您选择的版本下载不同的工具

「壹」环境准备

1.1. 引入Jooq

    选择Jooq框架的主要目的如下:

  • 和原生SQL的DDL语句结合得比较紧,在做动态建模的时候更容易使用面向对象的方式执行元数据操作(参考zero-atom 项目),包括视图创建、表更改、字段增删等。

  • Jooq近似于一个ORM框架,可以在开发过程中很方便实现面向对象模式的CRUD操作,并且让开发人员不用去关心底层SQL,但它提供了SQL模式的思路来实现数据库访问,比很多ORM框架更加灵活。

Jooq具有代码生成功能,对于最基本的增删查改等操作,开发人员可避免在项目过程中书写Domain/Dao/Service等重复性代码,这些代码可以直接使用jooq-codegen 工具生成。

    Zero中的Jooq设计整体如下图:

    Zero框架中,Jooq是以插件模式引入到系统内部,如果项目不需要访问数据库,该模块的整体功能如下:

  • 和Vert.x协同提供通用的CRUD编程接口(同步和异步双版本),实现面向对象的无缝编程。

  • 提供查询分析引擎,使用Json格式的语法实现复杂的SQL查询,支持大部分常用的聚集功能。

  • 可接入Redis或其他缓存接口,实现AOP层的缓存支持,内置使用Cache-Aside模式。

  • 使用单Class<?>构造UxJooq统一访问接口,在生成的Dao基础之上不需要引入额外的类来完成数据库访问操作。

    为什么要封装Jooq?既然Jooq已经自带了所有核心级别的CRUD操作,那么Zero对它的封装是基于什么目的呢,这也许是很多读者不太容易理解的点,这样的做法是不是有点 重复造轮子 的行为?其实相反,Zero对Jooq的封装是基于实际业务场景的一种补充:

  1. Jooq和Vert.x并没有强相关性,在开发过程中,让开发人员结合Jooq和Vert.x进行编程会有一定的难度,封装过后的API底层是基于Jooq和Vert.x,这样开发人员就不用关心CRUD的技术细节,可实现这层操作的无缝对接。

  2. Zero中提供了强大的查询分析引擎,可构造各种动态SQL以及复杂查询,并且这种查询分析引擎语法使用JSON数据实现,并且提供了特殊的API处理底层数据类型的兼容。

  3. Zero提供了三层缓存,L1的数据库级缓存、L2的业务级缓存、L3的HTTP缓存,在数据库缓存中,开发人员不需要再额外开发缓存逻辑,OOB中提供了Redis的Cache-Aside缓存架构,可处理高并发访问。

1.2. 准备步骤

    这一小节我带着大家一起看看Zero中如何准备Jooq的基本环境,准备步骤完成后,我们再来讲解Jooq中的核心编程接口,参考up-athena 项目。

1.2.1. 创建数据表

    使用如下SQL语句初始化您的数据库,默认数据库名DB_ETERNAL

    写好语句保存到文件,然后执行下边脚本,输入密码、则可初始化一个空库:

    创建数据表有两种方式,在我们生产环境项目中,使用的是liquibase创建数据表,当然您也可以手工创建,在数据库中执行如下代码:

为了兼容Oracle,所有的SQL关键字以及表名字段名全部统一使用大小写敏感的大写,虽然在Zero中这个动作不是必须,不过推荐使用此种方式处理。

1.2.2. 代码生成

    访问文首链接地址下载所有工具,将工具放在script/code目录下(重要 ),修改配置文件config/zero-jooq.xml中的部分核心配置,参考下边的片段部分:

    修改了上述配置后,运行工具脚本,生成对应的领域模型、Dao层代码。脚本运行后,您的项目目录src/main/java 中将会有新代码生成。

1.2.3. 配置

    在您的src/main/resources目录中准备下边三个资源配置文件(Zero中的Jooq相关核心配置):

vertx.yml

vertx-jooq.yml

注意此处生成代码时使用的是固定数据库,从空库到有表的库转换,且为固定名称 DB_ETERNAL

vertx-inject.yml

    最后在Maven中配置MySQL的依赖:

1.3. 连接测试

    接下来使用JUnit执行下边代码,测试一下连接:

    运行测试用例,您将看到如下输出(检查数据库中的数据,您也可以看到新插入的数据信息),如此,Jooq的配置就完成了。     注意 :这里使用的XTabular对象是jooq生成的模型对象而不是表对象,生成对象中有两个同名类:cn.vertxup.demo.domain.tables.pojos.XTabularcn.vertxup.demo.domain.tables.XTabular,我们使用的是第一个带有pojos包名的模型对象,这点经常在环境中容易搞错。

「贰」常用操作

    根据第一章节的引导,您的环境配置就已经彻底完成了,那么本章节我们来看看基本的CRUD操作,通过对本章节的学习,让读者理解Zero中如何实现数据库的基本CRUD,Zero中提供了八大类的常用数据库操作:

  • 添加/批量添加

  • 更新/批量更新

  • 删除/批量删除/按添加删除

  • 保存:添加 + 更新

  • 存在丢失检查

  • 搜索

  • 各种查找编程接口

  • 聚集/分组

    Zero中操作数据库的核心对象为io.vertx.up.uca.jooq.UxJooq,它的实例化方式如下:

    为了提高读者的辨识度,后边所有的示例代码中定义的变量如下:

变量名称
数据类型
含义

t

泛型T

生成的实体类型

list

List<T>

生成的实体列表集合类型

key

泛型ID

主键类型,可以是字符串,可以是整数

criteria

JsonObject

查询条件

query

JsonObject

带分页、排序、列过滤的查询引擎完整条件

jobject

JsonObject

可序列化的Json单条数据

jarray

JsonArray

可序列化的Json多条数据

collection

集合类型

可以是Set也可以是List

array

[]

数组类型,也可以是变参类似 T...

pojo

String

POJO模式中的文件名(支持映射)

field

String

字段名称

aggr

String

聚集字段名称

value

Object

字段传入值

    初次接触这份基础编程接口时,很多读者会觉得数量繁多不太容易记住,但实际上这是在实际项目中使用过后的一份总结,为了方便开发人员,对很多编程接口进行了扩展,主要扩展点如下:

  • 由于很多遗留系统使用的字段名和期望字段名不匹配,如旧系统使用的是:sname,而客户端的新请求在迁移过程中使用的是:name ,这种情况下,很多开发人员不得不修订基础字段,更有甚者会修改数据库中的列名,而Zero框架中不需要这样做,Zero中引入了一层映射层,可以通过pojo 的配置文件将输入的数据直接映射到实体(T) 信息中,这样开发人员就不会为了字段的变更而烦恼,简单说,输入数据的字段、POJO模型的字段直接解耦,防止二者不匹配的情况。

Vert.x中最常用的数据结构是JsonObject/JsonArray,为了让开发人员可以不去思考序列化的细节,所以Zero引入了默认序列化机制,调用这种类型的编程接口,只要定义了Dao类型,开发人员可以直接将数据丢给Jooq来完成数据库的访问,这种场景下甚至不需要开发人员去构造生成的领域模型。

  • 数据库访问是在遗留系统和Vert.x的纯异步系统中演化而来的,所以在提供编程的API时,所有的接口都有同步、异步两个版本,带Async 关键字的就是异步版本,而同步版本在某些场景中依然实用。

    为了辅助开发人员记忆和使用,参考下边的规则来理解每一种操作扩展过后的API含义。

  1. 第一个维度是同步和异步,主要分两种:带Async的是异步版本。

  2. 第二个维度是输入,主要分三种:领域模型、Json数据、带映射层(Pojo)的Json数据。

  3. 第三个维度是返回值,主要分四种T、List<T>、JsonObject、JsonArray

2.1. 基础增删改

2.1.1. 新增

    新增接口的骨架代码:

新增接口只有一点需要说明,如果传入的实体、JsonObject数据本身没有主键值,那么Zero会使用UUID的方式为主键赋值,生成一个新的主键,并且在返回的数据中会带上该主键信息,为了配合前端开发,Zero中推荐所有主键使用属性名key 而不是使用传统常用的id,当然如果开发人员定义了自己的主键,那么Zero会从生成的jooq代码中自动识别。

    将上述代码统计一下,可得到下边的表格,其中领域模型T、Json数据、带Pojo映射层为新增接口的入参搭配。

返回值
领域模型 T
Json数据
带Pojo映射层

insert

T

T

T

JsonObject

T

JsonObject

String

List<T>

List<T>

List<T>

JsonArray

List<T>

JsonArray

String

insertJ

JsonObject

T

JsonObject

JsonObject

JsonObject

JsonObject

String

JsonArray

List<T>

JsonArray

JsonArray

JsonArray

JsonArray

String

insertAsync

Future<T>

T

Future<T>

JsonObject

Future<T>

JsonObject

String

Future<List<T>>

List<T>

Future<List<T>>

JsonArray

Future<List<T>>

JsonArray

String

insertJAsync

Future<JsonObject>

T

Future<JsonObject>

JsonObject

Future<JsonObject>

JsonObject

String

Future<JsonArray>

List<T>

Future<JsonArray>

JsonArray

Future<JsonArray>

JsonArray

String

2.1.2. 搜索

    搜索接口的骨架代码:

先写查询接口的教程,主要原因是后续编程接口都会牵涉带条件查询,在查询过程中让读者对Zero中的查询引擎有所了解,然后再慢慢来深入到查询引擎部分。

    搜索是读取数据中最简单的接口,因为它只包含了两种模式:带POJO映射和不带POJO映射,带POJO映射的模式中,输入和输出的字段名都是调用POJO映射之前的字段名,只有内部的领域模型可能不是该名称,这样整个数据转换过程对开发人员都是透明的。

输入格式

    Zero中的查询引擎详细语法在后边的查询引擎部分详细讲解,此处不再加以说明。

输出格式

    搜索接口的输出在Zero中使用下边这种固定格式:

字段名
数据类型
业务含义

count

整数值

总数据量

list

Json数组

当前页数据(带分页参数) / 完整数据

2.1.3. 读取

    读取接口的骨架代码:

    Zero中的读取接口内容最多,但实际上也可以分类来记忆,它和Insert唯一的不同点是入参的搭配有些差异:

返回值
字段kv/主键
criteria条件
带Pojo映射层

fetchAll

List<T>

fetchJAll

JsonArray

String

fetchAllAsync

Future<List<T>>

fetchJAllAsync

Future<JsonArray>

String

fetch

List<T>

String,Object

List<T>

JsonObject

List<T>

JsonObject

String

fetchJ

JsonArray

String,Object

JsonArray

JsonObject

JsonArray

JsonObject

String

fetchAsync

Future<List<T>>

String,Object

Future<List<T>>

JsonObject

Future<List<T>>

JsonObject

String

fetchJAsync

Future<JsonArray>

String,Object

Future<JsonArray>

JsonObject

Future<JsonArray>

JsonObject

String

fetchIn

List<T>

String, Collection

List<T>

String, Object...

List<T>

String, JsonArray

fetchJIn

JsonArray

String, Collection

JsonArray

String, Object...

JsonArray

String,JsonArray

fetchInAsync

Future<List<T>>

String, Collection

Future<List<T>>

String, Object...

Future<List<T>>

String, JsonArray

fetchJInAsync

Future<JsonArray>

String, Collection

Future<JsonArray>

String, Object...

Future<JsonArray>

String, JsonArray

fetchAnd

List<T>

JsonObject

List<T>

JsonObject

String

fetchJAnd

JsonArray

JsonObject

JsonArray

JsonObject

String

fetchAndAsync

Future<List<T>>

JsonObject

Future<List<T>>

JsonObject

String

fetchJAndAsync

Future<JsonArray>

JsonObject

Future<JsonArray>

JsonObject

String

fetchOr

List<T>

JsonObject

List<T>

JsonObject

String

fetchJOr

JsonArray

JsonObject

JsonArray

JsonObject

String

fetchOrAsync

Future<List<T>>

JsonObject

Future<List<T>>

JsonObject

String

fetchJOrAsync

Future<JsonArray>

JsonObject

Future<JsonArray>

JsonObject

String

fetchById

T

key, Object

fetchByIdAsync

Future<T>

key, Object

fetchJById

JsonObject

key, Object

fetchJByIdAsync

Future<JsonObject>

key, Object

fetchOne

T

key, Object

T

JsonObject

T

JsonObject

String

fetchJOne

JsonObject

key, Object

JsonObject

JsonObject

JsonObject

JsonObject

String

fetchOneAsync

Future<T>

key, Object

Future<T>

JsonObject

Future<T>

JsonObject

String

fetchJOneAsync

Future<JsonObject>

key, Object

Future<JsonObject>

JsonObject

Future<JsonObject>

JsonObject

String

    编程接口的扩展在于是否使用领域模型T字段级查询映射层使用,这里再强调一下此处三个知识点的核心使用场景:

  • 领域模型:在Java语言中,可以用class 来定义一个领域模型,定义过后基于JavaBean的规范,可使用不同的API对该模型中的数据进行访问;但在Vert.x编程中,很多地方都不使用Java中的对象,取而代之的是简化过后的JsonObjectJsonArray ,于是就出现了是否使用领域模型的API分离。

  • 字段级查询:程序访问数据库的过程中,单条件 一直是一个高频场景,不论是单条件单值还是单条件多值,在编程过程中都是业务系统的核心,所以Zero提供了快速的字段查询功能,如fetchInfetch 中的(field, value)方法签名模式,方便开发人员直接查询相关数据。

  • 映射层:映射层是一个附加功能,主要目的是做接口兼容,不论是旧系统和新系统做对接,还是新系统和旧系统做对接,两边的系统都不可能绝对统一,为了保证数据字段的灵活性,对两边的 模型属性名进行解耦,于是Zero提供了映射层 的机制,使得集成开发变得更加简单和流畅。

2.1.4. 更新

    更新接口的骨架代码:

    更新接口的二维表如下:

返回值
领域模型 T
Json数据
带Pojo映射层
更新条件

update

T

T

T

JsonObject

T

JsonObject

String

T

T

key

T

JsonObject

key

T

JsonObject

String

key

T

T

criteria

T

JsonObject

criteria

T

T

String

criteria

T

JsonObject

String

criteria

List<T>

List<T>

List<T>

JsonArray

List<T>

JsonArray

String

updateJ

JsonObject

T

JsonObject

JsonObject

JsonObject

JsonObject

String

JsonObject

T

key

JsonObject

JsonObject

key

JsonObject

JsonObject

String

key

JsonObject

T

criteria

JsonObject

JsonObject

criteria

JsonObject

T

String

criteria

JsonObject

JsonObject

String

criteria

JsonArray

List<T>

JsonArray

JsonArray

JsonArray

JsonArray

String

updateAsync

Future<T>

T

Future<T>

JsonObject

Future<T>

JsonObject

String

Future<T>

T

key

Future<T>

JsonObject

key

Future<T>

JsonObject

String

key

Future<T>

T

criteria

Future<T>

JsonObject

criteria

Future<T>

T

String

criteria

Future<T>

JsonObject

String

criteria

Future<List<T>>

List<T>

Future<List<T>>

JsonArray

Future<List<T>>

JsonArray

String

updateJAsync

Future<JsonObject>

T

Future<JsonObject>

JsonObject

Future<JsonObject>

JsonObject

String

Future<JsonObject>

T

key

Future<JsonObject>

JsonObject

key

Future<JsonObject>

JsonObject

String

key

Future<JsonObject>

T

criteria

Future<JsonObject>

JsonObject

criteria

Future<JsonObject>

T

String

criteria

Future<JsonObject>

JsonObject

String

criteria

Future<JsonArray>

List<T>

Future<JsonArray>

JsonArray

Future<JsonArray>

JsonArray

String

2.1.5. 合并

    合并操作和其他操作有本质的区别,其区别点就在于语义上的不同,合并 操作具有语义:“按某个条件进行合并”,所以合并操作不支持method(T)method(list) 这种方法直接合并。Zero中的Jooq合并基本规则如下:

  1. 编程接口必须提供参数按什么条件合并,目前支持criteria, key和查找函数finder三种,查找函数finder 主要用于批量合并时判断两条记录是否具有同样语义。

  2. 合并内部会调用核心CRUD接口,主要包含了INSERT和UPDATE两种操作,如果可以找到记录则执行更新,反之找不到记录执行插入。

    合并接口的骨架代码:

    合并接口的二维表如下:

返回值
领域模型 T
Json数据
带Pojo映射层
更新条件

upsert

T

T

key

T

JsonObject

key

T

JsonObject

String

key

T

T

criteria

T

T

String

criteria

T

JsonObject

criteria

T

JsonObject

String

criteria

List<T>

List<T>

criteria

List<T>

List<T>

String

criteria

List<T>

JsonArray

criteria

List<T>

JsonArray

String

criteria

upsertAsync

Future<T>

T

key

Future<T>

JsonObject

key

Future<T>

JsonObject

String

key

Future<T>

T

criteria

Future<T>

T

String

criteria

Future<T>

JsonObject

criteria

Future<T>

JsonObject

String

criteria

Future<List<T>>

List<T>

criteria

Future<List<T>>

List<T>

String

criteria

Future<List<T>>

JsonArray

criteria

Future<List<T>>

JsonArray

String

criteria

upsertJ

JsonObject

T

key

JsonObject

JsonObject

key

JsonObject

JsonObject

String

key

JsonObject

T

criteria

JsonObject

T

String

criteria

JsonObject

JsonObject

criteria

JsonObject

JsonObject

String

criteria

JsonArray

List<T>

criteria

JsonArray

List<T>

String

criteria

JsonArray

JsonArray

criteria

JsonArray

JsonArray

String

criteria

upsertJAsync

Future<JsonObject>

T

key

Future<JsonObject>

JsonObject

key

Future<JsonObject>

JsonObject

String

key

Future<JsonObject>

T

criteria

Future<JsonObject>

T

String

criteria

Future<JsonObject>

JsonObject

criteria

Future<JsonObject>

JsonObject

String

criteria

Future<JsonArray>

List<T>

criteria

Future<JsonArray>

List<T>

String

criteria

Future<JsonArray>

JsonArray

criteria

Future<JsonArray>

JsonArray

String

criteria

2.1.6. 删除

    删除接口的骨架代码:

    删除接口的二维表如下:

返回值
领域模型 T
Json数据
带Pojo映射层
删除条件

delete

T

T

T

JsonObject

T

JsonObject

String

List<T>

List<T>

List<T>

JsonArray

List<T>

JsonArray

String

deleteAsync

Future<T>

T

Future<T>

JsonObject

Future<T>

JsonObject

String

Future<List<T>>

List<T>

Future<List<T>>

JsonArray

Future<List<T>>

JsonArray

String

deleteJ

JsonObject

T

JsonObject

JsonObject

JsonObject

JsonObject

String

JsonArray

List<T>

JsonArray

JsonArray

JsonArray

JsonArray

String

deleteJAsync

Future<JsonObject>

T

Future<JsonObject>

JsonObject

Future<JsonObject>

JsonObject

String

Future<JsonArray>

List<T>

Future<JsonArray>

JsonArray

Future<JsonArray>

JsonArray

String

deleteById

Boolean

key

Boolean

keys

deleteByIdAsync

Future<Boolean>

key

Future<Boolean>

keys

deleteBy

Boolean

JsonObject

criteria

Boolean

JsonObject

String

criteria

deleteByIdAsync

Future<Boolean>

JsonObject

criteria

Future<Boolean>

JsonObject

String

criteria

2.1.7. 检查

    检查接口的骨架代码:

  • 存在检查:存在 = true,不存在 = false。

  • 丢失检查:存在 = false,不存在 = true。

    检查接口的二维表如下:

返回值
Json数据
带Pojo映射层
检查条件

existById

Boolean

key

existByIdAsync

Future<Boolean>

key

missById

Boolean

key

missByIdAsync

Future<Boolean>

key

exist

Boolean

JsonObject

criteria

Boolean

JsonObject

String

criteria

miss

Boolean

JsonObject

criteria

Boolean

JsonObject

String

criteria

existAsync

Future<Boolean>

JsonObject

criteria

Future<Boolean>

JsonObject

String

criteria

missAsync

Future<Boolean>

JsonObject

criteria

Future<Boolean>

JsonObject

String

criteria

    检查接口会经常用于一些表单提交过程中的验证环节,比如检查用户名是否存在、检查邮箱是否存在、检查当前手机是否是一个新手机等等。

2.1.8. 分组

    分组接口的骨架代码:

分组不支持POJO的直接模式,只能通过on方法绑定。

    分组接口的二维表如下:

返回值
输入数据
分组条件

group

ConcurrentMap<String,List<T>>

String

field

ConcurrentMap<String,List<T>>

JsonObject,String

field

groupAsync

Future<ConcurrentMap<String,List<T>>>

String

field

Future<ConcurrentMap<String,List<T>>>

JsonObject,String

field

groupJ

ConcurrentMap<String,JsonArray>

String

field

ConcurrentMap<String,JsonArray>

JsonObject,String

field

groupJAsync

Future<ConcurrentMap<String,JsonArray>>

String

field

Future<ConcurrentMap<String,JsonArray>>

JsonObject,String

field

2.2. 聚集函数

2.2.1. 计数

    计数接口的骨架代码:

    计数接口的二维表如下:

此处Map数据结构为ConcurrentMap<String,Long>

返回值
输入数据
带Pojo映射层
分组条件

countAll

Long

countAllAsync

Future<Long>

count

Long

JsonObject

Long

JsonObject

String

countAsync

Future<Long>

JsonObject

Future<Long>

JsonObject

String

countBy

Map

String

field

Map

JsonObject,String

field

JsonArray

String[]

field[]

JsonArray

JsonObject,String[]

field[]

countByAsync

Future<Map>

String

field

Future<Map>

JsonObject,String

field

Future<JsonArray>

String[]

field[]

Future<JsonArray>

JsonObject,String[]

field[]

响应格式

    基本的计数操作在底层都是执行类似COUNT的SQL语法,对大部分场景而言它已经足够使用了,而上述接口中如果是 单字段分组 计数,那么响应结果就是ConcurrentMap<String,Long>格式,其中键的值就是当前组名。在Zero的Jooq编程接口中,多字段分组 格式采用了JsonArray 的数据类型,其中每一组包含了分组字段名和固定字段count的值。例如下边语句:

假设字段对应数据如:

列名
字段名
含义

USERNAME

username

用户名

EMAIL

email

邮件

    那么最终返回的多组格式如:

    这种模式下,分组时无法计算具有业务意义的键值,所以使用了数组格式来存储每一组的数据,如上述例子中虽然两组的用户名都是Lang ,但由于email地址不同,所以会生成两条用户名相同的记录。

    不仅如此,下边所有的聚集函数格式和计数都类似,只是分组过后的字段名称有些差异,简单总结一下聚集函数部分的编程接口:

此处op表示聚集函数专用名称,如sum、max、min、avg、count等。

  1. 所有的聚集函数都分为两种编程接口:opopBy,前者用于统计,返回结果为单结果集,后者用于分组统计,返回结果为上述分组接口。

  2. 只有op类型的接口支持POJO模式,查询条件中可以直接引用映射层,而opBy接口不支持POJO模式访问。

2.2.2. 求和

    求和接口的骨架代码:

    求和接口的二维表如下:

此处Map数据结构为ConcurrentMap<String,BigDecimal>aggr则是被聚集字段。

返回值
输入数据
Pojo映射层
分组条件

sum

BigDecimal

aggr

field

BigDecimal

aggr,JsonObject

field

BigDecimal

aggr,JsonObject

String

field

sumAsync

Future<BigDecimal>

aggr

field

Future<BigDecimal>

aggr,JsonObject

field

Future<BigDecimal>

aggr,JsonObject

String

field

sumBy

Map

aggr,String

field

Map

aggr,JsonObject,String

field

JsonArray

aggr,String[]

field[]

JsonArray

aggr,JsonObject,String[]

field[]

sumByAsync

Future<Map>

aggr,String

field

Future<Map>

aggr,JsonObject,String

field

Future<JsonArray>

aggr,String[]

field[]

Future<JsonArray>

aggr,JsonObject,String[]

field[]

2.2.3. 求平均

    求平均接口的骨架代码:

    求平均接口的二维表如下:

此处Map数据结构为ConcurrentMap<String,BigDecimal>aggr则是被聚集字段。

返回值
输入数据
Pojo映射层
分组条件

avg

BigDecimal

aggr

field

BigDecimal

aggr,JsonObject

field

BigDecimal

aggr,JsonObject

String

field

avgAsync

Future<BigDecimal>

aggr

field

Future<BigDecimal>

aggr,JsonObject

field

Future<BigDecimal>

aggr,JsonObject

String

field

avgBy

Map

aggr,String

field

Map

aggr,JsonObject,String

field

JsonArray

aggr,String[]

field[]

JsonArray

aggr,JsonObject,String[]

field[]

avgByAsync

Future<Map>

aggr,String

field

Future<Map>

aggr,JsonObject,String

field

Future<JsonArray>

aggr,String[]

field[]

Future<JsonArray>

aggr,JsonObject,String[]

field[]

2.2.4. 最大值

    最大值接口的骨架代码:

    最大值接口的二维表如下:

此处Map数据结构为ConcurrentMap<String,BigDecimal>aggr则是被聚集字段。

返回值
输入数据
Pojo映射层
分组条件

max

BigDecimal

aggr

field

BigDecimal

aggr,JsonObject

field

BigDecimal

aggr,JsonObject

String

field

maxAsync

Future<BigDecimal>

aggr

field

Future<BigDecimal>

aggr,JsonObject

field

Future<BigDecimal>

aggr,JsonObject

String

field

maxBy

Map

aggr,String

field

Map

aggr,JsonObject,String

field

JsonArray

aggr,String[]

field[]

JsonArray

aggr,JsonObject,String[]

field[]

maxByAsync

Future<Map>

aggr,String

field

Future<Map>

aggr,JsonObject,String

field

Future<JsonArray>

aggr,String[]

field[]

Future<JsonArray>

aggr,JsonObject,String[]

field[]

2.2.5. 最小值

    最小值接口的骨架代码:

    最小值接口的二维表如下:

此处Map数据结构为ConcurrentMap<String,BigDecimal>aggr则是被聚集字段。

返回值
输入数据
Pojo映射层
分组条件

min

BigDecimal

aggr

field

BigDecimal

aggr,JsonObject

field

BigDecimal

aggr,JsonObject

String

field

minAsync

Future<BigDecimal>

aggr

field

Future<BigDecimal>

aggr,JsonObject

field

Future<BigDecimal>

aggr,JsonObject

String

field

minBy

Map

aggr,String

field

Map

aggr,JsonObject,String

field

JsonArray

aggr,String[]

field[]

JsonArray

aggr,JsonObject,String[]

field[]

minByAsync

Future<Map>

aggr,String

field

Future<Map>

aggr,JsonObject,String

field

Future<JsonArray>

aggr,String[]

field[]

Future<JsonArray>

aggr,JsonObject,String[]

field[]

    Zero中整个数据库部分的基本操作接口到此就告一段落,其实从整个编程接口中可以看到对开发人员还是比较友好的,虽然接口繁多,但整体命名规范方便记忆(大部分接口使用了函数重载模式),使得本身使用起来比较简单——再借用IDE的代码智能感知,使用过程就变得十分流畅了。

「叁」查询引擎

    Zero中的查询引擎语法使用Json数据格式,它主要分为两种,前文中提到的criteriaquery ,一种是查询格式,一种是全格式,其中query 包含了criteria格式。

    criteria的格式在Zero的Jooq中是本章要讲解的查询引擎基本语法,而query(包含了排序、列过滤、分页功能)的完整格式如下:

    四个属性的含义如下:

属性名
类型
备注

criteria

JsonObject

查询条件

pager

JsonObject

分页参数

sorter

JsonArray

排序参数

projection

JsonArray

列过滤参数

3.1. 基本语法

    查询语法本身是一个Json树的格式,支持嵌套,整个查询树的每一个节点使用column=value的方式,节点主要包含三种类型。

节点
格式

直接节点

column = value

嵌套节点

column = {}

连接节点

"" = true/false

    上述查询条件中,根据column的格式不同,可演变成不同条件,每一个column=value的语法格式如下:

    其中:

  1. field是属性名。

  2. op是操作符。

  3. value是当前查询条件的属性值。

3.1.1. op操作列表

    接下来以示例字段为例看看不同的op构成的最终查询条件,假设系统中有如下模型:

属性名
数据类型
列名

name

字符串

NAME

email

字符串

EMAIL

password

字符串

PASSWORD

age

数值

AGE

active

逻辑值

ACTIVE

    目前Zero中支持的所有op列表如下:

操作符
格式
含义
SQL等价

<

"age,<":20

小于某个值

AGE < 20

<=

"age,<=":20

小于等于某个值

AGE < 20

>

"age,>":16

大于某个值

AGE >= 16

>=

"age,>=":16

大于等于某个值

AGE >= 16

=

"age,=":12 或 "age":12

等于某个值

AGE = 12

<>

"name,<>":"LANG"

不等于

NAME <> 'LANG'

!n

"name,!n":"随意"

不为空

NAME IS NOT NULL

n

"name,n":"随意"

为空

NAME IS NULL

t

"active,t":"随意"

等于TRUE

NAME = TRUE

f

"active,f":"随意"

等于FALSE

NAME = FALSE

i

"name,i":["A","B"]

在某些值内

NAME IN ('A','B')

!i

"name,!i":["C","D"]

不在某些值内

NAME NOT IN('A','B')

s

"name,s":"Lang"

以某个值开始

NAME LIKE 'Lang%'

e

"name,e":"Lang"

以某个值结束

NAME LIKE '%Lang'

c

"name,c":"Lang"

模糊匹配

NAME LIKE '%Lang%'

3.1.2. 连接节点

    上述op直接节点专用的column中的操作符语法(等价于SQL中的ANDOR),嵌套节点 则直接在criteria基础上递归,本小节讲解一下** 连接节点的语法。连接节点使用了编程中的一个禁忌空键**,使用它代替连接节点的目的如:

  1. 方便开发人员记忆,只要学会了Zero中的查询引擎语法,就可以直接将Json转换成核心查询树。

  2. 空键不具有任何业务意义,在真实业务场景中不属于任何业务领域,这样就和其他占位符不构成任何冲突。

    Zero中的空键只有两个值:true/false,它们的含义如下:

连接符

""

true

AND(或者不写)

""

false

OR(或者不写)

3.2. 示例

  1. 查询引擎中的默认连接符是AND

  2. 如果值格式是JsonArray则语法字段转换为IN/NOT IN

  3. 如果op不书写,则默认为语法=

  4. $?主要用于特殊条件下的占位符,补齐column=value中的两端专用,推荐使用$前缀,使得系统不会因为属性名存在而冲突。

3.2.1. 基本语句

    等价的SQL语句如:

3.2.2. 嵌套格式

    等价的SQL语句如:

3.2.3. 同字段查询

    由于Json数据格式中键的限制,如果是同一个条件出现多次时,只能使用嵌套模式代替平行模式,如:

    等价的SQL语句如:

3.2.4. 其他操作符

    等价的SQL语句如:

「肆」L1缓存

    除了前文提到的Zero中的查询引擎以外,新版的Zero(0.6.0 )引入了查询缓存,系统采用Cache-Aside模式提供了数据库级别的缓存,为了使得缓存结构本身不具有代码侵入性,整个缓存的调用会在您启用了AspectJ的时候执行。

    其实以前有人吐槽过Zero框架本身越来越重 的问题,关于这一点我只能说我会跟着项目实际需要和产品研发的路线走,它并不是一个单纯得什么都没有的框架,很多需求只是在开发过程中逐渐形成的,而它本身的插件模式使得您可以在使用过程中选择自己所需的组件,针对不同的项目可选择不同的Zero插件来完成。

4.1. 整体架构

    Zero中的完整L1缓存架构如下:

    Zero中的L1缓存设计主要基于目前Ox平台的CMDB高并发访问场景,它要解决如下几个核心问题:

  1. 在多种不同查询交叉查询相同数据时,提高缓存中不同查询条件的命中率和缓存本身的使用效率。

  2. 保证启用了缓存过后,数据本身的实时性,由监控平台实时查询Zero系统的数据。

  3. 在写数据的过程中完善处理缓存中的变更(双删策略),减轻查询压力、提高查询的准确性,保证缓存数据一致性。

4.2. 环境配置

    Zero中的缓存使用了Redis + MySQL的架构,所以在启用缓存时需要在项目中配置Redis的相关信息,Zero中的Redis配置和Jooq配置比较类似。

vertx.yml

vertx-redis.yml

vertx-inject.yml

vertx-cache.yml

    lime 使用最多的语义是“石灰”,具有侵入、腐蚀、热之隐喻,最早考虑使用这个词语也很随意,它负责将所有其他的文件全部组合到一起,如上述配置中因为配置了redis, cache 两个节点,就意味着你需要另外两个文件vertx-redis.yml、vertx-cache.yml来配置其扩展部分,此处我们其实配置了两个扩展。

4.3. 缓存中的问题

4.3.1. 穿透

    缓存穿透指数据库和缓存中都没有数据,当用户不断发起请求,如发起id = x 的请求时,数据库中根本不存在id = x 的请求,如果用户本人就是攻击者,那么这种请求会无限放大,这种情况称之为穿透。这种情况可以用如下方法:

  1. 接口层增加校验,如鉴权校验、参数校验、基于拦截。

  2. 从缓存取不到数据,而且数据库中也没取到,可以将缓存写成key=null

  3. 减掉缓存的有效时间,如30秒(太长也会使得缓存无法使用),这样防止用户使用同一个id暴力攻击。

4.3.2. 击穿

    缓存击穿是指缓存中没有数据,但数据库中有数据(一般是缓存时间到期),此时由于并发用户特别多,同时读取缓存时并没读到数据,而此时又同时去数据库中读取数据,造成数据库压力瞬间增大,造成了过大压力。这种情况可以用如下方法:

  1. 设置热点数据(经常访问的)永远不过期。

  2. 加互斥锁代码逻辑。

4.3.3. 雪崩

    缓存雪崩是指缓存中数据大批量过期,而查询数据量巨大,引起数据库压力过大甚至停止。和击穿不同的是,缓存击穿是并发访问同一条数据,而雪崩是不同数据都过期导致大面积无法查询到。这种情况可以用如下方法:

  1. 缓存数据的过期时间随机设置,防止同一时间大量数据过期现象发生。

  2. 如果缓存数据是分布式部署,将热点数据均匀分布在不同的缓存数据库中。

  3. 设置热点数据永不过期。

「伍」小结

    到此,Zero中的Jooq部分教程就到此为止了,本文没有讲解和Jooq相关的太多实例内容,这部分内容可以参考up-athena 项目中的测试用例,在文中就不重复了。

Last updated