pREST功能简介

August 14, 2024

pREST是一个使用go开发的开源项目,用于为 PostgreSQL 数据库创建实时高性能的 REST 接口,旨在简化和加速基于 PostgreSQL 的应用开发。它允许开发者通过简单的配置将现有的 PostgreSQL 数据库转化为一套完整的 REST 服务,无需自己去实现繁杂的后端CRUD服务。 pREST 的核心在于其直接与 PostgreSQL 数据库交互的能力,利用 Go 语言的并发特性实现高效率。此外,它还支持 JWT 身份验证,提供数据迁移工具,并兼容多种环境,如 Docker 和 Heroku。类似的项目有postgrest

刚好最近在关注一些开源低代码平台,尤其在后台CRUD能力这块一直没有看到纯后端的实现。之前阅读了directus的源码,它在一些复杂关系的处理上感觉很晦涩,不够清晰易懂也缺乏文档说明。刚好看了使用go实现的pREST,所以安装后体验了一把,对相关能力和实现原理有了个大致的了解。

部署

老习惯,还是喜欢本地安装的方式来调试其用法。

  1. 下载仓库并编译。
    [shell]
    go build -o prestd cmd/prestd/main.go
  2. 配置文件参考 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/"
  3. DB创建,复用了之前docker创建的postgres服务,新建了用户以及database。
  4. 创建用户表,供鉴权测试使用。创建用户表完毕后手工在 prest_users 中写入一条记录。
    [shell]
    go run cmd/prestd/main.go migrate up auth
  5. 创建测试表,并写入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 为例:

[json]
[
    {
        "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 一致。

[go]
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个字段。

[json]
[
    {
        "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:

[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 的数据。
[sql]
-- URL:{{BASE_URL}}/{{DATABASE}}/{{SCHEMA}}/{{TABLE}}?_select=year&year=$gte.2022
-- 对应的sql
SELECT "year" FROM "prest"."public"."cars" WHERE "year" >= 2022

join

用于连表查询,格式:

[text]
/{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 - 用于关联表及字段。

来个列子:

[sql]
-- 创建一个新表用于关联
 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

[sql]
SELECT * FROM cars WHERE brand = '{{.bd}}' OR model = '{{.md}}'

注意需要在location下新建二级目录,因为go路由匹配需要。

在模板文件中有两个参数,需要在API调用时由客户端通过http 参数或者header携带到服务端,拼接成完整的sql。

所以其调用方式大致如下:

[shell]
curl {{BASE_URL}}/_QUERIES/awesome_folder/example_of_cars?bd=BMW&md=A4

 

可见自定义查询的本质就是使用模板字符串和外部参数,拼接成完整的SQL语句并执行,来保证最大程度的灵活性。

这样也有一个问题,模板通常是固定死的,比如想在模板中使用in语句,如 where field in ({{bd1}}, {{bd2}}) 。如果参数数量不固定,这样就需要编写多套类似的模板,根据参数数量来调用不同的模板。对于此类问题,pREST也提供了一定的拓展能力。

pREST提供了几个函数来处理动态参数的问题:

[go]
// 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中指定的默认参数:

[sql]
SELECT * FROM cars
WHERE brand = '{{defaultOrValue "bd" "Tesla"}}'

这样客户端就可以指定参数或者使用默认参数

[shell]
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需求。

👉🏼
但是,对于复杂的join(比如多级join)查询是无法直接支持的,只能通过自定义查询去写sql解决。本质的难点还是如何将REST参数转换为多级join,在directus的实现中,提供了管理台来给到管理员去主动去配置 1:N 、N:1 等关联关系,降低了普通用户的使用门槛。但是pREST作为一个后台项目,确实提供自定义查询是一个不错的思路。毕竟开发者对自己的数据模型会理解的更加的清楚,也避免去理解类似directus中的Relation关系的成本。

PS:本文所实现的查询http请求存放在了postman,需要的可以在这里fork一份。

See all postsSee all posts