pREST功能简介
August 14, 2024
pREST是一个使用go开发的开源项目,用于为 PostgreSQL 数据库创建实时高性能的 REST 接口,旨在简化和加速基于 PostgreSQL 的应用开发。它允许开发者通过简单的配置将现有的 PostgreSQL 数据库转化为一套完整的 REST 服务,无需自己去实现繁杂的后端CRUD服务。 pREST 的核心在于其直接与 PostgreSQL 数据库交互的能力,利用 Go 语言的并发特性实现高效率。此外,它还支持 JWT 身份验证,提供数据迁移工具,并兼容多种环境,如 Docker 和 Heroku。类似的项目有postgrest。
刚好最近在关注一些开源低代码平台,尤其在后台CRUD能力这块一直没有看到纯后端的实现。之前阅读了directus的源码,它在一些复杂关系的处理上感觉很晦涩,不够清晰易懂也缺乏文档说明。刚好看了使用go实现的pREST,所以安装后体验了一把,对相关能力和实现原理有了个大致的了解。
部署
老习惯,还是喜欢本地安装的方式来调试其用法。
- 下载仓库并编译。[shell]
go build -o prestd cmd/prestd/main.go - 配置文件参考 prest.toml[toml]
debug = true [jwt] default = true [auth] enabled = true type = "body" encrypt = "MD5" [pg] host = "127.0.0.1" user = "prest" pass = "prest" port = 5432 database = "prest" single = true [http] port = 3000 [ssl] mode = "disable" [queries] location = "/Users/git/prest/prest/custom_queries/" - DB创建,复用了之前docker创建的postgres服务,新建了用户以及database。
- 创建用户表,供鉴权测试使用。创建用户表完毕后手工在 prest_users 中写入一条记录。[shell]
go run cmd/prestd/main.go migrate up auth - 创建测试表,并写入mock记录。[sql]
CREATE TABLE cars ( brand VARCHAR(255), model VARCHAR(255), year INT ); -- 插入第一条记录 INSERT INTO cars (brand, model, year) VALUES ('Toyota', 'Corolla', 2020); -- 插入第二条记录 INSERT INTO cars (brand, model, year) VALUES ('Honda', 'Civic', 2021); -- 插入第三条记录 INSERT INTO cars (brand, model, year) VALUES ('Ford', 'Mustang', 2019); -- 插入更多记录(这里只是展示格式,你可以继续添加) INSERT INTO cars (brand, model, year) VALUES ('Tesla', 'Model S', 2022); INSERT INTO cars (brand, model, year) VALUES ('BMW', 'X5', 2021); INSERT INTO cars (brand, model, year) VALUES ('Audi', 'A4', 2020); -- 如果你想要批量插入数据,PostgreSQL 也支持这样的语法(使用多个 VALUES) INSERT INTO cars (brand, model, year) VALUES ('Volvo', 'S60', 2022), ('Mercedes-Benz', 'C-Class', 2021), ('Lexus', 'IS', 2020);
所有任务完成后即可启动服务。
接口能力
pREST提供了一系列REST的API,可以通过API了解其基础能力。
meta
主要用于拉取一些元数据,包括db、schema以及table列表。可以通过修改配置禁用此类接口。
| /databases | 列举所有db |
| /schemas | 列举所有schema |
| /tables | 列举所有table |
| /{DATABASE}/{SCHEMA} | 列举db+schema下的所有table |
返回格式为json,以 /tables 为例:
[
{
"name": "cars",
"type": "table",
"owner": "prest",
"schema": "public"
},
{
"name": "prest_users",
"type": "table",
"owner": "prest",
"schema": "public"
},
{
"name": "prest_users_id_seq",
"type": "sequence",
"owner": "prest",
"schema": "public"
}
]
PS:除了手动创建的用户表外(为测试鉴权使用),pREST基本没有其他系统表,类似directus中的collection表、字段配置表以及操作日志等。
auth
用户认证使用,用于获取JWT Token。支持两种方式的认证,注意请求参数携带方式需要与配置文件中的 auth.type 一致。
func Auth(w http.ResponseWriter, r *http.Request) {
login := Login{}
switch config.PrestConf.AuthType {
// TODO: form support
case "body":
// to use body field authentication
dec := json.NewDecoder(r.Body)
dec.DisallowUnknownFields()
//nolint
dec.Decode(&login)
case "basic":
// to use http basic authentication
var ok bool
login.Username, login.Password, ok = r.BasicAuth()
if !ok {
http.Error(w, unf, http.StatusBadRequest)
return
}
}
// ...
}
此时会返回jwt token,后续需要鉴权的请求需要带上对应的 Authorization header。
table
Endpoints: /show/{DATABASE}/{SCHEMA}/{TABLE}
用于查看某个table的表结构,即各个field字段的详细信息,包括字段名、数据类型,长度限制、is_nullable、is_updatable(是否允许更新?)、default_value、table_schema等。以上面的cars表为例,对应存在3个字段。
[
{
"position": 1,
"data_type": "character varying",
"max_length": 255,
"table_name": "cars",
"column_name": "brand",
"is_nullable": "YES",
"is_generated": "NEVER",
"is_updatable": "YES",
"table_schema": "public",
"default_value": null
},
{
"position": 2,
"data_type": "character varying",
"max_length": 255,
"table_name": "cars",
"column_name": "model",
"is_nullable": "YES",
"is_generated": "NEVER",
"is_updatable": "YES",
"table_schema": "public",
"default_value": null
},
{
"position": 3,
"data_type": "integer",
"max_length": 32,
"table_name": "cars",
"column_name": "year",
"is_nullable": "YES",
"is_generated": "NEVER",
"is_updatable": "YES",
"table_schema": "public",
"default_value": null
}
]
ps:table_schema 这种表的公共信息不应该存放在field中。类似directus有一个系统表collection存放所有的表基础信息。
此类信息基本也是从postgres的系统表 information_schema.columns 实时查询而来。对应的sql:
SELECT table_schema, table_name, ordinal_position as position, column_name,data_type,
CASE WHEN character_maximum_length is not null
THEN character_maximum_length
ELSE numeric_precision end as max_length,
is_nullable,
is_generated,
is_updatable,
column_default as default_value
FROM information_schema.columns
WHERE table_name='cars' AND table_schema='public'
ORDER BY table_schema, table_name, ordinal_position
CRUD
- 插入记录: POST /{DATABASE}/{SCHEMA}/{TABLE}
- 更新记录:PUT/PATCH /{DATABASE}/{SCHEMA}/{TABLE}?{FIELD NAME}={VALUE}
- 删除记录:DELETE /{DATABASE}/{SCHEMA}/{TABLE}?{FIELD NAME}={VALUE}
- 查询记录:GET /{DATABASE}/{SCHEMA}/{TABLE}
返回插入、更新后的结果数据。查询时拉取表中的所有数据,可以通过查询参数中的各参数组合指定查询条件。
查询参数
查询参数的本质是将API参数转换为pg对应的SQL语句,pREST支持通过参数来制定sql条件。包括
- 指定查询字段,用于控制select语句后的字段名
- 指定过滤条件,用于控制where语句中的字段条件。
- 翻页参数,控制sql语句中的limit、offset等翻页查询场景的参数。
- 排序参数:控制sql语句中的order字段
- 分组:控制sql语句中的group by
- 聚合参数:对应sql中的count、sum等聚合函数。
来看两个具体的例子。
- 使用distinct查询年份字段,且分页拉取[sql]
-- URL:{{BASE_URL}}/{{DATABASE}}/{{SCHEMA}}/{{TABLE}}?_page=1&_page_size=10&_select=year&_distinct=true -- 对应的sql SELECT DISTINCT "year" FROM "prest"."public"."cars" LIMIT 10 OFFSET(1 - 1) * 10
- 查询每个品牌brand的车数量[sql]
-- URL:{{BASE_URL}}/{{DATABASE}}/{{SCHEMA}}/{{TABLE}}?_select=brand&_count=*&_renderer=json&_groupby=brand -- 对应的sql SELECT COUNT(*), brand FROM "prest"."public"."cars" GROUP BY "brand"
高级查询
operator比较
支持>、<、in、like等各中operator。
- 查询year > 2022 的数据。
-- URL:{{BASE_URL}}/{{DATABASE}}/{{SCHEMA}}/{{TABLE}}?_select=year&year=$gte.2022
-- 对应的sql
SELECT "year" FROM "prest"."public"."cars" WHERE "year" >= 2022
join
用于连表查询,格式:
/{DATABASE}/{SCHEMA}/{TABLE}?_join={TYPE}:{TABLE JOIN}:{TABLE.FIELD}:{OPERATOR}:{TABLE JOIN.FIELD}
参数介绍
- Type: 关联类型
- inner
- left
- right
- outer
- Table 用于关联的表
- Table.field 用于关联的字段
- Operator: 字段标胶操作符
- $eq
- $lt
- $gt
- $lte
- $gte
- Table2.field - 用于关联表及字段。
来个列子:
-- 创建一个新表用于关联
CREATE TABLE brands (
brand VARCHAR(255),
information text
);
INSERT INTO brands (brand, information) VALUES ('Toyota', 'Toyota Motor Corporation is a Japanese multinational automotive manufacturer headquartered in Toyota City');
INSERT INTO brands (brand, information) VALUES ('Tesla', 'Tesla, Inc. is an American electric vehicle and clean energy company based in Austin, Texas.');
INSERT INTO brands (brand, information) VALUES ('Apple', 'Apple Inc. is an American multinational technology company that specializes in consumer electronics, computer software, and online services.');
-- 构建查询
-- URL: {{BASE_URL}}/{{DATABASE}}/{{SCHEMA}}/{{TABLE}}?_select=year,cars.brand,model,brands.information&_join=left:brands:cars.brand:$eq:brands.brand&cars.brand=Tesla
-- 对应的SQL:
SELECT "year","cars"."brand","model","brands"."information"
FROM "prest"."public"."cars"
LEFT JOIN "brands" ON "cars"."brand" = "brands"."brand"
WHERE "cars"."brand" = 'Tesla'
注意:查询语句的构造参数过于复杂,目前仅支持1级关联。如果需要更加复杂的关联,官方建议使用自定义查询。
自定义查询
对于一些复杂查询的场景,pREST采用了直接交给用户编写sql的方案来实现自定义查询方式解决。
执行前,先创建一份查询模板文件,文件位置与配置文件中的location参数一致。如 custom_queries/awesome_folder/example_of_cars.read.sql
SELECT * FROM cars WHERE brand = '{{.bd}}' OR model = '{{.md}}'
注意需要在location下新建二级目录,因为go路由匹配需要。
在模板文件中有两个参数,需要在API调用时由客户端通过http 参数或者header携带到服务端,拼接成完整的sql。
所以其调用方式大致如下:
curl {{BASE_URL}}/_QUERIES/awesome_folder/example_of_cars?bd=BMW&md=A4
可见自定义查询的本质就是使用模板字符串和外部参数,拼接成完整的SQL语句并执行,来保证最大程度的灵活性。
这样也有一个问题,模板通常是固定死的,比如想在模板中使用in语句,如 where field in ({{bd1}}, {{bd2}}) 。如果参数数量不固定,这样就需要编写多套类似的模板,根据参数数量来调用不同的模板。对于此类问题,pREST也提供了一定的拓展能力。
pREST提供了几个函数来处理动态参数的问题:
// RegistryAllFuncs for template
func (fr *FuncRegistry) RegistryAllFuncs() (funcs template.FuncMap) {
funcs = template.FuncMap{
"isSet": fr.isSet,
"defaultOrValue": fr.defaultOrValue,
"inFormat": fr.inFormat,
"unEscape": fr.unEscape,
"split": fr.split,
"limitOffset": fr.limitOffset,
}
return
}
以defaultOrValue为例,此函数用来提供默认参数,如果客户端没有携带参数bd时,就使用sql中指定的默认参数:
SELECT * FROM cars
WHERE brand = '{{defaultOrValue "bd" "Tesla"}}'
这样客户端就可以指定参数或者使用默认参数
curl {{BASE_URL}}/_QUERIES/awesome_folder/defaultOrValue?bd=Volvo
curl {{BASE_URL}}/_QUERIES/awesome_folder/defaultOrValue
总结
pREST作为一个纯后台项目,在灵活性上还是非常不错的。虽然仅支持pg数据库,但是查询的基本要素基本都支持了,包括:
- select字段
- where条件中的字段以及对应的operation操作:> < == in 等
- order指定
- group by分组
- 聚合函数,包括count、sum、avg、max/min等
可以满足基础的查询功能。另外也支持简单的join需求。
PS:本文所实现的查询http请求存放在了postman,需要的可以在这里fork一份。