本文基於 gorm v2 版本html
Go 裏面也不用整什麼單例了,直接用私有全局變量。mysql
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 操做之間互不影響:git
func GetDB(ctx context.Context) *gorm.DB { return globalDB.WithContext(ctx) }
通常測試環境才這麼玩,生產上推薦交給 DBA 處理,應用使用低權限帳號github
gorm 提供 db.AutoMigrate(model)
方法自動建表 。如今咱們想要實現數據庫初始化後執行 AutoMigrate
,而且可配置關閉 AutoMigrate
。sql
項目中通常每一個表一個 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 語句來實現也很是簡單:session
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) } }
在 DAO 層咱們通常會封裝對 model 增刪改查等基本操做。每一個方法都須要 db 做爲參數,因此咱們用面向對象的方式作一下封裝。以下:app
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 後續若是要作鏈路追蹤、超時控制等也很方便擴展。測試
咱們只須要把 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 })
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)) } }