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)
}
参考文献:
-
无恒实验室联合GORM推出安全好用的ORM框架-GEN:mp.weixin.qq.com/s/SfLIkU8E2...
-
Gen 指南:gorm.io/gen/index.h...
-
GEN 项目地址:github.com/go-gorm/gen