【CloudWeGo】字节跳动 Golang 微服务框架 Hertz 集成 Gorm-Gen 最佳实践

hello大家好,我是千羽。
其实在上一篇,讲了《Golang 微服务框架 Hertz 集成 Gorm 》之后,这里已经有持久层orm框架Gorm,为什么还会有Gorm Gen呢?Gorm Gen又是什么呢?
Gorm Gen是什么?
早在 2021 年 11 月,我就在字节技术公众号上第一次了解到 Gen,还因此加入了一个技术交流群。时间过去三年才来写这篇文章,是不是有点懒呢?😂 其实就是懒~

GORM Gen官方介绍

GEN 是一个基于 GORM 的安全 ORM 框架,其主要通过代码生成方式实现 GORM 代码封装。旨在安全上避免业务代码出现 SQL 注入,同时给研发带来最佳用户体验。
在我看来,它相当于 Java 的 MyBatis 加强版 —— MyBatis-Plus。
GEN使用文档地址:https://github.com/go-gorm/gen#readme

为什么说 GORM Gen 带来最佳用户体验?
⚡️ 自动同步库表,省去繁琐复制
🔗 代码一键生成,专注业务逻辑
🐞 字段类型安全,执行 SQL 也安全
😉 查询优雅返回,完美兼容 GORM
GEN 提供了自动同步数据表结构体到 GORM 模型,使用非常简单,即使数据库字段信息改变,可以一键同步,数据库查询相关代码可以一键生成,CRUD 只需要调用对应的方法,开发体验飞起。
GEN 采用了类型安全限制,所有参数都做了安全限制,完全不用担心存在注入;最重要的是自定义 SQL 只需要通过模板注释到 interface 的方法上,自动帮助你生成安全的代码,是的,自定义 SQL 也不会出现 SQL 注入问题,而且工具完美兼容 GORM!
使用 GORM 与 GEN 工具的对比

GORM 和 GEN 查询对比案例
//GORM 需要先定义类型
var user model.User
err:=db.Where(“id=?”,5).Take(&user).Error

//GEN 可以直接查询,返回对应类型
user,err:= u.Where(u.ID.Eq(5)).Take()
Hertz 集成 Gorm-Gen
以下是 Hertz 项目的基本目录结构,展示了如何集成 Gorm-Gen:
├── biz
│ ├── dal # 实现具体的数据库连接等操作
│ ├── handler # handler层
│ │ ├── ping.go
│ │ └── user
│ │ └── user_service.go
│ ├── model
│ │ ├── api
│ │ │ └── api.pb.go
│ │ ├── hertz
│ │ │ └── user
│ │ │ └── user.pb.go
│ │ ├── model
│ │ │ └── users.gen.go # 描述与数据库表对应的数据结构(体)
│ │ ├── orm_gen
│ │ │ └── users.gen.go
│ │ ├── query # 生成的代码存放目录, 在执行代码生成操作后自动创建
│ │ │ ├── gen.go # 生成的通用查询代码
│ │ │ └── users.gen.go # 生成的单个表字段和相关的查询代码
│ ├── pack
│ │ └── user.go # 数据转换
│ └── router
│ ├── register.go
│ └── user
│ ├── middleware.go
│ └── user.go
├── cmd
│ └── generate.go # 包含main函数,执行其即可完成生成代码步骤
├── docker-compose.yml
├── go.mod
├── go.sum
├── hertz_gorm_gen
├── idl # 存放proto 文件
│ ├── api.proto
│ └── user
│ └── user.proto
├── main.go
├── router.go
└── router_gen.go

快速开始
在 数据库初始化文件 中更新数据库 DSN 为你自己的配置。
参考代码注释,在 生成文件 中编写配置。
使用以下命令进行代码生成,你可以从数据库生成结构体或为结构体生成基础的类型安全 DAO API。
cd bizdemo/hertz_gorm_gen/cmd
go run generate.go
如何运行
Step 1:使用 Docker 启动 MySQL容器
cd bizdemo/hertz_gorm_gen && docker-compose up
由于上一篇项目mysql的端口是 9910,所以这次mysql和项目的有差异,记得修改~~
version: ‘3’
services:
mysql:
image: ‘mysql:latest’
volumes:
– ./biz/model/sql:/docker-entrypoint-initdb.d
ports:
– 9910:3306
environment:
– MYSQL_DATABASE=gorm
– MYSQL_USER=gorm
– MYSQL_PASSWORD=gorm
– MYSQL_RANDOM_ROOT_PASSWORD=”yes”
Step 2:编译并运行项目
cd bizdemo/hertz_gorm_gen
go build -o hertz_gorm_gen && ./hertz_gorm_gen
看到下面的日志👇,就说明启动成功啦
2024/11/09 11:32:23.830397 engine.go:396: [Info] HERTZ: Using network library=netpoll
2024/11/09 11:32:23.830493 transport.go:115: [Info] HERTZ: HTTP server listening on address=[::]:8888

