您现在的位置是:首页 >技术杂谈 >【Goalng 开源项目】还在手写重复的 CRUD 吗?这个开源项目帮你解放双手网站首页技术杂谈

【Goalng 开源项目】还在手写重复的 CRUD 吗?这个开源项目帮你解放双手

萌宅鹿同学 2024-10-25 00:01:06
简介【Goalng 开源项目】还在手写重复的 CRUD 吗?这个开源项目帮你解放双手

Gormpher 介绍

gormpher 是一个轻量级的 Golang 库

  • 基于 Gin 和 Gorm
  • WebObject 机制:根据模型生成对应的 Restful API,一键生成平时开发中重复的 CRUD 代码
  • 通用 Gorm 泛型函数
  • 基于泛型的动态接口函数
  • 将 WebObject 注册到 Admin,自带测试用的 Web 界面
  • 完善的单元测试,覆盖率接近 80%(完善中)

gormpher github:https://github.com/restsend/gormpher

使用 gormpher 的开源项目 rabbit-admin:https://github.com/szluyu99/rabbit-admin
建议看完本文,理解 gormpher 后可以查看该开源项目,体会其在实际开发中到底能节约多少工作量

快速开始

先直观的体会一下这个项目最基础的功能是用来做什么的。

go get github.com/restsend/gormpher

示例源于 gormpher 仓库下 example/main.go:(下面是其简化版)

代码版本可能会更新,文章中不一定是最新的,可以参考上面的 example/main.go 文件获取最新用法

使用流程:

  1. 定义好一个结构体 struct,指定其 primarykey
  2. 创建 gormpher 依赖的 gorm 指针 和 gin 路由对象(后续考虑适配其他 web 框架)
  3. 创建 gormpher 中的 WebObject 对象
  4. 注册 WebObject 对象到指定路由组
  5. (可选)将 WebObject 对象注册到 Admin 中,展示一个测试用的 Web 界面
package main

