关于mysql:在-Go-项目中优雅的使用-gorm-v2

76次阅读

共计 3625 个字符,预计需要花费 10 分钟才能阅读完成。

本文基于 gorm v2 版本

连贯数据库

Go 外面也不必整什么单例了,间接用公有全局变量。

func Connect(cfg *DBConfig) {
    dsn := fmt.Sprintf(
        "%s?charset=utf8&parseTime=True&loc=Local",
        cfg.DSN,
    )
    log.Debugf("db dsn: %s", dsn)
    
    var ormLogger logger.Interface
    if cfg.Debug {ormLogger = logger.Default.LogMode(logger.Info)
    } else {ormLogger = logger.Default}
    
    db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
            Logger: ormLogger,
            NamingStrategy: schema.NamingStrategy{TablePrefix: "tb_", // 表名前缀,`User` 对应的表名是 `tb_users`},
    })
    if err != nil {log.Fatal(err)
    }
    
    globalDB = db
    
    log.Info("db connected success")
}

调用方应用 GetDB 从 globalDB 获取 gorm.DB 进行 CURD。WithContext 理论是调用 db.Session(&Session{Context: ctx}),每次创立新 Session,各 db 操作之间互不影响:

func GetDB(ctx context.Context) *gorm.DB {return globalDB.WithContext(ctx)
}

主动创立数据表

个别测试环境才这么玩,生产上举荐交给 DBA 解决,利用应用低权限账号

gorm 提供 db.AutoMigrate(model) 办法主动建表。当初咱们想要实现数据库初始化后执行 AutoMigrate,并且可配置敞开 AutoMigrate

我的项目中个别每个表一个 go 文件,model 相干的 CURD 都在一个文件中。如果用 init 初始化,则 db 必须在 init 执行前初始化,否则 init 执行时 db 还未初始。应用 init 函数不是一个好的实际,一个包中多个 init 函数的执行程序也是个坑。不必 init 则须要被动去调用每个表的初始化。有没有更好的办法呢?这里能够应用回调函数实现依赖反转,应用 init 注册回调函数,在 db 初始化之后再去执行所有回调函数,达到提早执行的目标。代码如下:

var injectors []func(db *gorm.DB)

// 注册回调
func RegisterInjector(f func(*gorm.DB)) {injectors = append(injectors, f)
}

// 执行回调
func callInjector(db *gorm.DB) {
    for _, v := range injectors {v(db)
    }
}

// 主动初始化表构造
func SetupTableModel(db *gorm.DB, model interface{}) {if GetDBConfig().AutoMigrate {err := db.AutoMigrate(model)
        if err != nil {log.Fatal(err)
        }
    }
}
// 调用方
func init() {dbcore.RegisterInjector(func(db *gorm.DB) {dbcore.SetupTableModel(db, &petmodel.Pet{})
    })
}

主动创立数据库

gorm 没有提供主动创立数据库的办法,这个咱们通过 CREATE DATABASE IF NOT EXISTS SQL 语句来实现也非常简单:

func CreateDatabase(cfg *DBConfig) {slashIndex := strings.LastIndex(cfg.DSN, "/")
    dsn := cfg.DSN[:slashIndex+1]
    dbName := cfg.DSN[slashIndex+1:]

    dsn = fmt.Sprintf("%s?charset=utf8&parseTime=True&loc=Local", dsn)
    db, err := gorm.Open(mysql.Open(dsn), nil)
    if err != nil {log.Fatal(err)
    }
    
    createSQL := fmt.Sprintf(
        "CREATE DATABASE IF NOT EXISTS `%s` CHARACTER SET utf8mb4;",
        dbName,
    )

    err = db.Exec(createSQL).Error
    if err != nil {log.Fatal(err)
    }
}

通过 Context 传递事务

在 DAO 层咱们个别会封装对 model 增删改查等基本操作。每个办法都须要 db 作为参数,所以咱们用面向对象的形式做一下封装。如下:

type petDb struct {db *gorm.DB}

func NewPetDb(ctx context.Context) struct {return GetDB(ctx)
}

func (s *petDb) Create(in *petmodel.Pet) error {return s.db.Create(in).Err
}

func (s *petDb) Update(in *petmodel.Pet) error {return s.db.Updates(in).Err
}

事务个别是在 Service 层,如果当初须要将多个 CURD 调用组成事务,如何复用 DAO 层的逻辑?咱们很容易想到将 tx 作为参数传递到 DAO 层办法中即可。

如何优雅的传递 tx 参数?Go 外面没有重载,这种状况有个比拟通用的计划:Context。 应用 Context 后续如果要做链路追踪、超时管制等也很不便扩大

gorm 链路追踪可参考 github 上大佬的实现

咱们只须要把 GetDB 改改,尝试从 ctx 中获取 tx,如果存在则不须要新建 session,间接应用传递的 tx。这个有个小技巧, 应用构造体而不是字符串作为 ctx 的 key,能够保障 key 的唯一性 。代码如下:

func GetDB(ctx context.Context) *gorm.DB {iface := ctx.Value(ctxTransactionKey{})

    if iface != nil {tx, ok := iface.(*gorm.DB)
        if !ok {log.Panicf("unexpect context value type: %s", reflect.TypeOf(tx))
            return nil
        }

        return tx
    }

    return globalDB.WithContext(ctx)
}

在事务上做一下 context 的封装:

func Transaction(ctx context.Context, fc func(txctx context.Context) error) error {db := globalDB.WithContext(ctx)

    return db.Transaction(func(tx *gorm.DB) error {txctx := CtxWithTransaction(ctx, tx)
        return fc(txctx)
    })
}

应用事务:

ownerId := "xxx"
err := Transaction(context.Background(), func(txctx context.Context) error {pet, err := NewPetDb(txctx).Create(&petmodel.Pet{
        Name: "xxx",
        Age:  1,
        Sex:  "female",
    })
    if err != nil {return err}

    _, err = NewOwner_PetDb(txctx).Create(&petmodel.Owner_Pet{
        OwnerId: ownerId,
        PetId:   pet.Id,
    })
    return err
})

Hooks & Callbacks

gorm 提供 Hooks 性能,能够在某个生命周期执行钩子函数,例如创立前生成 uuid:

func (u *Pet) BeforeCreate(tx *gorm.DB) error {u.Id = NewUlid()
    return nil
}

然而 Hooks 是针对某个 model,如果须要对所有 model,能够应用 Callbacks。

func registerCallback(db *gorm.DB) {
    // 主动增加 uuid
    err := db.Callback().Create().Before("gorm:create").Register("uuid", func (db *gorm.DB) {db.Statement.SetColumn("id", NewUlid())
    })
    if err != nil {log.Panicf("err: %+v", errx.WithStackOnce(err))
    }
}

我的项目残缺代码:https://github.com/win5do/go-…

正文完
 0