簡介
gorm 是 go orm 實現之一,這篇文章將以 mysql 為例,帶你體驗 gorm 80%+ 的內容。
安裝
go get -u gorm.io/gorm
go get -u gorm.io/driver/mysql
連接池
db, err := gorm.Open(mysql.Open("root:root@tcp(localhost:3306)/demo?parseTime=true&loc=Asia%2FShanghai"), &gorm.Config{})
if err != nil {
log.Fatal(err)
}
sqlDb, _ := db.DB()
sqlDb.SetMaxOpenConns(5)
sqlDb.SetMaxIdleConns(2)
sqlDb.SetConnMaxIdleTime(time.Minute)
這樣就初始化了一個最大連接數為5,最大空閑連接數為2,最大空閑時間為1分鐘的連接池。后續直接使用 db 操作數據庫即可。
AutoMigrate
gorm 使用結構體標識 table ,即一個 struct 對應數據庫里的一個 table 。
type BaseModel struct {
ID uint `gorm:"primary_key" json:"id"`
CreatedAt JsonTime `json:"created_at"`
UpdatedAt JsonTime `json:"updated_at"`
DeletedAt gorm.DeletedAt `sql:"index" json:"-"`
}
type User struct {
BaseModel
Username string `json:"username"`
Password string `json:"password"`
Avatar string `json:"avatar"`
Age *int
}
err = db.AutoMigrate(&models.User{})
if err != nil {
log.Fatal(err)
}
JsonTime 是內嵌 time.Time 的自定義類型,主要用于格式化日期,以更好的符合國內使用者的習慣。
type JsonTime struct {
time.Time
}
func (t JsonTime)MarshalJSON()([]byte,error) {
str := fmt.Sprintf("\"%s\"", t.Format("2006-01-02 15:04:05"))
return []byte(str),nil
}
func (t JsonTime)Value()(driver.Value,error) {
var zeroTime time.Time
if t.Time.UnixNano() == zeroTime.UnixNano() {
return nil,nil
}
return t.Time,nil
}
func (t *JsonTime)Scan(v interface{}) error {
value,ok := v.(time.Time)
if ok {
*t = JsonTime{Time: value}
return nil
}
return fmt.Errorf("error %v",v)
}
運行代碼,gorm 就會自動幫你把 table 創建出來,名稱規范遵循 蛇形命名
。
創建記錄
age := 20
user := models.User{Username:"gorm",Password:"",Age:&age}
db.Debug().Create(&user)
fmt.Println(user.ID)
Debug() 將會在終端顯示運行的 sql 語句:
也可以使用 map 創建記錄,但是必須通過 Model() 或者 Table() 指定表名。
user := map[string]interface{}{
"username":"map",
"password":"",
"age":20,
}
db.Debug().Model(&models.User{}).Create(&user)
更新
var user models.User
db.Last(&user)
user.Avatar = "xxxx"
*user.Age += 1
db.Debug().Save(&user)
fmt.Println(user)
只要模型具有 ID 屬性且不為空,Save() 將做全字段更新,可以使用 Select 指定需要更新的字段,只需更新一個字段則使用 UpdateColumn() 更為方便。
使用 struct 更新默認不會更新零值,可以通過 Select 或者使用 map 更新解決。
刪除
tx := db.Debug().Where("name = ?", "jinzhu").Delete(&email)
if tx.Error != nil {
log.Fatal(tx.Error)
}
fmt.Println(tx.RowsAffected)
gorm 默認使用軟刪除,Delete 方法實際是做更新操作。如果強制刪除,則添加 .Unscoped() 方法。
查詢
查詢涉及到的內容就比較多了,gorm 使用鏈式 Api ,跟其他語言的 ORM 使用起來非常類似。
var user models.User
db.Debug().First(&user)
db.Debug().Where("username = ?","purelight").First(&user)
db.Debug().Find(&user,18)
var age int
db.Debug().Select("age").Model(&models.User{}).First(&age)
有兩個比較重要的,一是確定 table ,這個可以通過 Model() ,Table() ,或者通過 Find 等 Finisher
方法的結構體指針參數確定;二是獲取查詢結果,除了直接映射到結構體指針和map指針外,還可以使用 .Rows() 然后去遍歷。
另外由于默認采用了軟刪除,所以 gorm 在查詢是會自動帶上 deleted_at is not null 的條件。
其他常見的 Where,Order,GroupBy,Offset,Limit,Distinct,Join,Count 都是支持的,詳細可查閱具體文檔。
Scope
func AgeOfAdult(db *gorm.DB) *gorm.DB {
return db.Where("age >= ?",18)
}
db.Scopes(AgeOfAdult).First(&user)
很好理解,跟 Laravel 的 scope 相似,用于封裝通用過濾條件。
模型關聯
-
Belongs To
type Pet struct { BaseModel Name *string `json:"name"` Age *int `json:"age"` UserID int `json:"-"` User User `json:"-"` }
pet 屬于 user ,UserID 是外鍵,對應的數據表列名是 user_id ,當然,可以通過 tag 修改默認映射的列名,比如我們數據表列名是 u_id ,只需:
type Pet struct { BaseModel Name *string `json:"name"` Age *int `json:"age"` UserID int `json:"-" gorm:"column:u_id"` User User `json:"-"` }
如果我們的 struct 已經有一個 UID 字段并且就是外鍵,我們可以重寫外鍵:
type Pet struct { BaseModel Name *string `json:"name"` Age *int `json:"age"` UID int User User `json:"-" gorm:"foreignKey:UID"` }
如果不是關聯的 user 的 id ,比如 name ,則可以重寫引用:
type Pet struct { BaseModel Name *string `json:"name"` Age *int `json:"age"` UserID int `json:"-"` User User `json:"-" gorm:"references:Name"` }
查詢使用 Preload() 可以提前加載關聯,可以避免
N+1
的問題。 -
Has One
type User struct { gorm.Model CreditCard CreditCard } type CreditCard struct { gorm.Model Number string UserID uint }
可見,與 Belongs To 相似。
還有種自引用:
type Area struct { BaseModel Name string ParentID *uint Children []Area `gorm:"ForeignKey:ParentID"` }
-
HasMany
type User struct { BaseModel Username string `json:"username"` Password string `json:"password"` Avatar string `json:"avatar"` Age *int CreditCards []CreditCard }
就是將 Has One 的單個模型改成 slice 。
-
多態(適用于 Has One 和 Has Many)
type Cat struct { BaseModel Name string Animal Animal `gorm:"polymorphic:Owner;"` } type Dog struct { BaseModel Name string Animal Animal `gorm:"polymorphic:Owner;"` } type Animal struct { BaseModel Name string OwnerID int OwnerType string } tx := db.Create(&models.Dog{Name:"dog1",Animal:models.Animal{Name:"dog1"}}) fmt.Println(tx.Error) var dog models.Dog tx = db.Debug().Model(models.Dog{}).Preload("Animal").First(&dog) if tx.Error != nil { log.Fatal(tx.Error) } fmt.Println(dog.Animal)
-
Many To Many
type Languages struct { BaseModel Name string Users []User `gorm:"many2many:user_languages"` } type User struct { BaseModel Username string `json:"username"` Languages []Languages `gorm:"many2many:user_languages;"` }
這里會創建中間表 user_languages ,表僅有兩列:user_id 和 language_id 。
雖然關聯模式中默認的列名可以更改,但是建議開發中還是按照框架約定的規范來,不僅看著舒服,代碼還能更簡潔。
Preload() 只是會提前加載關聯關系,如果我們僅僅只想獲取關聯關系怎么辦?這是應使用 Associations :
var user models.User
db.Debug().Find(&user,54)
var langs []models.Languages
db.Debug().Model(&user).Where("name = ?","English").Association("Languages").Find(&langs)
fmt.Println(len(langs))
for _,lang := range langs {
fmt.Println(lang.Name)
}
并且支持多層關聯:
var departments []models.Department
err := db.Debug().Model(&user).Association("Company.Departments").Find(&departments)
if err != nil {
log.Fatal(err)
}
for _,dep := range departments {
fmt.Println(dep.Name)
}
錯誤處理
if err := db.Where("name = ?", "jinzhu").First(&user).Error; err != nil {
// 處理錯誤...
}
主動進行錯誤處理是個好習慣~
Hook
gorm 提供查詢,更新,刪除,創建場景下的 hook ,相當完善。
func (user *User)BeforeDelete(db *gorm.DB)(err error) {
fmt.Println(user.ID,"即將刪除")
return nil
}
func (user *User)BeforeUpdate(db *gorm.DB)(err error) {
fmt.Println(user.ID,"更新")
return nil
}
鏈式操作
建議完成參考文檔 鏈式方法 。
關鍵是要注意協程安全,想要復用 db ,務必確保其處于 ”新建會話模式“ 。
事務
err := db.Debug().Transaction(func(tx *gorm.DB) error {
if err := tx.Create(&models.User{Username:"trans1"}).Error;err != nil {
return err
}
tx.Transaction(func(tx2 *gorm.DB) error {
user := models.User{Username:"trans2"}
user.ID = 55
if err := tx.Create(&user).Error;err != nil {
return err
}
return nil
})
return nil
},nil)
if err != nil {
log.Fatal(err)
}
fmt.Println("事務執行成功")
這是自動模式,return error 自動回滾,否則自動提交。另外還有手動控制提交回滾的方式。
gorm 基于 savepoint 支持嵌套事務。
關于事務,gorm 創建更新刪除操作默認也是在事務里面執行,配置關閉:SkipDefaultTransaction: true 將會提升不少性能。
Migrator
fmt.Println(db.Migrator().HasTable(models.User{}))
Migrator 更為精細化控制 table metadata 。
Logger
f,err := os.OpenFile("sql.log",os.O_APPEND|os.O_RDWR|os.O_CREATE,os.ModePerm)
if err != nil {
log.Fatal(err)
}
defer f.Close()
logger1 := logger.New(log.New(f,"\r\n",log.LstdFlags),logger.Config{
SlowThreshold: time.Second, // 慢 SQL 閾值
LogLevel: logger.Info, // 日志級別
IgnoreRecordNotFoundError: true, // 忽略ErrRecordNotFound(記錄未找到)錯誤
Colorful: false, // 禁用彩色打印
})
var user models.User
var user2 models.User
tx := db.Session(&gorm.Session{Logger:logger1})
tx.First(&user)
tx.Last(&user2)
fmt.Println(user.Username)
fmt.Println(user2.Username)
自定義類型
type Book struct {
ID uint64
CreatedAt JsonTime
UpdatedAt JsonTime
DeletedAt DeletedAt
Name string
Tags StringArray `gorm:"type:varchar(255)"`
}
type StringArray []string
func (sa *StringArray)Scan(value interface{}) error {
tags,ok := value.([]byte)
if !ok {
return errors.New("類型有誤")
}
*sa = strings.Split(string(tags),",")
return nil
}
func (sa StringArray)Value() (driver.Value,error){
if len(sa) == 0 {
return "",nil
}
return strings.Join(sa,","),nil
}
這里將切片類型的 tags 轉成 ,
分隔的文本存入數據庫,讀取的時候再將文本轉成切片使用:
book := models.Book{Name:"ruby",Tags:models.StringArray{"xx","ff"}}
db.Debug().Create(&book)
var b1 models.Book
db.Debug().Where("name = ?","ruby").First(&b1)
fmt.Println(b1.Tags)
dbresolver
可實現讀寫分離,負載均衡。
master1 := "root:root@tcp(localhost:33060)/demo?parseTime=true&loc=Asia%2FShanghai"
replica1 := "root:root@tcp(localhost:33061)/demo?parseTime=true&loc=Asia%2FShanghai"
replica2 := "root:root@tcp(localhost:33062)/demo?parseTime=true&loc=Asia%2FShanghai"
db,err := gorm.Open(mysql.Open(master1),&gorm.Config{})
if err != nil {
log.Fatal(err)
}
db.Use(dbresolver.Register(dbresolver.Config{
Sources:[]gorm.Dialector{mysql.Open(master1)},
Replicas:[]gorm.Dialector{mysql.Open(replica1),mysql.Open(replica2)},
Policy:dbresolver.RandomPolicy{},
}).SetMaxOpenConns(10).SetMaxIdleConns(5).SetConnMaxIdleTime(time.Minute))
db.AutoMigrate(&models.User{})
//age := 10
//u1 := models.User{Username:"scl",Age:&age}
//if err = db.Create(&u1).Error;err != nil {
// log.Fatal(err)
//}
//fmt.Println("u1創建成功")
//var user models.User
//db.Debug().First(&user)
//var rs int
//db.Debug().Raw("select sleep(120);").Scan(&rs)
//fmt.Println(user.Username)
db.Clauses(dbresolver.Write).Debug().Exec("select sleep(300);")
需自行搭配好數據庫的主從集群,使用主從依然存在一個問題,創建場景下剛創建的數據立馬去查從庫,從庫大概率沒這么快同步完成,這時候要去主庫查,其他框架一般有個 Sticky 選項,gorm 這里可以通過 Clause(dbresolver.Write) 指定從主庫讀取。
安全
userInput := "jinzhu;drop table users;"
// 安全的,會被轉義
db.Where("name = ?", userInput).First(&user)
// SQL 注入
db.Where(fmt.Sprintf("name = %v", userInput)).First(&user)
永遠不要相信用戶的輸入。
其它
原生SQL,Context,約束,配置,插件相關內容請查閱 gorm 官方文檔。
總結
麻雀雖小,腑臟俱全。基本該有的都有,畢竟我也只深入了解了 gorm 這一個 orm ,與其他 orm 的對比還請參考搜索引擎 。
2022-01-17