优势劣势
首先要承认银弹是没有的,所以先说 劣势
- 在增、删、改、查4个过程中,增的环境劣势比较明显
-
操作查询结果时,不能像Entity一样有IDE的提示和自动补齐,减少了IDE的协助确实让许多人寸步难行,
大部分人也是在这里被劝退的。 - 在插入数据时,不能像像Entity一样:userService.save(user),而是需要指定表名:service.save(HR_USER, row);
以上问题如何平衡的
- AnyLine返回的结果集与Entity之间随时可以相互转换,也可以在查询时直接返回Entity
有思想的程序员会想为何要造个轮子 可靠吗,所以再说 疑问
-
AnylineLine并非新造了一个轮子,只是简单的把业务参数传给了底层的spring-jdbc
接下来的操作(如事务控制、连接池等)完全交给了spring-jdbc(没有能力作好的事我们不作) -
如果非要说是一个新轮子,那只能说原来的轮子太难用,太消耗程序员体力了。
正事还没开始就先生成一堆的mapper,OOO,各种铺垫
铺垫完了要操作数据实现业务了,依然啰嗦,各种 劳力 不劳心 的遍历及加减乘除
不但操作数据库慢,查询出来的结果集更是一堆废柴,除了能遍历能get/set啥也不是
所以重点说 优势
1. 关于查询条件
这是开发人员最繁重的体力劳动之一
接收参数、验证、格式化、层层封装传递到mapper.xml,再各种判断、遍历就为生成一条SQL
下面的这些标签许多人可能感觉习以为常了,以下是反例 反例 反例
<if test="code != null and code != '' "> AND CODE = #{code} </if> <if test="name != null and name != '' "> AND NAME like concat('%',#{name},'%') </if> <if test="types != null and types.size > 0 "> AND TYPE IN <foreach collection="types" item="type" open="(" close=")" separator=","> #{type} </foreach> </if>
但这并不正常,这期间还有什么是必须程序员参与的,程序员不参与就自动不了,就约定不了的吗?
换一种方式处理:
不要mapper.xml了,也更不要定位SQL的ID的
直接在java中这样处理
condition("CODE:code","NAME:name%", "TYPE:[type]")
其他的交给机器处理,生成SQL
WHERE CODE = ? AND NAME LIKE '?%' AND TYPE IN(?,?...)
更多的约定可以参考这里的【约定规则】
2. 结果集的二次操作
这是开发人员最繁重的劳动之二
从数据库中查询出数据后,根据业务需求还需要对结果集作各种操作,最简单的如加减乘除、交集差集、筛选过滤等
这些常见的操作DataSet中都已经提供默认实现了,如ngl表达式、聚合函数、类SQL筛选过滤、维度转换等。
3. 关于面向动态与运行时环境
这里说的动态是指出动态数据源、动态数据结构、动态结果集
运行时环境是指在系统运行阶段才能确定以上内容,而不是在需求、设计、编码阶段
动态数据源:
一般是在系统运行时生成
典型场景如数据中台,用户通过管理端提交第三方数据库的地址帐号,中台汇聚多个数据源的数据
这种情况下显示不是在配置文件中添加多个数据源可以解决的
而是需要在接收到用户提交数据后,生成动态的数据源
生成的动态数据源最好交给Spring等容器管理
以充分利用其生态内的连接池,事务管理,切面等现有工具
在切换数据源时也不能通过切面来实现
而是根据组织或租户身份等上下文环境来切换
动态数据结构:
一般由非专业开发人员甚至是最终用户来设计表结构
根据用户设置或不同场景返回不同结构的结果集
查询条件也由用户动态指定
结果集与查询条件的选择范围也不能在编码阶段设置限定
典型场景如物联网平台仪器设备参数、低代码平台、报表工具
常用的数据结构有两种
1).DataRow类似于一个Map
2).DataSet是DataRow的集合,并内含了分页信息
以下场景中将逐步体现出相对于List,Entity的优势
1). 最常见的如更新或查询部分列
DataRow row = service.query("HR_USER(ID,CODE)")
service.update(row,"CODE")
2).可视化数据源、报表输出、数据清洗
这些场景下都需要的数据结构都是灵活多变的
经常是针对不同的业务从多个表中合成不同的结构集
甚至是运行时根据用户输入动态结合的结构集
输出结果集后又需要大量的对比及聚合操作
这种情况下显示不可能为每个结果集生成一个对应Entity,只能是动态的Map结构
在对结构集的二次操作上,DataRow/DataSet可以在抽象设计阶段就完成,而Entity却很难
3).低代码后台、元数据管理
作为一个低代码的后台,首先需要具体灵活可定制的表结构(通常会是一个半静半动的结构)
我们将不再操作具体的业务对象与属性。对大部分业务的操作都只能通过抽象的元数据进行。
举例来说一个简单的求和过程,原来在对静态结构时常用的的遍历、Lamda、反射都难堪重任了。
我们能接收到的信息通常是这样的:类型(学生)、属性(年龄)、条件(年级=1)、聚合公式(平均值)
Anyline的实现过程类似这样
DataSet set = service.querys(学生,年级=1);
int 平均年龄 = set.agg(平均值,年龄);
4).运行时自定义表单、查询条件
许多情况下我们的基础版本产品,很难满足用户100%的需求,
而这些新需求又大部分是一些简单的表单、查询条件
如果是让程序员去开发一个表单,添加几个查询条件,那确实很简单
但用户不是程序员,我们也不可能为每个用户提供全面全天候的技术支持
考虑到成本与用户体验的问题通常会给用户提供一个自定义表单与查询条件的功能
自定义并不难,难的是对自定义表单的存储、查询、关联,以及对自定义查询条件的支持
与上一条说的元数据管理一样,我们在代码实现环节还是不知道会有什么对象什么属性
当然也更不会有对应的service, dao, mapper, VO/DTO/BO/DO/PO/POJO
Anyline的动态查询类似这样实现
service.query(类型(属性集合),condition().add('对比方式','属性','值');
5).物联网环境(特别是像Cassandra、ClickHouse等列式数据库 InfluxDB、timescale等时序数据库)
与低代码平台类似都需要一种动态的结构,并且为了数据读取的高效,数据在水平方向上变的更分散。
这与最终用户需要显示的格式完全不一样,直接通过数据库查询出来的原始数据通常是类似这样
时间戳 | KEY | VALUE |
---|---|---|
1657330073131 | LAT | 39.917055 |
1657330073131 | LNG | 116.392191 |
1657330073132 | LAT | 39.917055 |
1657330073132 | LNG | 116.392191 |
1657330073133 | LAT | 39.917055 |
1657330073134 | LNG | 116.392191 |
而最终展示的界面可能是这样:
时间戳 | LNG | LAT |
---|---|---|
1657330073131 | 116.392191 | 39.917055 |
1657330073131 | 116.392191 | 39.917055 |
日期 | 时间点1 | 时间点2 | 时间点...N | |||
---|---|---|---|---|---|---|
LNG | LAT | LNG | LAT | LNG | LAT | |
01-01 | 116.392191 | 39.917055 | 116.392191 | 39.917055 | 116.392191 | 39.917055 |
01-02 | 116.392191 | 39.917055 | 116.392191 | 39.917055 | 116.392191 | 39.917055 |
当然实战中会比这更复杂,历经实战的程序员一定体验过什么是千变万化、什么是刁钻苛刻
数据库中将不再有一一对应的hello表格,java中也没有对应的Entity
可以想像的出来基于一个静态结构或者原始的Map,List结构需要程序员负责多少体力
要在这个基础上实现让用户自定义报表,那可能比把用户培养成一个程序员还要困难
而一个有思想的程序员应该会把以上问题抽象成简单的行列转换的问题
并在项目之前甚至没有项目的时候就已经解决之。
各种维度的转换可以参考DataSet.pivot()的几个重载 或示例代码 anyline-simple-result
6).关于分页查询的数据存储结构
通过默认的方式查询
- 无论是否分页 都可以通过DataSet结构接收数据
- 不同的是分页后DataSet.PageNavi中会嵌入详细的分页信息
通过User.class查询数据时
- 如果没有分页 可以通过List<User>>结构接收数据
- 如果有分页了 那需要通过Page<List<User>>结构接收数据
- 简单查询个部门列表,还要根据分不分页写两个接口吗
7).数据加密
对于需要加密的数据经常会遇到数字类型的ID
而加密后的数据类型通常是String类型,导致原对象无法存储
-
http参数到jdbc参数的转换
在实际开发中、业务开发人员经常需要大量的时间,不厌其烦的从http/rpc中提取参数,判断验证,生成jdbc要求格式的参数,再把参数依次传到service、dao,最后返回一个实现bean。这整个过程中经常有各种小细节容易忽略而导致异常,如空值处理,IN条件生成等。
而在整个项目中这些过程又是大量重复或类似的。这不但影响开发速度与代码质量,更影响心情。
所以AnyLine提供了一个统一约定格式来实现这些繁琐的操作,格式大致如下service.querys("表", condition(String ... 条件)),中condition方法的参数格式约定,如:service.querys("CRM_USER", condition("TYPE_ID:type", "NAME:%name%"))
这是AnylineController中提供的方法,功能比较单一,低代码场景可以通过【ConfigStore】实现复杂条件的合成
参数值⇢
约定格式⇣1 2 3 4 5 6 7 8 code= '0' code= code= 0&code=1&cd=2&user=5 code= 0,1&cd=2&user=5
cd=2&cd=3 code= '0'(密文) cd=2(密文)&cd=3(密文) code=1,2 1 CODE:code CODE = '0' 忽略 CODE = '0'
CODE = '0' 忽略 忽略 忽略 x 2 CODE:code::int CODE = 0 忽略 CODE = 0 CODE = 0 忽略
忽略
忽略
x 3 CODE:%code% CODE LIKE '%'0'%' 忽略 CODE LIKE '%'0'%'
CODE LIKE '%'0'%'
忽略 忽略 忽略 x 4 CODE:%code CODE LIKE '%'0' 忽略 CODE LIKE '%'0'
CODE LIKE '%'0' 忽略 忽略 忽略 x 5 CODE:code% CODE LIKE '0'%' 忽略 CODE LIKE '0'%'
CODE LIKE '0'%' 忽略 忽略 忽略 x 6 CODE:%code:cd% CODE LIKE '%'0'%' 忽略 CODE LIKE '%'0'%'
CODE LIKE '%'0'%' CODE LIKE '%2%' 忽略 忽略 x 7 CODE:%code:cd:${9}% CODE LIKE '%'0'%' CODE LIKE '%9%' CODE LIKE '%'0'%'
CODE LIKE '%'0'%' CODE LIKE '%2%' 忽略 忽略 x 8 CODE:%code:cd CODE LIKE '%'0' 忽略 CODE LIKE '%'0'
CODE LIKE '%'0' CODE LIKE '%2' 忽略 忽略 x 9 CODE:%code:cd:${9} CODE LIKE '%'0' CODE LIKE '%9' CODE LIKE '%'0'
CODE LIKE '%'0' CODE LIKE '%2' 忽略 忽略 x 10 CODE:[code] CODE = '0' 忽略 CODE IN('0','1')
CODE IN('0','1') 忽略 忽略 忽略 x 11 CODE:[code]::int CODE = 0 忽略
CODE IN(0,1) CODE IN(0,1) 忽略
忽略
忽略
x 12 CODE:[split(code)] CODE = '1' 忽略
CODE IN('0',’1')
CODE IN('0',’1')
忽略
忽略
忽略
CODE IN('1','2') 13 CODE:[org.ClassA.split(code)] CODE = '1' 忽略
CODE IN('0','1')
CODE IN('0',’1')
忽略
忽略
忽略
CODE IN('1','2') 14 CODE:[code:cd] CODE = '0' 忽略 CODE IN('0','1')
CODE IN('0',’1') CODE IN('2',’3') 忽略 忽略 x 15 CODE:[cd+] 忽略
忽略
CODE = '2'
CODE = '2' CODE IN('2','3') 忽略
CODE IN('2','3') x 16 CODE:[code:cd:${[6,7,8]}] CODE = '0' CODE IN('6','7','8') CODE IN('0','1')
CODE IN('0','1') CODE IN('2','3') 忽略 忽略 x 17 CODE:[code:cd:${6,7,8}]
CODE = '0' CODE IN('6','7','8') CODE IN('0','1')
CODE IN('0','1') CODE IN('2','3') 忽略 忽略 x 18 CODE:![code] CODE NOT IN('0') 忽略
CODE NOT IN('0','1')
CODE NOT IN('0','1')
忽略
忽略
忽略
x 19 +CODE:code CODE = '0' CODE IS NULL CODE = '0'
CODE = '0' CODE IS NULL 忽略 忽略 x 20 ++CODE:code CODE = '0' 不执行 CODE = '0'
CODE = '0' 不执行 忽略 忽略 x 21 CODE:>code CODE > '0' 忽略 CODE > '0'
CODE > '0' 忽略 忽略 忽略 x 22' CODE:>code:cd CODE > '0' 忽略 CODE > '0'
CODE > '0' CODE > 2 忽略 忽略 x 23 CODE:>code:${9} CODE > '0' CODE > 9 CODE > '0'
CODE >'0' CODE > 9 CODE > 9 CODE > 9 x 24 CODE:>=code CODE >= '0' CODE >= '9' CODE >= '0' CODE >= '0'
CODE >= '9'
CODE >= '9'
CODE >= '9'
x 25 CODE:!=code
CODE != '0' CODE != '9' CODE >= '0' CODE >= '0'
CODE != '9'
CODE != '9'
CODE != '9'
x 26 CODE:code:cd CODE = '0' 忽略 CODE = '2'
CODE = '2' CODE = '2' 忽略 忽略 x 27 CODE:code:cd:${9} CODE = '0' CODE = '9' CODE = '0'
CODE = '0' CODE = '2' 忽略 忽略 x 28 CODE:code|cd CODE = '0' 忽略 CODE = '0' OR CODE = '2'
CODE = '0' OR CODE = '2' 忽略 忽略 忽略 x 29 CODE:code|{NULL} CODE = '0' OR CODE IS NULL 忽略 CODE = '0' OR CODE IS NULL
CODE = '0' OR CODE IS NULL 忽略
忽略
忽略
x 30 CODE:code|CODE:cd CODE = '0' 忽略 CODE = '0' OR CODE = '1'
CODE = '0' OR CODE = '1' CODE = '2' 忽略 忽略 x 31 CODE:code|CD:cd CODE = '0' 忽略 CODE = '0' OR CD = '2'
CODE = '0' OR CD = '2' CD = '2' 忽略 忽略 x 32' CODE:code:cd|user
CODE = '0' 忽略 CODE = '0' OR CODE = '5'
CODE = '0' OR CODE = '5' CODE = '2' 忽略 忽略 x 33 CODE:code:cd|${9}
CODE = '0'
忽略 CODE = '0' OR CODE = '9'
CODE = '0' OR CODE = '9' CODE = '2' OR CODE = '9' CODE = '9' CODE = '9' x 34 CODE:code+:${9} CODE = '9' CODE = '9' CODE = '9'
CODE = '9' CODE = '9' CODE = '0' CODE = '9' x 35 CODE:code+:cd:${9} CODE = '9' CODE = '9' CODE = '2'
CODE = '2' CODE = '2' CODE = '0' CODE = '9' x 36 CODE:code+:cd+ 忽略
忽略
忽略
忽略
忽略
CODE = '0' CODE = '2' x 37 CODE:code|CODE:cd|CD:cd|CD:code CODE = '0' OR CD = '0' 忽略 CODE = '0' OR CODE = '2' OR ID = '0' OR ID = '2'
CODE = '0' OR CODE = '2' OR ID = '0' OR ID = '2' CODE =2 OR CD =2 忽略 忽略 x 38 CODE:code:${9}|CD:cd:${9} CODE = '0' OR CD = '9' CODE = '9' OR CD = '9' CODE = '0' OR CD = '2'
CODE = '0' OR CD = '2' CODE = '9' OR CD = '2' CODE = '9' OR CD = '9' CODE = '9' OR CD = '9' x 39 [CODES]:code FIND_IN_SET('0',CODES) 忽略
FIND_IN_SET('0',CODES) OR FIND_IN_SET('1',CODES)
FIND_IN_SET('0',CODES) OR FIND_IN_SET('1',CODES)
忽略
忽略
忽略
x 40 [CODES]:[code] FIND_IN_SET('0',CODES) 忽略
FIND_IN_SET('0',CODES) OR FIND_IN_SET('1',CODES)
FIND_IN_SET('0',CODES) OR FIND_IN_SET('1',CODES)
忽略
忽略
忽略
FIND_IN_SET('1',CODES)
OR FIND_IN_SET('2',CODES)
41 [CODES]:split(code) FIND_IN_SET('0',CODES) 忽略
FIND_IN_SET('0',CODES) OR FIND_IN_SET('1',CODES)
FIND_IN_SET('0',CODES) OR FIND_IN_SET('1',CODES)
忽略
忽略
忽略
FIND_IN_SET('1',CODES)
OR FIND_IN_SET('2',CODES)
在提交多个值时FIND_IN_SET会涉及到AND,OR两种情况,默认按OR实现,如果需要实现AND请参考ConfigStore.and()
在低代码平台或运行时用户自定义查询条件场景中可以用到以下格式: 列名不是由Java中指定而是通过解析参数确定,也就是说:前后都是http 的参数key,如${column}:code
结尾的::int 表示参数值的数据类型 类型以StandardTypeMetadata枚举值为准,不区分大小写,有些数据库可以自动隐式转换的不需要指定,
查表时通常可以用ConfigTable.IS_AUTO_CHECK_METADATA=tue,来实现数据类型定位会更简单
service.query("USER", "ID:1::int")以上SQL在实际运行中以占位符?生成,类似CODE > '0'的条件实际是CODE > ?,java中通过 preapreStatement赋值,最终执行结果与数据类型有关
忽略:表示合成SQL时不拼接当前查询条件
不执行:表示整个SQL不执行,querys返回长度为0的DataSet,query返回null
[]表示数组
[CODES]:cd表示数据库中CODES是数组形式如1,2,3 查询时需要FIND_IN_SET函数 FIND_IN_SET('1', CODES)
CODE:[cd]表示request参数中cd是数组形式查询需要IN函数 CODE IN(1,2,3)"+"开头表示必须条件,如果没有值传则生成CODE IS NULL的条件(仅"="时有效,其他IN,>时,当前条件忽略)
“++”开头时,如果没有传值则整个SQL不执行,返回长度为零的DataSet
"x"表示不符合预期,就不能这么用,不是为这种场景设计的
-
常规数据库操作
AnyLine提供了一对基础的service/dao。通过接收上一步约定好格式的参数,只需要在项目中注入AnylineService即可完成绝大部分的数据库操作。(AnylineService已通过@Service("anyline.service")的形式注册到Spring上下文中)
在实际开发过程中,通常是用项目的BaseController继承tAnylineController(适用于springmvc框架,如果是Struts2框架则继承AnylineAction)
AnylineController中已经注入AnylineService serive,并重载了大量condition函数。
Controller层操作过程大致如下:DataSet set = service.querys("HR_USER(ID,NM)" , condition(true,"TYPE_CODE:[type]","NM:nm%","+ZIP_CODE:zip:","++DEPT_CODE:dept","POST_CODE:post:pt:{101}") , "IS_PUB:1");
实现功能:
根据TYPE_CODE,NM,IS_PUB列查询HR_USER表,并根据http参数自动分页
生成的SQL(以mysql为例):SELECT ID,NM FROM HR_USER WHERE TYPE_CODE IN (?,?,?) AND NM LIKE CONCAT(?,'%') AND ZIP_CODE = ? AND DEPT_CODE = ? AND POST_CODE = ? AND IS_PUB = ? LIMIT 0,10
以SQL根据http参数传递情况动态生成,如果http/rpc中没有相关的参数则相应的查询条件不生成。
IS_PUB值不是来自http
TYPE_CODE IN(?,?,?)中的占位符数量根据http中type参数数量生成
如果http中没有提供zip则生成ZIP_CODE IS NULL的查询条件
如果http中没有提供dept则整个SQL将不执行,当前querys函数返回长度为0的DataSet
如果http中没有提供post,则取根据pt取值,如果没有提供pt,则使用默认值101,其中的{}表示常量,而不是http中的参数key
注意IS_PUB的值是一个固定值1,也不是通过http参数中取值,所以不需要放在condition函数中转换