import (
	"flag"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/restsend/gormpher"
	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

// 1. 定义结构体,指定 primarykey
type User struct {
	ID        uint       `json:"id" gorm:"primarykey"`
	CreatedAt time.Time  `json:"createdAt"`
	UpdatedAt time.Time  `json:"updatedAt"`
	Name      string     `json:"name"`
	Age       int        `json:"age"`
	Enabled   bool       `json:"enabled"`
	LastLogin *time.Time `json:"lastLogin"`
}

func main() {
	var dsn string
	var addr string

	flag.StringVar(&dsn, "n", "", "DB DSN")
	flag.StringVar(&addr, "a", ":8890", "Api Server Addr")
	flag.Parse()

	// 2. gorm db, gin router
	db, _ := gorm.Open(sqlite.Open(dsn), nil)
	db.AutoMigrate(User{})
	r := gin.Default()
	
	// 3. 创建 gormpher WebObject 对象(User 为模型对象)
	objs := []gormpher.WebObject{
		{
			Name:         "user",
			Model:        &User{},
			SearchFields: []string{"Name", "Enabled"}, // 可模糊搜索的字段
			EditFields:   []string{"Name", "Age", "Enabled", "LastLogin"}, // 可编辑的字段
			FilterFields: []string{"Name", "CreatedAt", "Age", "Enabled"}, // 可条件查询的字段
			OrderFields:  []string{"CreatedAt", "Age", "Enabled"}, // 可排序的字段
			GetDB:        func(ctx *gin.Context, isCreate bool) *gorm.DB { return db }, // 返回 *gorm.DB 的方法 
		},
	}

	// 4. 注册 gormpher WebObject 对象到指定路由组,生成以下 API
	// PUT 		http://localhost:8890/api/user
	// GET 		http://localhost:8890/api/user/:key
	// PATCH	http://localhost:8890/api/user/:key
	// POST 	http://localhost:8890/api/user
	// DELETE   http://localhost:8890/api/user/:key
	gormpher.RegisterObjects(r.Group("api"), objs)
	
	// 5. (可选)将 WebObject 对象注册到 Admin 中,展示一个测试用的 Web 界面
	// 访问 URL: http://localhost:8890/admin
	gormpher.RegisterObjectsWithAdmin(r.Group("admin"), objs)
	
	r.Run(addr)
}

运行该程序:

go run main.go

经过上面简短的代码,我们就生成了以下 API:

[GIN-debug] GET    /api/user/:key            --> github.com/restsend/gormpher.(*WebObject).RegisterObject.func1 (3 handlers)
[GIN-debug] PUT    /api/user                 --> github.com/restsend/gormpher.(*WebObject).RegisterObject.func2 (3 handlers)
[GIN-debug] PATCH  /api/user/:key            --> github.com/restsend/gormpher.(*WebObject).RegisterObject.func3 (3 handlers)
[GIN-debug] DELETE /api/user/:key            --> github.com/restsend/gormpher.(*WebObject).RegisterObject.func4 (3 handlers)
[GIN-debug] POST   /api/user                 --> github.com/restsend/gormpher.(*WebObject).RegisterObject.func5 (3 handlers)

目前你可能不太理解这些接口的用法,具体参数本文后面会介绍:核心约定


访问:http://localhost:8890/admin 查看测试用的 Web 界面

先尝试体验一下,通过上面这么简单的几行代码可以实现的接口效果。

在这里插入图片描述

WebObject 接口约定

平时开发中的重复的 CRUD 代码可以归纳为以下情况:

  • 新增数据(单条)- Create
  • 删除数据(单条 或 多条)- Delete
  • 编辑数据(单条)- Edit
  • 查询单条数据 - Get
  • 条件查询多条数据(分页)- Query

概述:Gormpher 的核心思想是基于模型对象来生成其 API,例如对于一个 user 模块

  • PUT /user 创建一条 user 数据
    • 请求体 传递要创建的 user 对象
  • DELETE /user/:key 删除主键为 key 的 user 数据
  • PATCH /user/:key 即编辑主键为 key 的 user 数据
    • 请求体 传递要编辑的 user 对象
  • GET /user/:key 查询主键为 key 的单条 user 数据
  • POST /user 根据条件查询多条 user 的信息(分页)
    • 请求体 传递查询参数

涉及到 请求体 全部使用 Content-Type: application/json 传输数据

即对于 user 模块,最终我们生成的 API 如下:

GET    /user/:key
PUT    /user
PATCH  /user/:key
DELETE /user/:key
POST   /user

下面再解析一下以上的行为,并给出对应的接口请求与响应示例。

查询单条数据

GET /user/:key

  • 请求参数
    • URL 路径参数:key 为数据的主键
# Path
GET /user/1
  • 响应数据:User 对象
{
    "id": 1,
    "createdAt": "2023-06-13T23:43:27.590377962+08:00",
    "updatedAt": "2023-06-13T23:43:27.590377962+08:00",
    "name": "u1",
    "age": 10,
    "enabled": false,
    "lastLogin": "2023-06-01T23:43:00Z"
}

删除单条数据

DELETE /user/:key

  • 请求参数
    • URL 路径参数:key 为数据的主键
# Path
DELETE /user/1
  • 响应数据Boolean,表示是否删除成功
true

创建单条数据

PUT /user

  • 请求参数
    • 请求体参数:User 对象
# Path
PUT /user

# Request Body
{
    "name": "u2",
    "age": 5,
    "lastLogin": "2023-06-01T23:49",
    "enabled": true
}
  • 响应数据:User 对象
{
    "id": 2,
    "createdAt": "2023-06-13T23:50:01.615012885+08:00",
    "updatedAt": "2023-06-13T23:50:01.615012885+08:00",
    "name": "u2",
    "age": 5,
    "enabled": true,
    "lastLogin": "2023-06-01T23:49:00Z"
}

编辑单条数据

PATCH /user/:key

  • 请求参数
    • URL 路径参数:key 为数据的主键
    • 请求体参数:需要编辑的 User 对象(未传的属性不会被修改,传了的即使是空值也会被修改)
# Path
PATCH /user/1

# Request Body
{
	"enabled" :  true,
	"name": "aaaa"
}
  • 响应数据Boolean,表示是否编辑成功
true

注意:

  • 并不是所有的字段都可以被前端传来的参数所编辑的
  • 因此,我们将可以被编辑的字段暴露成一个配置项 EditFields
  • 如果前端传递了非 EditFeilds 中指定的字段,则无法编辑成功

示例中指定了 user 可编辑字段为:NameAgeEnabledLastLogin

{
	// ...
	Model:        &User{},
	EditFields:   []string{"Name", "Age", "Enabled", "LastLogin"}, // 可编辑的字段
	// ...
}

条件查询多条数据

POST /user

  • 请求参数QueryForm 对象
    • 请求体参数:User 对象
  • 响应数据QueryResult 对象

这是一个非常重要且复杂的行为,大部分重复的代码都出自它,为抽象出一个通用规则,我们将以下字段暴露成可配置项:

  • SearchFields:可以被模糊搜索的字段
  • FilterFields:可以被条件查询的字段
  • OrderFields:可以被排序的字段
{
	// ...
	Model:        &User{},
	SearchFields: []string{"Name", "Enabled"}, // 可模糊搜索的字段
	FilterFields: []string{"Name", "CreatedAt", "Age", "Enabled"}, // 可条件查询的字段
	OrderFields:  []string{"CreatedAt", "Age", "Enabled"}, // 可排序的字段
	// ...
}

查询表单对象:QueryForm

NameTypeDescDefault
posnumber分页参数,数据查询位置0
limitnumber分页参数 ,数据查询范围50
keywordnumber模糊搜索关键字,字段需要配置在 SearchFields“”
filters[]Filter条件查询对象数组,字段需要配置在 FilterFieldsnull
orders[]Order排序对象数组,字段需要配置在 OrderFieldsnull

Filter:

NameOpDesc
namestring字段
opstring=, <>, in, not_in, >, >=, <, <=
valuestring

示例:检索 age > 10 且 enabled = true 的数据,请求参数如下

{
	filters: [
		{ "name": "age", "op": ">", "value": 10 },
		{ "name": "enabled", "op": "=", "value": true }
	]
}

Order:

NameOpDesc
namestring
opstringasc, desc

示例:检索按 age 降序排序的数据,请求参数如下

{
	orders: [
		{ "name": "age", op: "desc" }
	]
}

请求示例:检索条件如下

  • 查询开始位置 0,查询数量 10
  • 模糊查询关键字为 “u”
  • age > 10 且 enabled = true
  • 按 age 降序排序
{
	"pos": 0,
	"limit": 10,
	"keyword": "u",
	"filters": [
		{ "name": "age", "op": ">", "value": 10 },
		{ "name": "enabled", "op": "=", "value": true }
	],
	"orders": [
		{ "name": "age", op: "desc" }
	]
}

查询结果:QueryResult

NameTypeDesc
posnumber本次查询中分页参数
limitnumber本次查询分页参数
keywordstring本次查询模糊搜索关键字
totalnumber本次查询数据总数
items[]object本次查询数据数组

响应示例:

{
    "total": 2,
    "pos": 0,
    "limit": 10,
    "items": [
        {
            "id": 1,
            "createdAt": "2023-06-13T23:43:27.590377962+08:00",
            "updatedAt": "2023-06-14T00:23:36.46322106+08:00",
            "name": "u1",
            "age": 10,
            "enabled": true,
            "lastLogin": "2023-06-01T23:43:00Z"
        },
        {
            "id": 2,
            "createdAt": "2023-06-13T23:50:01.615012885+08:00",
            "updatedAt": "2023-06-13T23:50:01.615012885+08:00",
            "name": "u2",
            "age": 5,
            "enabled": true,
            "lastLogin": "2023-06-01T23:49:00Z"
        }
    ]
}

下面的内容后续完善。

进阶

WebObject 配置项

动态接口函数

Gorm 泛型函数

Admin

源码

handleEditObject

handleQueryObject

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。