API 接口调试
根据启动的日志,我们进行各个接口验证
2024/11/09 11:32:23.830039 engine.go:668: [Debug] HERTZ: Method=POST absolutePath=/v1/user/create –> handlerName=github.com/cloudwego/hertz-examples/bizdemo/hertz_gorm_gen/biz/handler/user.CreateUserResponse (num=2 handlers)
2024/11/09 11:32:23.830258 engine.go:668: [Debug] HERTZ: Method=POST absolutePath=/v1/user/query –> handlerName=github.com/cloudwego/hertz-examples/bizdemo/hertz_gorm_gen/biz/handler/user.QueryUserResponse (num=2 handlers)
2024/11/09 11:32:23.830267 engine.go:668: [Debug] HERTZ: Method=POST absolutePath=/v1/user/delete/:user_id –> handlerName=github.com/cloudwego/hertz-examples/bizdemo/hertz_gorm_gen/biz/handler/user.DeleteUserResponse (num=2 handlers)
2024/11/09 11:32:23.830274 engine.go:668: [Debug] HERTZ: Method=POST absolutePath=/v1/user/update/:user_id –> handlerName=github.com/cloudwego/hertz-examples/bizdemo/hertz_gorm_gen/biz/handler/user.UpdateUserResponse (num=2 handlers)
2024/11/09 11:32:23.830279 engine.go:668: [Debug] HERTZ: Method=GET absolutePath=/ping
/ping 接口
请求:http://localhost:8888/ping
响应:
{
“message”: “pong”
}

具体的代码是在:bizdemo/hertz_gorm_gen/biz/handler/ping.go
// Ping .
func Ping(ctx context.Context, c *app.RequestContext) {
c.JSON(200, utils.H{
“message”: “pong”,
})
}
1.创建用户接口
请求:http://localhost:8888/v1/user/create/
入参
{
“name”: “hertz_gorm_gen”,
“gender”: 1,
“age”: 18,
“introduce”: “牛马打工人”
}
响应:
{
“msg”: “Create data successfully”
}

去mysql查询

  1. 查询用户接口 /v1/user/query/
    请求 URL: http://localhost:8888/v1/user/query/
    请求参数:
    {
    “page”: 1,
    “page_size”:10
    }
    响应示例:
    {
    “user”: [
    {
    “user_id”: 1,
    “name”: “hertz_gorm_gen”,
    “gender”: 1,
    “age”: 18,
    “introduce”: “牛马打工人”
    }
    ],
    “total”: 10
    }
  2. 修改用户接口 /v1/user/update/11
    请求:http://localhost:8888/v1/user/update/11
    请求参数:
    {
    “name”: “hertz_gorm_gen”,
    “gender”: 1,
    “age”: 18,
    “introduce”: “下班”
    }
  3. 删除用户接口 /v1/user/delete/1
    删除,软删除,不会真正的删除

Step 4:代码解析
api.proto
syntax = “proto3”;
package api;
import “google/protobuf/descriptor.proto”;
option go_package = “/api”;

extend google.protobuf.FieldOptions {
string raw_body = 50101;
string query = 50102;
string header = 50103;
string cookie = 50104;
string body = 50105;
string path = 50106;
string vd = 50107;
string form = 50108;
string go_tag = 51001;
string js_conv = 50109;
}

extend google.protobuf.MethodOptions {
string get = 50201;
string post = 50202;
string put = 50203;
string delete = 50204;
string patch = 50205;
string options = 50206;
string head = 50207;
string any = 50208;
string gen_path = 50301;
string api_version = 50302;
string tag = 50303;
string name = 50304;
string api_level = 50305;
string serializer = 50306;
string param = 50307;
string baseurl = 50308;
}

