【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

相关推荐
自在的LEE5 小时前
当 Go 遇上 Windows:15.625ms 的时间更新困局
后端·kubernetes·go
Gvto1 天前
使用FakeSMTP创建本地SMTP服务器接收邮件具体实现。
go·smtp·mailtrap
白泽来了2 天前
【Go进阶】手写 Go websocket 库(一)|WebSocket 通信协议
开源·go
witton2 天前
将VSCode配置成Goland的视觉效果
ide·vscode·编辑器·go·字体·c/c++·goland
非凡的世界2 天前
5个用于构建Web应用程序的Go Web框架
golang·go·框架·web
湫qiu2 天前
6.5840 Lab-Key/Value Server 思路
后端·go
我是前端小学生2 天前
Go语言中的init函数
go
我是前端小学生3 天前
Go语言中内部模块的可见性规则
go
我是前端小学生3 天前
一文理解Go Modules的相关内容
go
非凡的世界3 天前
Iris简单实现Go web服务器
golang·go