3 个版本
0.1.2 | 2022年4月10日 |
---|---|
0.1.1 | 2022年4月10日 |
0.1.0 | 2022年4月2日 |
#2375 在 数据库接口
45KB
661 代码行
提供关系数据库查询服务,独立于关系数据库,只提供查询服务。
基本原理
- 按照
tables.json
文件的配置内容,将需要查询的数据加载到内存中。 - 按照
tables.json
文件配置的对象关系,在内存中建立关联关系。 - 执行
query
目录下*.path
文件的查询过程。
快速入门
tables.json 文件
tables.json
文件用于配置从数据库中加载的内容,说明如下:
{
// 数据库连接
"conn": "mssql://sa:class123!@[email protected]/AFproduct_zhengshi",
// 数据库表声明
"tables": {
// 表名
"t_userinfo": {
// 主键,可以没有,有主键,会根据主键建立B树,以加快查询
"key": "f_userinfo_id",
// 要加载的字段
"fields": {
// 字段名,及类型,基本类型有:[Int, String, Double, Datetime]
"f_userinfo_id": "Int",
"f_user_state": "String",
// 引用类型有:[OneToOne, OneToMany, ManyToOne],建立了引用类型,在path中就可以按对象路径进行检索
// 引用类型参数:["引用表", "外键字段"].
// 引用表: 在这个表上会建立外键,外键值为主表主键值
// 外键字段: 从表外键,其值为主表主键值
"address": {"OneToOne": ["t_user_address", "f_userinfo_id"]},
"sellinggas": {"OneToMany": ["t_sellinggas", "f_userinfo_id"]},
}
},
// 被引用表的名字
"t_sellinggas": {
"fields": {
"id": "Int",
// 从表外键
"f_userinfo_id": "Int",
// 浮点数用double,不用管数据库本身类型
"f_pregas": "Double",
// 日期型
"f_operate_date": "Datetime",
// 设置多对一关系,参数["引用类型", “外键字段”]
"userinfo": {"ManyToOne":["t_userinfo", "f_userinfo_id"]}
}
}
}
}
路径查询
查询目录下存放查询内容,查询在path.yaml
文件中注册。path语言的查询说明如下:
// 客户端传过来的参数声明,客户端参数按json串提供
p {
// 参数名及类型,类型有[datetime, int, string, double]
start: datetime,
end: datetime
}
// 这里`g.`表示由rust语言提供的函数,这些函数怎么提供,在设计文档里有说明
// `p.`说明取参数值。
// year函数也是rust提供的
let y = g.year(p.start)
// 根据开始时间取得每个月开始及结束时间
// 前面定义的变量可以直接用,不用加前缀
let start1 = g.date_start(y, 1, 1)
let end1 = g.date_end(y, 1, 31)
let start2 = g.date_start(y, 2, 1)
let end2 = g.date_end(y, 2, 28)
// path语言的核心,沿对象路径查询
// 从t_userinfo开始,中括号里内容是过滤过程,可多次过滤
// [1..100]是取过滤后第1到100条数据,用于处理翻页
// `address.f_residential_area`就是沿一对一对象关系进行过滤了
// 点加小括号,是取过滤后的内容
// 最上层表加`db`前缀,表示取数据库表。
// p.s_address: 表示参数address的原始字符串形式,转换后的内容为p.address
// 抽取汇总及分页的公共部分, path支持把公共部分进行抽取
let search = db.t_userinfo[f_user_state != "销户" && (p.s_address == "" || address.f_residential_area == p.address)]
// path支持if语句,page_total是系统传过来的标志,看是否求汇总
if page_total {
// 求总和,total表示,后面的选择是聚集函数。
search.total(count() c)
} else {
// 分页查询内容
// page_start, page_end: 传递过来的页开始及结束参数。
search[page_start..page_end].(
// 取用户编码,每一个选择项都必须有一个别名
f_userinfo_id f_userinfo_id,
// 沿一对一关系取内容
address.f_residential_area area,
// 对于一对多关系,可以在关联对象中进一步过滤,过滤后,调用sum函数求和
// 下面这句是在时间段内,这个用户的总气量
sellinggas[f_operate_date >= p.start && f_operate_date <= p.end].sum(f_pregas) sum_gas,
// 进行两遍过滤,在时间段过滤后,在取出每个月的气量来
sellinggas[f_operate_date >= p.start && f_operate_date <= p.end][f_operate_date >= start1 && f_operate_date <= end1].sum(f_pregas) gas1,
sellinggas[f_operate_date >= p.start && f_operate_date <= p.end][f_operate_date >= start2 && f_operate_date <= end2].sum(f_pregas) gas2
)
}
编译并启动服务
每次修改配置文件及path程序后,都必须重新编译。为了迫使rust编译,把main程序随便改变下。运行如下命令编译:
cargobuild --release
编译完成后,启动服务,把target release
下的内容添加到系统path
环境下,就可以运行如下命令启动服务了:
search
访问服务内容
服务通过支持post发送的postman等工具进行访问,服务内容有:
http://127.0.0.1:8000/monthgas.path
:直接执行查询。http://127.0.0.1:8000/monthgas.path/n
:求总和,将调用path的求总和部分。http://127.0.0.1:8000/monthgas.path/1/5
:求分页内容,结构为:/查询名/页号-从1开始/每页数据
测试
为了测试不受数据库环境影响,提供了数据生成过程,测试用例编写过程如下:
tests下的数据生成文件
在tests下建立json格式的数据生成文件,说明如下:
{
// 要产生测试数据的表
"t_userinfo": {
// 产生记录的个数
"nums": 10,
// 产生记录的字段说明
"fields": {
// 这个字段从1开始,一直累加,每次加1
"f_userinfo_id": {"from": 1},
// 这个字段的内容,只能是给定内容
"f_user_state": {"from": ["正常"]}
}
},
"t_user_address": {
"nums": 10,
"fields": {
"f_userinfo_id": {"from": 1},
"f_residential_area": {"from": ["西安软件园"]}
}
},
"t_sellinggas": {
"nums": 100,
"fields": {
"id": {"from": 1},
// 这个字段从1到10循环产生
"f_userinfo_id": {"from": 1, "to": 10},
// 这个字段内容在[10.0, 10.5, 20.0]之中循环取值
"f_pregas": {"from": [10.0, 10.5, 20.0]},
// 这个字段是日期类型,每次加1秒
"f_operate_date": {"from": "2021-01-01T00:00:00"}
}
}
}
在`src/bin.rs下添加测试用例
每个测试用例内容如下:
// 所有测试用例,统一创建一遍测试数据
initialize();
// 启动服务
let client = Client::tracked(rocket()).expect("valid rocket instance");
// 从外部传给查询的参数
let str = "{\"start\":\"2021-01-01T00:00:00\", \"end\":\"2021-12-31T23:59:59\"}");
// 执行某个查询
let response = client.post("/agg_filter").body(str).dispatch();
// 检查返回状态
assert_eq!(response.status(), Status::Ok);
// 执行快照检查,如果没有快照,或者快照有问题,将产生后缀名为`.new`的快照文件。
// 检查没问题后,把`.new`后缀去掉,在运行测试过程,就通过了。
assert_snapshot!("test_agg_filter", response.into_string().unwrap());
运行测试
命令如下:
cargotest --release
性能测试
可以对查询性能优化进行测试,测试代码在benches
目录下,运行下面命令进行性能测试:
cargobench
并发测试
bin.rs
中test_update
和test_update_search
用来进行并发测试,正常测试不会执行,用下面命令执行。
cargotest ----ignored
添加自己的Rust函数
下一版将尝试在path程序里定义函数,形式如下:
let f x = x + 3
f(5)
对于一些基础函数,应该在Rust语言中进行添加,具体位置是src/search/database.rs
文件,自己定义的函数建议添加在文件末尾。添加好后,在path程序里通过g.函数调用
的方式进行调用。
支持的数据库
目前只提供了sqlserver的支持。path语言与数据库无关,数据库只用于初始数据的加载过程。这段代码在src/search/table.rs
中。
数据变化处理
数据库的每个表增加s_timestamp
,字段类型必须是timestamp
。这种字段,在数据发生变化后,会自动增加。系统根据这个字段内容决定数据是否变化。 读写过程没有加任何锁,Rust对基本数据及集合提供了线程安全的读写操作,不用加锁。
几个可能影响查询性能的考虑
- 一对一关系可能有空值,目前在选择及条件判断时,均加了空值判断,不知是否影响性能。
- 条件中的空值,转换成了默认值。字符串默认值为空串,数字型为0,日期型为0。
- 选择中的空值,返回了json的null。
- 枚举型,比如记录是否有效等,考虑按
u8
形式存放,在字段声明时,把枚举的字符串列出来即可。
查询注册
所有查询全部写在query目录下,查询在path.yaml中进行注册,path.yaml
中有注释说明。
依赖项
~31–66MB
~1M SLoC