Go框架三件套详解(ORM) | 青训营笔记

103 阅读6分钟

这是我参与「第五届青训营」伴学笔记创作活动的第 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提供了BeginCommitRollback方法用于使用事务

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生态

GORM生态.png

总结

对于链式调用的ORM来说,以result := db.Where("age > 18").Find(&users)为例,需要注意的是像where这是API都是拼接SQL语句的,而只有当我们调用FirstFindCreate这种API才会真正执行SQL,不能将前者放到后者之后调用。