#table #query #json #快速查询库

nightly bin+lib af_search

快速查询库

3 个版本

0.1.2 2022年4月10日
0.1.1 2022年4月10日
0.1.0 2022年4月2日

#2375数据库接口

MIT/Apache

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.rstest_updatetest_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