GEN 自动生成 GORM 模型结构体文件

背景

GEN 是一个基于 GORM 的安全 ORM 框架

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

示例代码

在项目的指定目录,新建一个名为data的文件夹

data新建一个目录gen,在里面创建gen.go文件,内容如下

package main

import (
    "strings"

    "gorm.io/gorm/schema"

    "gorm.io/driver/mysql"
    "gorm.io/gen"
    "gorm.io/gorm"
)

// 更新会覆盖原有文件,所以通过 g.GenerateModel("oss", fieldOpts...) 指定需要更新的表,不要全部覆盖

const DBDSN = "root:123456@(127.0.0.1:3306)/workbench?charset=utf8mb4&parseTime=True&loc=Local"

func main() {
    cfg := gen.Config{
        OutPath: "../dal",                                                           // 输出的文件夹
        Mode:    gen.WithoutContext | gen.WithDefaultQuery | gen.WithQueryInterface, // generate mode
        // 表字段可为 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
    }
    // 处理表名
    cfg.WithTableNameStrategy(func(tableName string) (targetTableName string) {
        // 需要忽略的表
        if strings.EqualFold(tableName, "pay_credit") ||
            strings.EqualFold(tableName, "pay_log") ||
            strings.EqualFold(tableName, "space") ||
            strings.EqualFold(tableName, "space_account") {
            return ""
        }
        return tableName
    })
    // 处理 model名
    cfg.WithModelNameStrategy(func(tableName string) (targetTableName string) {
        s := tableName
        if strings.HasPrefix(tableName, "conf_") {
            s = strings.TrimPrefix(tableName, "conf_")
        }
        ns := schema.NamingStrategy{}
        return ns.SchemaName(s)
    })
    // 处理文件名
    cfg.WithFileNameStrategy(func(tableName string) (targetTableName string) {
        if strings.HasPrefix(tableName, "conf_") {
            return strings.TrimPrefix(tableName, "conf_")
        }
        return tableName
    })
    g := gen.NewGenerator(cfg)

    gormdb, _ := gorm.Open(mysql.Open(DBDSN))

    g.UseDB(gormdb) // reuse your gorm db

    // 自定义字段的数据类型
    // 统一数字类型为int64,兼容protobuf
    dataMap := map[string]func(detailType gorm.ColumnType) (dataType string){
        "tinyint":   func(detailType gorm.ColumnType) (dataType string) { return "uint8" },
        "smallint":  func(detailType gorm.ColumnType) (dataType string) { return "int32" },
        "mediumint": func(detailType gorm.ColumnType) (dataType string) { return "int32" },
        "bigint": func(detailType gorm.ColumnType) (dataType string) {
            if detailType.Name() == "size" {
                return "uint64"
            }
            return "uint32"
        },
        "int": func(detailType gorm.ColumnType) (dataType string) { return "uint32" },
    }
    // 要先于`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
    })
    delField := gen.FieldType("deleted_at", "time.Time") // 不生成 默认的类型
    //sizeField := gen.FieldType("size", "uint64")         // 不生成 默认的类型
    // 将非默认字段名的字段定义为自动时间戳和软删除字段;
    // 自动时间戳默认字段名为:`updated_at`、`created_at, 表字段数据类型为: INT 或 DATETIME
    // 软删除默认字段名为:`deleted_at`, 表字段数据类型为: DATETIME
    //autoUpdateTimeField := 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, delField}

    // 创建模型的结构体,生成文件在 model 目录; 先创建的结果会被后面创建的覆盖
    // 这里创建个别模型仅仅是为了拿到`*generate.QueryStructMeta`类型对象用于后面的模型关联操作中
    // Address := g.GenerateModel("address")

    // 创建 全部模型文件 , 并覆盖前面创建的同名模型
    allModel := g.GenerateAllTable(fieldOpts...)

    // 指定特定的表名
    //models := []interface{}{
    //  g.GenerateModel("workspace_subscribe_log", fieldOpts...),
    //  g.GenerateModel("workspace_bill_log", fieldOpts...),
    //  g.GenerateModel("workspace_version", fieldOpts...),
    //  g.GenerateModel("storage_usage", fieldOpts...),
    //  g.GenerateModel("inbox", fieldOpts...),
    //}

    // 创建模型的方法,生成文件在 query 目录; 先创建结果不会被后创建的覆盖
    g.ApplyBasic()
    g.ApplyBasic(allModel...)

    g.Execute()
}

你只需要替换两处,DBDSN OutPath

运行程序 go run gen.go

……
……
2021/11/29 17:41:45 generate query file: D:\site\my-kratos\internal\data\dal\account.gen.go
2021/11/29 17:41:45 generate query file: D:\site\my-kratos\internal\data\dal\user.gen.go
2021/11/29 17:41:45 generate query file: D:\site\my-kratos\internal\data\dal\gen.go
2021/11/29 17:41:45 Generate code done.

此时,data目录下将多出两个文件夹dal model,里面包含了每张表的映射操作关系

目录结构为

├─dal
├─gen
└─model

注意,这两个目录为只读,里面封装了绝大多数场景能用到的增删改查和枚举值

使用如下,例如在业务逻辑层

query := dal.Use(dbCtx) // dbCtx 为 gorm 句柄
accounts := make([]uint32, 0)
err = query.Account.Select(query.Account.ID).Where(query.Account.UserID.Eq(userID)).Scan(&accounts)

报错指南

在执行go run gen.go出现报错
E:\gopath\pkg\mod\gorm.io\plugin\dbresolver@v1.5.0\dbresolver.go:147:5: unknown field PreparedSQL in struct literal of type gorm.PreparedStmtDB
此为,gorm.io/gormgorm.io/plugin/dbresolver版本没对应上,官方好像还没修复
源码文件dbresolver.go多出来个字段PreparedSQL

我这里的版本为

gorm.io/gen v0.3.26
gorm.io/gorm v1.25.11
gorm.io/plugin/dbresolver v1.5.0

这篇文章讲的很详细 https://www.cnblogs.com/jeffid/articles/16701279.html

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,193评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,306评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,130评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,110评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,118评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,085评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,007评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,844评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,283评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,508评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,667评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,395评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,985评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,630评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,797评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,653评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,553评论 2 352

推荐阅读更多精彩内容