这是我参与「第五届青训营」伴学笔记创作活动的第 6 天
Go框架三件套详解 —— Gorm
由于直播内容较长,将三件套分别记录
课程介绍
课程目标介绍、三件套(Gorm、Kitex、Hertz)介绍
课程目标
- 将前面所学知识应用到项目中
- 掌握Hertz/Kitex/Gorm的基本用法
- 通过学习实战案例,可以使用三件套完成日常后端开发任务
三件套介绍
Gorm
Gorm是一个已经迭代了10年+的功能强大的ORM框架,在字节内部被广泛使用并且拥有非常丰富的开源扩展。
Kitex
Kitex是字节内部的Golang微服务RPC框架,具有高性能、高可扩展的主要特点,支持多协议并且拥有丰富的开源扩展。
Hertz
Hertz是字节内部的HTTP框架,参考了其他开源框架的优势,结合字节跳动内部的需求,具有高易用、高性能、高扩展性特点。
Gorm的基础使用
Gorm的简单操作
// 定义gorm model
type Product struct {
Code string
Price uint
}
// 为model定义表名
func (p Product) TableName() string {
return "product"
}
// 具体使用
func main() {
// 连接数据库
db, err := gorm.Open{
mysql.Open("user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"),
&gorm.Config{})
if err != nil {
panic("failed to connect database")
}
// 创建数据
db.Create(&Product{Code:"D42", Price: 100})
// 查询数据
var product Product
db.First(&product, 1) // 默认根据整形主键查找
db.First(&product, "code = ?", "D42") // 查找code字段值为D42的记录
// 更新数据
db.Model(&product).Update("Price", 200)
db.Model(&product).Update(Product{Price: 200, Code: "F42"}) // 仅更新非零值字段
db.Model(&product).Update(map[string]interface{}{"Price": 200, "Code": "F42"})
// 删除数据
db.Delete(&product, 1)
}
- gorm model对应一张表,它的字段对应表中的字段。
db.Create可分为创建一条数据或多条数据,区别在于前者传递的是一个对象,后者传递的是一个切片(数组)。db.First只能查询一条数据,且要传结构体指针。gorm还设置了能查询多条数据的Find方法。db.Model方法是为了传递表名。传递表名有两种方式,一种是通过db.Model(&结构体),第二种是调用db.Table("表名")。db.Update在用结构体形式更新时只更新非零值字段,如果我们想更新零值,则用map形式去更新。
Gorm的约定
- Gorm使用名为ID的字段作为主键,当我们在结构体中设置了ID字段,则会自动使用它作为数据库的主键
- 如果我们没有使用
TableName()方法设置表名,则使用结构体的蛇形负数作为表名 - 字段名的蛇形作为列名
- 使用CreatedAt、UpdateAt字段作为创建、更新时间
Gorm支持的数据库
GORM目前支持MySQL、SQLServer、PostgreSQL、SQLite。下面是连接SQLServer数据库的示例
import (
"gorm.io/driver/sqlserver"
"gorm.io/gorm"
)
// github.com/denisenkom/go-mssqldb
dsn := "sqlserver://gorm:LoremIpsum86@localhost:9930/database=gorm"
db, err := gorm.Open(sqlserver.Open(dsn), &gorm.Config{})
GORM是通过驱动的方式连接数据库,如果需要连接其他类型的数据库,可以复用/自行开发驱动。复用是指某些数据库兼容MySQL协议,则使用MySQL驱动即可。DSN(data-source-name),包含连接数据库的信息。
GORM操作
GORM tag
使用column标签为字段命名
type Product struct{
ID uint `gorm:"prinaryley"`
Code string `gorm:"column: code"`
Price uint `gorm:"column: user_id"`
}
使用default标签为字段定义默认值,如:
type User struct {
ID int64
Name string `gorm:"default:galeone"`
Age int64 `gorm:"default:18"`
uuid.UUID UUID `gorm:"type:uuid;default:gen_random_uuid()"` // 数据库函数
}
注意 0、''、false 之类零值,这些字段定义的默认值不会被保存到数据库,您需要使用指针类型或 Scanner/Valuer 来避免这个问题,例如:
type User struct {
gorm.Model
Name string
Age *int `gorm:"default:18"`
Active sql.NullBool `gorm:"default:true"`
}
创建数据
如果遇到唯一索引冲突,使用clause.OnConflict处理数据冲突
// 以不处理冲突为例,创建一条数据
p := &Product{Code: "D42", ID: 1}
db.Clauses{clause.OnConflict{DoNothing: true}).Create(&p)
像调用.Create()、.Update()、.Delete()这些动词的finishAPI才是真正执行SQL语句的,而这些API前面加的API例如.Clauses()是组合API,组合API是来拼接SQL语句的。如果我们在.Create()后面加一个.Where()是不生效的,因为前者已经执行了SQL语句。
查询数据
First的使用踩坑
使用First时,需要注意查询不到数据会返回ErrRecordFound。而使用Find查询多条数据,查询不到的话返回空数组而不返回报错。
使用结构体作为查询条件
当使用结构体作为条件查询,GORM只会查询非零值字段。这意味着如果您的字段值为0、"、false或其他零值,该字段不会被用于构建查询条件,使用Map来构建查询条件。
更新数据
使用struct更新时,只会更新非零值,如果需要更新零值可以使用Map更新或使用Select选择字段。
// 条件更新单个列
db.Model(&User{ID: 111}).Where("age > ?", 18).Update("name", "hello")
// 根据struct更新属性,只会更新非零值的字段
db.Model(&User{ID: 111}).Updates(User{Name: "hello", Age: 18})
// 根据map更新属性
db.Model(&User{ID: 111}).Updates(map[string]interface{}{"name": "hello", "age": 18, "actived": false})
// 更新选定字段
// 下面的map中虽然有多个键值对,但因为select已经选定name,所以只会更新name
db.Model(&User{ID: 111}).Select("name").Updates(map[string]interface{}{"name":"hello", "age":18, "actived": false})
// SQL表达式更新
// UPDATE "products" SET "price" = price * 2 +100
db.Model(&User{ID: 111}).Update("age", gorm.Expr("age * ? + ?", 2, 100))
删除数据
物理删除
db.Delete(&User{}, 10) // DELETE FROM users WHERE id=10;
db.Delete(&User{}, "10") // DELETE FROM users WHERE id = 10;
db.Delete(&User{}, []int{1, 2, 3}) // DELETE FROM users WHERE id IN (1,2,3);
db.Where("name LIKE ?", "%jinzhu%").Delete(User{}) // DELETE FROM users WHERE name LIKE "%jinzhu%";
db.Delete(User{}, "name LIKE ?", "%jinzhu%") // DELETE FORM users WHERE name LIKE "%jinzhu%";
软删除
GORM提供了gorm.DeletedAt用于帮助用户实现软删除,拥有软删除能力的Model调用Delete时,记录不会被从数据库中真正删除。但GORM会将DeleteAt置为当前时间,并且你不能再通过正常的查询方法找到该记录。
使用Unscoped可以查询到被软删除的数据
GORM事务
Gorm提供了Begin、Commit、Rollback方法用于使用事务
db, err := gorm.Open(mysql.Open("username:password@tcp(localhost:9910)/database?charset=utf8"), &gorm.Config{})
if err != nil {
panic("failed to connect database")
}
if err = db.Transaction(func(tx *gorm.DB) error {
if err = tx.Create(&User{Name:
一定要注意开启事务后用tx而不是db!!!
Gorm提供了Tansaction方法用于自动提交事务,避免用户漏写Commit、Rollback。(推荐使用这种)
db, err := gorm.Open(mysql.Open("username:password@tcp(localhost:9910)/database?charset=utf8"), &gorm.Config{})
if err != nil {
panic("failed to connect database")
}
if err = db.Transaction(func(tx *gorm.DB) error {
if err = tx.Create(&User{Name: "name"}).Error; err != nil {
return err
}
if err = tx.Create(&User{Name: "name1"}).Error; err != nil {
tx.Rollback()
return err
}
return nil
}); err != nil {
return
}
GORM Hook
GORM提供了CURD的Hook能力。Hook是在创建、查询、更新、删除等操作之前、之后自动调用的函数。Hook操作会自动添加默认事务。
如果任何Hook返回错误,GORM将停止后续的操作并回滚事务。
type User struct {
ID int64
Name string `gorm:"default:galeone"`
Age int64 `gorm:"default:18"`
}
type Email struct {
ID int64
Name string
Email string
}
func (u *User) BeforeCreate(tx *gorm.DB) (err error) {
if u.Age < 0 {
return errors.New("can't save invalid data")
}
return
}
func (u *User) AfterCreate(tx *gorm.DB) (err error) {
return tx.Create(&Email{ID: u.ID, Email: u.Name + "@***.com"}).Error
}
GORM性能提高
对于写操作(创建、更新、删除),为了确保数据的完整性,GORM会将它们封装在事务内运行。但这会降低性能,你可以使用SkipDefaultTransaction关闭默认事务。使用PrepateStmt缓存预编译语句可以提高后续调用的速度,本机测试提高大约35%左右。
db, err := gorm.Open(mysql.Open("username:password@tcp(localhost:9910)/database?charset=utf8"), &gorm.Config{
SkipDefaultTransaction: true, // 关闭默认事务
PrepareStmt: true, // 缓存预编译语句
})
GORM生态
总结
对于链式调用的ORM来说,以result := db.Where("age > 18").Find(&users)为例,需要注意的是像where这是API都是拼接SQL语句的,而只有当我们调用First、Find、Create这种API才会真正执行SQL,不能将前者放到后者之后调用。