extend google.protobuf.EnumValueOptions {
int32 http_code = 50401;
}
代码讲解:
syntax = “proto3”; 表示使用 Protocol Buffers 版本 3。
package api; 声明包名为 api。
import “google/protobuf/descriptor.proto”; 引入 descriptor.proto 文件,以便使用 FieldOptions、MethodOptions、EnumValueOptions 等扩展定义。
option go_package = “/api”; 指定生成的 Go 代码的包名。
FieldOptions 扩展在 FieldOptions 扩展部分,定义了一系列与字段(field)相关的自定义选项。这些选项可以让字段在传输时通过不同的方式传递参数,例如从请求体、查询参数、头信息等位置提取。
raw_body、query、header 等扩展字段:这些字段定义如何将消息字段映射到 HTTP 请求的不同部分,如 query 表示查询参数,header 表示 HTTP 头。
go_tag:用于在生成的 Go 代码中定义自定义标签,方便进行代码生成控制。
js_conv:可能用于 JavaScript 相关的转换设置。
MethodOptions 扩展在 MethodOptions 扩展部分,定义了一些 HTTP 请求相关的选项,用于配置 API 的请求类型、路径和其他属性。
get、post、put 等:这些字段指定 HTTP 方法类型,比如 get 表示 GET 请求,post 表示 POST 请求。
gen_path:用于定义 API 路径。
api_version:指定 API 的版本。
tag:标签,可以用于 API 的文档生成或分组。
name、api_level、serializer 等:可以为 API 请求指定额外的属性,比如 serializer 指定序列化方式,baseurl 设置基础 URL,param 用于指定额外的请求参数。
user.proto
syntax = “proto3”;
package user;
// biz/model
option go_package = “hertz/user”;
import “api.proto”;
enum Code {
Success = 0;
ParamInvalid = 1;
DBErr = 2;
}

enum Gender {
Unknown = 0;
Male = 1;
Female = 2;
}

message User{
int64 user_id = 1;
string name = 2;
Gender gender = 3;
int64 age = 4;
string introduce = 5;
}

message CreateUserReq{
string name = 1 [(api.body) = “name”, (api.form) = “name”, (api.vd) = “(len($) > 0 && len($) < 100)”]; Gender gender = 2 [(api.body) = “gender”, (api.form) = “gender”, (api.vd) = “($ == 1||$ == 2)”]; int64 age = 3 [(api.body) = “age”, (api.form) = “age”, (api.vd) = “$>0”];
string introduce = 4 [(api.body) = “introduce”, (api.form) = “introduce”, (api.vd) = “(len($) > 0 && len($) < 1000)”];
}

message CreateUserResp{
Code code = 1;
string msg = 2;
}

message QueryUserReq{
string keyword = 1 [(api.body) = “keyword”, (api.form) = “keyword”];
int64 page = 2 [(api.body) = “page”, (api.form) = “page”, (api.vd) = “$>0”];
int64 page_size = 3 [(api.body) = “page_size”, (api.form) = “page_size”, (api.vd) = “($ > 0 || $ <= 100)”];
}

message QueryUserResp{
Code code = 1;
string msg = 2;
repeated User user = 3;
int64 total = 4;
}

message DeleteUserReq{
int64 user_id = 1 [(api.path) = “user_id”, (api.vd) = “$>0”];
}

message DeleteUserResp{
Code code = 1;
string msg = 2;
}

message UpdateUserReq{
int64 user_id = 1 [(api.path) = “user_id”, (api.vd) = “$>0”];
string name = 2 [(api.body) = “name”, (api.form) = “name”, (api.vd) = “(len($) > 0 && len($) < 100)”]; Gender gender = 3 [(api.body) = “gender”, (api.form) = “gender”, (api.vd) = “($ == 1||$ == 2)”]; int64 age = 4 [(api.body) = “age”, (api.form) = “age”, (api.vd) = “$>0”];
string introduce = 5 [(api.body) = “introduce”, (api.form) = “introduce”, (api.vd) = “(len($) > 0 && len($) < 1000)”];
}

message UpdateUserResp{
Code code = 1;
string msg = 2;
}

message OtherResp {
string msg = 1;
}

service UserService {
rpc CreateUserResponse(CreateUserReq) returns(CreateUserResp) {
option (api.post) = “/v1/user/create”;
}
rpc QueryUserResponse(QueryUserReq) returns(QueryUserResp){
option (api.post) = “/v1/user/query”;
}
rpc UpdateUserResponse(UpdateUserReq) returns(UpdateUserResp){
option (api.post) = “/v1/user/update/:user_id”;
}
rpc DeleteUserResponse(DeleteUserReq) returns(DeleteUserResp){
option (api.post) = “/v1/user/delete/:user_id”;
}
}

