GEN 自动生成 GORM 模型结构体文件及使用示例

news/2024/9/20 18:35:54

后端 - GEN 自动生成 GORM 模型结构体文件及使用示例 - 个人文章 - SegmentFault 思否

背景

GEN 是一个基于 GORM 的安全 ORM 框架, 由字节跳动无恒实验室与 GORM 作者联合研发,主要功能说白了就是帮助生成数据表对应的模型文件和更安全方便地执行SQL。

直接使用 GORM 与 GEN 工具的对比:

直接使用GORM使用GEN
需手动创建与数据表各列一一对应的结构体 指定表名后自动读取并生成对应结构体
需手动实现具体的 go 代码查询逻辑 描述 SQL 查询逻辑即可,工具自动转换成安全稳定的代码
查询接口十分灵活,但不能保持查询的 SQL 不发生语法错误,
只能通过测试保证部分场景的正常运行
查询接口使用类型安全,编译可通过,查询逻辑即是正常合理的
需人工评经验保证业务不存在安全问题,
一旦出错往往在上线前才能发现,影响上线流程
提供的安全可靠的查询 API,开发时能用的就是安全的

本文目标

GEN 提供的功能是强大而丰富的,本文相当一个上手指引只挑些常见操作进行示例说明:

  • 表字段的整型类型无论大小和有无符号,结构体中统一使用 int64 (为了简化后续使用时的操作)。
  • 个别结构体字段 json 序列化时由数字类型转成字符串类型,如:余额字段 balance 在表中是 DECIMAL 类型,结构体中是 int64,业务需要的 json 字段类型为字符串。
  • 使用非默认字段名实现自动更新、创建时间戳和软删除。
  • 模型关联

示例环境:

  • go 1.18
  • gen v0.3.16 注意:当前GEN的最新版主版本是0, 也就是 API 在未来可能会有较大的变更, 使用时务必注意版本变更问题
  • MySQL 8.0

目标表有3个,分别是 useraddress和 hobbyuser与 address是一对多关系,如下所示:

