【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使用文档地址:github.com/go-gorm/gen...

为什么说 GORM Gen 带来最佳用户体验?

  • ⚡️ 自动同步库表,省去繁琐复制
  • 🔗 代码一键生成,专注业务逻辑
  • 🐞 字段类型安全,执行 SQL 也安全
  • 😉 查询优雅返回,完美兼容 GORM

GEN 提供了自动同步数据表结构体到 GORM 模型,使用非常简单,即使数据库字段信息改变,可以一键同步,数据库查询相关代码可以一键生成,CRUD 只需要调用对应的方法,开发体验飞起。

GEN 采用了类型安全限制,所有参数都做了安全限制,完全不用担心存在注入; 最重要的是自定义 SQL 只需要通过模板注释到 interface 的方法上,自动帮助你生成安全的代码,是的,自定义 SQL 也不会出现 SQL 注入问题,而且工具完美兼容 GORM!

使用 GORM 与 GEN 工具的对比

GORM 和 GEN 查询对比案例

go 复制代码
//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:

go 复制代码
├── 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。
bash 复制代码
cd bizdemo/hertz_gorm_gen/cmd
go run generate.go

如何运行

Step 1:使用 Docker 启动 MySQL容器

bash 复制代码
cd bizdemo/hertz_gorm_gen && docker-compose up

由于上一篇项目mysql的端口是 9910,所以这次mysql和项目的有差异,记得修改~~

go 复制代码
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:编译并运行项目

go 复制代码
cd bizdemo/hertz_gorm_gen
go build -o hertz_gorm_gen && ./hertz_gorm_gen

看到下面的日志👇,就说明启动成功啦

bash 复制代码
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 接口调试

根据启动的日志,我们进行各个接口验证

go 复制代码
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

响应:

go 复制代码
{
    "message": "pong"
}

具体的代码是在:bizdemo/hertz_gorm_gen/biz/handler/ping.go

go 复制代码
// Ping .
func Ping(ctx context.Context, c *app.RequestContext) {
	c.JSON(200, utils.H{
		"message": "pong",
	})
}

1.创建用户接口

请求:http://localhost:8888/v1/user/create/

入参

go 复制代码
{
    "name": "hertz_gorm_gen",
    "gender": 1,
    "age": 18,
    "introduce": "牛马打工人"
}

响应:

go 复制代码
{
    "msg": "Create data successfully"
}

去mysql查询

2. 查询用户接口 /v1/user/query/

请求 URL: http://localhost:8888/v1/user/query/

请求参数:

go 复制代码
{
    "page": 1,
    "page_size":10
}

响应示例:

go 复制代码
{
    "user": [
        {
            "user_id": 1,
            "name": "hertz_gorm_gen",
            "gender": 1,
            "age": 18,
            "introduce": "牛马打工人"
        }
    ],
    "total": 10
}

3. 修改用户接口 /v1/user/update/11

请求:http://localhost:8888/v1/user/update/11

请求参数:

go 复制代码
{
    "name": "hertz_gorm_gen",
    "gender": 1,
    "age": 18,
    "introduce": "下班"
}

4. 删除用户接口 /v1/user/delete/1

删除,软删除,不会真正的删除图片

Step 4:代码解析

api.proto

go 复制代码
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

go 复制代码
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

go 复制代码
// 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

ini 复制代码
// 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

go 复制代码
// 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

go 复制代码
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)
}

参考文献:

  1. 无恒实验室联合GORM推出安全好用的ORM框架-GEN:mp.weixin.qq.com/s/SfLIkU8E2...

  2. Gen 指南:gorm.io/gen/index.h...

  3. GEN 项目地址:github.com/go-gorm/gen

相关推荐
一个热爱生活的普通人21 小时前
Go语言中 Mutex 的实现原理
后端·go
孔令飞21 小时前
关于 LLMOPS 的一些粗浅思考
人工智能·云原生·go
小戴同学21 小时前
实时系统降低延时的利器
后端·性能优化·go
Golang菜鸟2 天前
golang中的组合多态
后端·go
Serverless社区2 天前
函数计算支持热门 MCP Server 一键部署
go
Wo3Shi4七2 天前
二叉树数组表示
数据结构·后端·go
网络研究院2 天前
您需要了解的有关 Go、Rust 和 Zig 的信息
开发语言·rust·go·功能·发展·zig
27669582922 天前
拼多多 anti-token unidbg 分析
java·python·go·拼多多·pdd·pxx·anti-token
程序员爱钓鱼3 天前
Go 语言邮件发送完全指南:轻松实现邮件通知功能
后端·go·排序算法
一个热爱生活的普通人3 天前
如何使用 Benchmark 编写高效的性能测试
后端·go