枚举类型
Code:定义了常见的返回码,包括 Success(成功)、ParamInvalid(参数无效)、DBErr(数据库错误)。
Gender:定义性别类型,包含 Unknown(未知)、Male(男性)、Female(女性)。
消息类型
User:用户信息的基本结构,包括 user_id(用户ID)、name(用户名)、gender(性别)、age(年龄)和 introduce(自我介绍)。
CreateUserReq、CreateUserResp:用于创建用户的请求和响应,包含用户基本信息和验证规则。
QueryUserReq、QueryUserResp:用于查询用户的请求和响应,支持分页查询并包含用户列表。
DeleteUserReq、DeleteUserResp:用于删除用户的请求和响应。
UpdateUserReq、UpdateUserResp:用于更新用户的请求和响应。
服务定义
UserService:包含以下接口:
CreateUserResponse:创建用户,POST 请求路径为 /v1/user/create。
QueryUserResponse:查询用户,POST 请求路径为 /v1/user/query。
UpdateUserResponse:更新用户,POST 请求路径为 /v1/user/update/:user_id。
Step 4:代码解析
创建用户 /v1/user/create
// CreateUserResponse .
// @router /v1/user/create [POST]
func CreateUserResponse(ctx context.Context, c *app.RequestContext) {
var err error
var req user.CreateUserReq
err = c.BindAndValidate(&req)
if err != nil {
c.String(400, err.Error())
return
}

resp := new(user.CreateUserResp)
err = query.User.WithContext(ctx).Create(&orm_gen.User{
Name: req.Name,
Gender: int32(req.Gender),
Age: int32(req.Age),
Introduce: req.Introduce,
})
if err != nil {
resp.Code = user.Code_ParamInvalid
resp.Msg = err.Error()
c.JSON(200, resp)
return
}

resp.Code = user.Code_Success
resp.Msg = “Create data successfully”
c.JSON(200, resp)
}
查询用户 /v1/user/query
// QueryUserResponse .
// @router /v1/user/query [POST]
func QueryUserResponse(ctx context.Context, c *app.RequestContext) {
var err error
var req user.QueryUserReq
err = c.BindAndValidate(&req)
if err != nil {
c.String(400, err.Error())
return
}
resp := new(user.QueryUserResp)
u, m := query.User, query.User.WithContext(ctx)
if req.Keyword != “” {
m = m.Where(u.Introduce.Like(“%” + req.Keyword + “%”))
}

var total int64
total, err = m.Count()
if err != nil {
resp.Code = user.Code_DBErr
resp.Msg = err.Error()
c.JSON(200, resp)
return
}

var users []*orm_gen.User
if total > 0 {
users, err = m.Limit(int(req.PageSize)).Offset(int(req.PageSize * (req.Page – 1))).Find()
if err != nil {
resp.Code = user.Code_DBErr
resp.Msg = err.Error()
c.JSON(200, resp)
return
}
}

resp.Code = user.Code_Success
resp.Total = total
resp.User = pack.Users(users)
c.JSON(200, resp)
}
更新用户 /v1/user/update/:user_id
// UpdateUserResponse .
// @router /v1/user/update/:user_id [POST]
func UpdateUserResponse(ctx context.Context, c *app.RequestContext) {
var err error
var req user.UpdateUserReq
err = c.BindAndValidate(&req)
if err != nil {
c.String(400, err.Error())
return
}

resp := new(user.UpdateUserResp)
u := &orm_gen.User{}
u.ID = req.UserId
u.Name = req.Name
u.Gender = int32(req.Gender)
u.Age = int32(req.Age)
u.Introduce = req.Introduce
_, err = query.User.WithContext(ctx).Updates(u)
if err != nil {
resp.Code = user.Code_DBErr
resp.Msg = err.Error()
c.JSON(200, resp)
return
}

resp.Code = user.Code_Success
resp.Msg = “Update data successfully”
c.JSON(200, resp)
}
删除用户 /v1/user/delete/:user_id
func DeleteUserResponse(ctx context.Context, c *app.RequestContext) {
var err error
var req user.DeleteUserReq
err = c.BindAndValidate(&req)
if err != nil {
c.String(400, err.Error())
return
}

resp := new(user.DeleteUserResp)
_, err = query.User.WithContext(ctx).Where(query.User.ID.Eq(req.UserId)).Delete()
if err != nil {
resp.Code = user.Code_DBErr
resp.Msg = err.Error()
c.JSON(200, resp)
return
}

resp.Code = user.Code_Success
resp.Msg = “Delete data successfully”
c.JSON(200, resp)
}
参考文献:
无恒实验室联合GORM推出安全好用的ORM框架-GEN:https://mp.weixin.qq.com/s/SfLIkU8E2b3sAO1qSUkyXA
Gen 指南:https://gorm.io/gen/index.html
GEN 项目地址:https://github.com/go-gorm/gen

声明:文中观点不代表本站立场。本文传送门:https://eyangzhen.com/423617.html

联系我们
联系我们
分享本页
返回顶部