CREATE TABLE IF NOT EXISTS `user` (`id` int unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',`name` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '用户名',`age` tinyint unsigned NOT NULL DEFAULT '0' COMMENT '年龄',`balance` decimal(11,2) unsigned NOT NULL DEFAULT '0.00' COMMENT '余额',`updated_at` datetime NOT NULL COMMENT '更新时间',`created_at` datetime NOT NULL COMMENT '创建时间',`deleted_at` datetime DEFAULT NULL COMMENT '删除时间',PRIMARY KEY (`id`)) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;CREATE TABLE IF NOT EXISTS `address` (`id` int unsigned NOT NULL AUTO_INCREMENT,`uid` int unsigned NOT NULL,`province` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '',`city` varchar(20) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '',`update_time` int unsigned NOT NULL,`create_time` int unsigned NOT NULL,`delete_time` int unsigned NOT NULL DEFAULT '0',PRIMARY KEY (`id`) USING BTREE,KEY `uid` (`uid`)) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC;CREATE TABLE IF NOT EXISTS `hobby` (`id` int unsigned NOT NULL AUTO_INCREMENT,`name` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '',`updated_at` int unsigned NOT NULL,`created_at` int unsigned NOT NULL,`deleted_at` int unsigned NOT NULL DEFAULT '0',PRIMARY KEY (`id`) USING BTREE) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC;

配置 GEN 并生成模型结构体

创建并初始化项目,再引入 GEN 包

mkdir gormgendemo && cd gormgendemo
go mod init
go get -u gorm.io/gen@v0.3.16

继续创建用于生成模型文件的 ./cmd/generate/main.go文件和用于执行 SQL 示例的 ./main.go文件,目录如下所示:

├── cmd
│   └── generate
│       └── main.go
├── go.mod
├── go.sum
└── main.go

./cmd/generate/main.go代码:

package mainimport ("fmt""strings""gorm.io/driver/mysql""gorm.io/gen""gorm.io/gen/field""gorm.io/gorm"
)const MySQLDSN = "root:123456@(localhost:3306)/gormgendemo?charset=utf8mb4&parseTime=True&loc=Local"func main() {// 连接数据库db, err := gorm.Open(mysql.Open(MySQLDSN))if err != nil {panic(fmt.Errorf("cannot establish db connection: %w", err))}// 生成实例g := gen.NewGenerator(gen.Config{// 相对执行`go run`时的路径, 会自动创建目录OutPath: "dal/query",// WithDefaultQuery 生成默认查询结构体(作为全局变量使用), 即`Q`结构体和其字段(各表模型)// WithoutContext 生成没有context调用限制的代码供查询// WithQueryInterface 生成interface形式的查询代码(可导出), 如`Where()`方法返回的就是一个可导出的接口类型Mode: gen.WithDefaultQuery | gen.WithQueryInterface,// 表字段可为 null 值时, 对应结体字段使用指针类型FieldNullable: true, // generate pointer when field is nullable// 表字段默认值与模型结构体字段零值不一致的字段, 在插入数据时需要赋值该字段值为零值的, 结构体字段须是指针类型才能成功, 即`FieldCoverable:true`配置下生成的结构体字段.// 因为在插入时遇到字段为零值的会被GORM赋予默认值. 如字段`age`表默认值为10, 即使你显式设置为0最后也会被GORM设为10提交.// 如果该字段没有上面提到的插入时赋零值的特殊需要, 则字段为非指针类型使用起来会比较方便.FieldCoverable: false, // generate pointer when field has default value, to fix problem zero value cannot be assign: https://gorm.io/docs/create.html#Default-Values// 模型结构体字段的数字类型的符号表示是否与表字段的一致, `false`指示都用有符号类型FieldSignable: false, // detect integer field's unsigned type, adjust generated data type// 生成 gorm 标签的字段索引属性FieldWithIndexTag: false, // generate with gorm index tag// 生成 gorm 标签的字段类型属性FieldWithTypeTag: true, // generate with gorm column type tag})// 设置目标 dbg.UseDB(db)// 自定义字段的数据类型// 统一数字类型为int64,兼容protobufdataMap := map[string]func(detailType string) (dataType string){"tinyint":   func(detailType string) (dataType string) { return "int64" },"smallint":  func(detailType string) (dataType string) { return "int64" },"mediumint": func(detailType string) (dataType string) { return "int64" },"bigint":    func(detailType string) (dataType string) { return "int64" },"int":       func(detailType string) (dataType string) { return "int64" },}// 要先于`ApplyBasic`执行g.WithDataTypeMap(dataMap)// 自定义模型结体字段的标签// 将特定字段名的 json 标签加上`string`属性,即 MarshalJSON 时该字段由数字类型转成字符串类型jsonField := gen.FieldJSONTagWithNS(func(columnName string) (tagContent string) {toStringField := `balance, `if strings.Contains(toStringField, columnName) {return columnName + ",string"}return columnName})// 将非默认字段名的字段定义为自动时间戳和软删除字段;// 自动时间戳默认字段名为:`updated_at`、`created_at, 表字段数据类型为: INT 或 DATETIME// 软删除默认字段名为:`deleted_at`, 表字段数据类型为: DATETIMEautoUpdateTimeField := gen.FieldGORMTag("update_time", "column:update_time;type:int unsigned;autoUpdateTime")autoCreateTimeField := gen.FieldGORMTag("create_time", "column:create_time;type:int unsigned;autoCreateTime")softDeleteField := gen.FieldType("delete_time", "soft_delete.DeletedAt")// 模型自定义选项组fieldOpts := []gen.ModelOpt{jsonField, autoCreateTimeField, autoUpdateTimeField, softDeleteField}// 创建模型的结构体,生成文件在 model 目录; 先创建的结果会被后面创建的覆盖// 这里创建个别模型仅仅是为了拿到`*generate.QueryStructMeta`类型对象用于后面的模型关联操作中Address := g.GenerateModel("address")// 创建全部模型文件, 并覆盖前面创建的同名模型allModel := g.GenerateAllTable(fieldOpts...)// 创建有关联关系的模型文件User := g.GenerateModel("user",append(fieldOpts,// user 一对多 address 关联, 外键`uid`在 address 表中gen.FieldRelate(field.HasMany, "Address", Address, &field.RelateConfig{GORMTag: "foreignKey:UID"}),)...,)Address = g.GenerateModel("address",append(fieldOpts,gen.FieldRelate(field.BelongsTo, "User", User, &field.RelateConfig{GORMTag: "foreignKey:UID"}),)...,)// 创建模型的方法,生成文件在 query 目录; 先创建结果不会被后创建的覆盖g.ApplyBasic(User, Address)g.ApplyBasic(allModel...)g.Execute()
}

执行生成程序:

go mod tidy
go run ./cmd/generate/main.go

结果目录:

.
├── cmd
│   └── generate
│       └── main.go
├── dal
│   ├── model
│   │   ├── address.gen.go
│   │   ├── hobby.gen.go
│   │   └── user.gen.go
│   └── query
│       ├── address.gen.go
│       ├── gen.go
│       ├── hobby.gen.go
│       └── user.gen.go
├── go.mod
├── go.sum
└── main.go

生成的模型结构体:

// User mapped from table <user>
type User struct {ID        int64          `gorm:"column:id;type:int unsigned;primaryKey;autoIncrement:true" json:"id"`                    // IDName      string         `gorm:"column:name;type:varchar(20);not null" json:"name"`                                      // 用户名Age       int64          `gorm:"column:age;type:tinyint unsigned;not null" json:"age"`                                   // 年龄Balance   float64        `gorm:"column:balance;type:decimal(11,2) unsigned;not null;default:0.00" json:"balance,string"` // 余额UpdatedAt time.Time      `gorm:"column:updated_at;type:datetime;not null" json:"updated_at"`                             // 更新时间CreatedAt time.Time      `gorm:"column:created_at;type:datetime;not null" json:"created_at"`                             // 创建时间DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;type:datetime" json:"deleted_at"`                                      // 删除时间Address   []Address      `gorm:"foreignKey:UID" json:"address"`
}// Address mapped from table <address>
type Address struct {ID         int64                 `gorm:"column:id;type:int unsigned;primaryKey;autoIncrement:true" json:"id"`UID        int64                 `gorm:"column:uid;type:int unsigned;not null" json:"uid"`Province   string                `gorm:"column:province;type:varchar(20);not null" json:"province"`City       string                `gorm:"column:city;type:varchar(20);not null" json:"city"`UpdateTime int64                 `gorm:"column:update_time;type:int unsigned;autoUpdateTime" json:"update_time"`CreateTime int64                 `gorm:"column:create_time;type:int unsigned;autoCreateTime" json:"create_time"`DeleteTime soft_delete.DeletedAt `gorm:"column:delete_time;type:int unsigned;not null" json:"delete_time"`User       User                  `gorm:"foreignKey:UID" json:"user"`
}// Hobby mapped from table <hobby>
type Hobby struct {ID        int64  `gorm:"column:id;type:int unsigned;primaryKey;autoIncrement:true" json:"id"`Name      string `gorm:"column:name;type:varchar(20);not null" json:"name"`UpdatedAt int64  `gorm:"column:updated_at;type:int unsigned;not null" json:"updated_at"`CreatedAt int64  `gorm:"column:created_at;type:int unsigned;not null" json:"created_at"`DeletedAt int64  `gorm:"column:deleted_at;type:int unsigned;not null" json:"deleted_at"`
}

上面3个模型都实现了更新、创建字段的自动时间戳功能,前面2个实现了软删除功能,表字段的整型类型都以 int64表示。

使用 GEN 进行数据库操作

./main.go代码:

package mainimport ("context""fmt""log""gormgendemo/dal/model""gormgendemo/dal/query""gorm.io/driver/mysql""gorm.io/gorm"
)const MySQLDSN = "root:123456@(localhost:3306)/gormgendemo?charset=utf8mb4&parseTime=True&loc=Local"func main() {// 连接数据库db, err := gorm.Open(mysql.Open(MySQLDSN))if err != nil {panic(fmt.Errorf("cannot establish db connection: %w", err))}query.SetDefault(db)q := query.Qctx := context.Background()//qc := q.WithContext(ctx)// 增insert(ctx, q)// 删del(ctx, q)// 改update(ctx, q)// 查find(ctx, q)fmt.Println("Done!")
}func insert(ctx context.Context, q *query.Query) {qc := q.WithContext(ctx)// 插入数据users := []*model.User{{Name: "张三",Age:  30,Address: []model.Address{{Province: "广东", City: "广州"},{Province: "广东", City: "深圳"},},},{Name: "李四",Age:  40,Address: []model.Address{{Province: "广东", City: "广州"},{Province: "广东", City: "深圳"},},},{Name: "王五",Age:  50,},}hobby := []*model.Hobby{{Name: "看电影"}, {Name: "看书"}, {Name: "跑步"},}err := qc.User.Create(users...)if err != nil {log.Fatal(err)}err = qc.Hobby.Create(hobby...)if err != nil {log.Fatal(err)}
}func del(ctx context.Context, q *query.Query) {qc := q.WithContext(ctx)qc.User.Where(q.User.Name.Eq("张三")).Delete()       // 软删qc.Address.Where(q.Address.City.Eq("深圳")).Delete() // 软删qc.Hobby.Where(q.Hobby.Name.Eq("看电影")).Delete()    // 非软删
}func update(ctx context.Context, q *query.Query) {qc := q.WithContext(ctx)qc.User.Where(q.User.Name.Eq("李四")).UpdateSimple(q.User.Age.Add(1))
}func find(ctx context.Context, q *query.Query) {qc := q.WithContext(ctx)// First、Take、Last 方法, 如果没有找到记录则返回错误 ErrRecordNotFound。_, err := qc.User.Preload(q.User.Address).Where(q.User.Name.Eq("张三")).First()if err != nil {fmt.Printf("%+v \n", err)//record not found}user, err := qc.User.Preload(q.User.Address).Where(q.User.Name.Eq("李四")).First()if err == nil {fmt.Printf("%+v \n", *user)//{ID:2 Name:李四 Age:41 Balance:0 UpdatedAt:2022-09-16 15:13:39 +0800 CST CreatedAt:2022-09-16 15:13:39 +0800 CST//DeletedAt:{Time:0001-01-01 00:00:00 +0000 UTC Valid:false}//Address:[{ID:3 UID:2rovince:广东 City:广州 UpdateTime:1663312418 CreateTime:1663312418 DeleteTime:0//User:{ID:0 Name: Age:0 Balance:0 UpdatedAt:0001-01-01 00:00:00 +0000 UTC CreatedAt:0001-01-01 00:00:00 +0000 UTC//CreatedAt:{Time:0001-01-01 00:00:00 +0000 UTC Valid:false} Address:[]}}]}}addr, err := qc.Address.Preload(q.Address.User).Offset(1).First()if err == nil {fmt.Printf("%+v \n", *addr)//{ID:3 UID:2 Province:广东 City:广州 UpdateTime:1663312418 CreateTime:1663312418 DeleteTime:0//User:{ID:2 Name:李四 Age:41 Balance:0 UpdatedAt:2022-09-16 15:13:39 +0800 CST CreatedAt:2022-09-16 39 +0800 CST//DeletedAt:{Time:0001-01-01 00:00:00 +0000 UTC Valid:false} Address:[]}}}_, err = qc.Hobby.Where(q.Hobby.Name.Eq("看电影")).First()if err != nil {fmt.Printf("%+v \n", err)//record not found}hb, err := qc.Hobby.Where(q.Hobby.Name.Eq("看书")).First()if err == nil {fmt.Printf("%+v \n", *hb)//{ID:2 Name:看书 UpdatedAt:1663312418 CreatedAt:1663312418 DeletedAt:0}}}

执行使用程序可以看到效果

go run main.go
 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.ryyt.cn/news/62640.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈,一经查实,立即删除!

相关文章

0920

线代 舒尔公式,化上三角,下三角,对角阵 范德蒙德行列式 X型行列式,{主对角中下标之和为(2k+1)的两项乘积-副对角中下标之和为(2k+1)的两项乘积【需与前面两项下标号相同】}的连乘 宽对角,a2=4bc,a2≠4bc计组 MAR位数说明存储单元位数 MDR位数说明字长 编译器:将高级语…

米尔STM32MP2核心板首发新品上市!高性能+多接口+边缘算力

米尔发布基于STM32MP257设计的嵌入式处理器模块MYC-LD25X核心板及开发板。核心板基于STM32MP2系列是意法半导体推出最新一代工业级64位微处理器,采用LGA 252 PIN设计,存储配置1GB/2GB LPDDR4、8GB eMMC,具有丰富的通讯接口,适用于高端工业HMI、边缘计算网关、新能源充电桩、…

Fork+GIT操作

上传分支,在主干,上传完成代码之后,点击新建分支,去一个名字,如1.0.9,双击分支,点击“push”即可将主干所以代码上传到分支上备份一个 合并分支,,双击进入分支,选中要合并的部分,右键点击“Cherry-pick”,再次点击“Push”

影像测试技能

主要分三种职业:3A算法开发、图像tuning,图像测试这三种。 3a算法:AE/AWB/AF的开发,入门难一些,资料很少。 图像tuning:在理解3A算法的基础上,调试3A参数,当前需求量还是比较大的,学习起来也不太难,难在实践积累,多看多调。 1、 AWB auto white balance 自动白平衡 …

Camstar建模表格弹出编辑

大部分都是用行内编辑,但是如果由弹出编辑的需求,也需要了解下。 以工单建模为例子。自己搞个VP。Grid是MaterialList。 此时,表格并不具备,弹出编辑的能力。改下表格的属性,DataSubmissionMode=Changed 添加WebPart编辑框开始修改,每个属性控件的,属性!不然会报错!!…

在 ASP.NET Core Web API 中使用异常筛选器捕获和统一处理异常

异常筛选器可以捕获和处理 ASP.NET Core Web API 中发生的异常,当系统中出现未经处理的异常的时候,异常筛选器就会执行,我们可以在异常筛选器中对异常进行处理,例如记录日志、返回自定义错误信息等。前言 在 ASP.NET Core Web API 中,异常筛选器(Exception Filter)是一…

js实现网页端录音功能

1、代码 首先安装依赖包:recorderxnpm install recorderx -S<template><div class="container"><div class="mt-30"><el-button @click="onStartRecord">开始录音</el-button><el-button @click="onStop…

教你几招,轻松设置Win11右键恢复旧版模样

大家好!今天要教大家如何把 Win11 右键菜单变回完整的展开模式哦!其实很简单,只要在 Windows 开始图标下面的运行里输入一段代码就可以啦!接下来就让我来详细地跟大家分享一下这个方法吧!Win11右键菜单恢复为完整展开模式的方法1、首先,按键盘上的【 Win + X 】组合键,或…