这是我参与「第五届青训营 」伴学笔记创作活动的第 12 天
前言
本文主要内容为Go
语言ORM
框架Gorm
的使用。
Gorm
是Go
语言目前比较热门的数据库ORM
操作库,对开发者也比较友好,使用非常简单,使用上主要就是把struct
类型和数据库表记录进行映射,操作数据库的时候不需要直接手写SQL
代码。
迄今为止,Gorm
是一个已经迭代了10
年+的功能强大的ORM
框架,在字节内部被广泛使用并且拥有非常丰富的开源扩展。
Gorm
目前支持数据库有MySQL
、SQLServer
、PostgreSQL
、SQLite
。
基础使用
下面将以使用Gorm
连接MySQL
为例,看下Gorm
的使用。
Gorm约定
Gorm
在使用时有以下约定:
Gorm
使用名为ID
的字段作为主键- 如果没有
TableName
函数,使用结构体的蛇形复数作为表名 - 字段名的蛇形作为列表
- 使用
CreatedAt
、UpdatedAt
字段作为创建更新时间
安装依赖
需要两个依赖:MySQL
和Gorm
。
// 安装MySQL依赖
go get -u gorm.io/driver/mysql
// 安装Gorm依赖
go get -u gorm.io/gorm
在实际执行的时候,安装MySQL
后,会自动安装Gorm
。
使用方法
Gorm
操作MySQL
的步骤如下:
- 先用
struct
定义一个模型,字段与数据库的字段一致 - 使用
Gorm
连接数据库 - 使用
Gorm
操作数据库
既然是操作数据库,那么就需要有数据库表,下面以product
表为例,先看下表结构
CREATE TABLE `product` (
`ID` int(11) NOT NULL AUTO_INCREMENT,
`code` varchar(50) DEFAULT NULL,
`price` int(11) DEFAULT NULL,
PRIMARY KEY (`ID`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;
导入连接MySQ
L的依赖包
import (
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
创建模型
创建一个Product
类型的结构体,结构体里的字段和MySQL
的product
表字段一一对应。
type Product struct {
ID unit
Code string
Price uint
}
struct
的字段和MySQL
表里的字段是一样对应的,包括大小写,如果MySQL
的字段是小写的话,比如code
字段不一致(如数据中叫product_code
),那么在结构体里需要加上关系对应,对应的写法为:
Code string `gorm:"column:product_code"`
如果是主键的话可以写成:
ID string `gorm:"primarykey"`
通过使用default标签为字段定义默认值:
Name string `gorm:"default:galeone"`
创建表名对应关系,刚才创建的结构体Product
和MySQL
里的表名并不一致,所以要创建一个对应关系,创建方式为为Product
结构体增加一个TableName
方法
func (p Product) TableName() string {
return "product"
}
连接数据库
打开数据库连接,通过mysql.Open
函数打开连接,该函数需要传递一个dsn
,dsn
是连接数据库的一个连接串,相当于Java
中的JDBC
连接串。
dsn
的格式为:{username}:{password}@tcp({host}:{port})/{dbname}?charset=utf8&parseTime=True&loc=Local
具体创建数据库连接的代码为:
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8&parseTime=True&loc=Local", username, password, host, port, dbname)
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
panic("failed to connect database, error=" + err.Error())
}
创建数据
通过Create
函数创建数据,先初始化一个结构体,然后调用Create
函数。
p := Product{Code: "D42", Price: 100}
if err := db.Create(&p).Error; err != nil {
fmt.Println("插入失败", err)
}
Create
函数还支持创建多条数据,如果是多条的话,需要传一个数组参数。
products := []*Product{{Code: "D42", Price: 100},{Code: "D43", Price: 200}}
if err := db.Create(&products).Error; err != nil {
fmt.Println("插入失败", err)
}
如果插入数据的时候,出现了主键冲突怎么办?可以使用OnConflict
处理冲突。
p := Product{Code: "D42", ID: 1}
if err := db.Clauses(clause.OnConflict{DoNothing: true}).Create(&p).Error; err != nil {
fmt.Println("插入失败", err)
}
DoNothing
为true
代表如果冲突的话,什么也不做。
查询数据
Gorm
通过First
函数来进行数据的查询,如果First
函数的第二个参数是个整形的话,会默认按主键进行查询,如果是字符串,则会直接拼接在where
后面。
var product Product
db.First(&product, 1)
这段代码生成的SQL
如下:
SELECT * FROM `product` WHERE `product`.`code` = 1 ORDER BY `product`.`code` LIMIT 1
如果要按照指定的条件查询的话,则需要传条件参数:
var product Product
db.First(&product, "Code = ?", "D42")
或者使用Where
函数:
db.Where("Code = ?", "D42").First(&product)
这两种方法的功能是一样的,生成的SQL
如下:
SELECT * FROM `product` WHERE Code = 'D42' ORDER BY `product`.`code` LIMIT 1
First
函数只能查询一条记录,如果要查多条的话,需要使用Find
函数。
products := make([]Product, 0)
db.Find(&products, "price = ?", 2)
使用IN
查询:
products := make([]*Product, 0)
db.Where("price IN ?", []uint{100, 2}).Find(&products)
使用like
查询:
products := make([]*Product, 0)
db.Where("code like ?", "%D%").Find(&products)
使用结构体查询:
db.Where(&Product{Code: "D43", Price: 0}).Find(&products)
生成的SQL为:
SELECT * FROM `product` WHERE `product`.`code` = 'D43'
注意:
- 使用
First
查询时,如果查询不到数据会返回ErrRecordNotFound
- 使用
Find
查询时,查询不到数据不会返回错误 - 使用结构体作为查询条件时,
Gorm
只会查询非零值字段,也就是0
、''
、false
或其他零值字段将被忽略,可以使用Map
或Select
来替换
更新数据
Gorm
更新数据是通过Update
函数操作的,Update
函数需要传入要更新的字段和对应的值。
需要通过Model
函数来传入要更新的模型,主要是用来确定表名,也可以使用Table
函数来确定表名。
db.Model(&Product{}).Update("Price", 200)
如果要更新多个字段的话,可以使用Updates
函数,该函数需要传入一个结构体或map
:
db.Model(&Product{}).Updates(Product{Code: "D43", Price: 1})
如果使用结构体作为参数的话,可以省略Model
这一部分,因为Gorm
也会从参数的结构体中查询相关表名。
注意在使用结构体时,不会更新零值,如果要更新的话,需要使用map
:
db.Model(&Product{}).Updates(map[string]interface{}{"Price": 200, "Code": "F42"})
如果是按条件更新的话,可以通过Where
函数传入条件:
db.Model(&Product{}).Where("Code = ?", "D42").Update("Price", 200)
更新选定字段,只会更新Select
函数里的字段,其他字段会被忽略:
db.Model(&Product{}).Select("price").Updates(map[string]interface{}{"Price": 200, "Code": "F42"})
表达式更新,如果要实现update product set price = price * 2 * 100
的话,实现方式如下:
db.Model(&Product{}).Update("price", gorm.Expr("price * ? * ?", 2, 100))
删除数据
删除数据又分为物理删除和逻辑删除。
物理删除
删除数据使用的是Delete
函数,Delete
函数需要传入一个结构体以及参数,如果参数是整形的话,会按主键进行删除。
db.Delete(&Product{}, 1)
db.Delete(&Product{}, "Code = ?", "F42")
db.Delete(&Product{}, []int{1,2,3})
db.Where("code like ?", "%F%").Delete(Product{})
如果想按条件删除的话,需要使用Where
函数:
db.Where("Code = ?", "F42").Delete(&Product{})
软删除
Gorm
提供了软删除的能力,需要在结构体中定义一个Deleted
字段,此时再调用Delete
删除函数,则会生成update
语句,并将deleted
字段赋值为当前删除时间。
疑问:下面代码是实际操作的代码,生成的SQL的删除字段是deleted
,但是从网上查的很多资料都是deleted_at
,不知道是不是我哪里写错了,如果有知道的小伙伴,帮忙指正一下吧,多谢。
type Product struct {
ID uint
Code string
Price uint
Deleted gorm.DeletedAt
}
再次执行删除操作:
db.Delete(&Product{}, 1)
生成的sql
为:
UPDATE `product` SET `deleted`='2023-01-30 22:22:22.202' WHERE `product`.`id` = 1 AND `product`.`deleted` IS NULL
查询被软删除的操作,要使用Unscoped
函数:
db.Unscoped().First(&product, 2)
有了DeleteAt
字段后,删除操作已经变成了更新操作,那么想要物理删除怎么办?也是使用Unscoped
函数:
db.Unscoped().Delete(&Product{}, 1)
Gorm事务
Gorm
提供了Begin
、Commit
、Rollback
方法用于使用事务。
// 开启事务
tx := db.Begin()
if err := tx.Create(&Product{Code: "D32", Price: 100}).Error; err != nil {
// 出现错误回滚
tx.Rollback()
return
}
if err := tx.Create(&Product{Code: "D33", Price: 100}).Error; err != nil {
// 出现错误回滚
tx.Rollback()
return
}
// 提交事务
tx.Commit()
注意:在开启事务后,调用增删改操作是应该使用开启事务返回的tx
而不是db
。
Gorm
还提供了Transaction
函数用于自定提交事务,避免用户漏写Commit
、Rollback
。
if err = db.Transaction(func(tx *gorm.DB) error {
if err := db.Create(&Product{Code: "D55", Price: 100}).Error; err != nil {
return err
}
if err := db.Create(&Product{Code: "D56", Price: 100}).Error; err != nil {
return err
}
return nil
}); err != nil {
return
}
这种写法,当出现错误时会自动进行Rollback
,当正常执行时会自动Commit
。
Hook
当我们想在执行增删改查操作前后做一些额外的操作时,可以使用Gorm
提供的Hook
能力。
Hook
是在创建、查询、更新、删除等操作之前、之后自动调用的函数,如果任何Hook
返回错误,Gorm
将停止后续的操作并回滚事务。
Hook
会开启默认事务,所以会带来了一些性能损失。
性能提高
对于写操作(创建、更新、删除),为了确保数据的完整性,Gorm
会将他们封装在事务内运行,但是这样会降低性能,可以使用SkipDefaultTransaction
关闭默认事务。
使用PrepareStmt
缓存预编译语句可以提高后续调用的速度。
db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{
SkipDefaultTransaction: true,
PrepareStmt: true,
})
Gorm生态
Gorm拥有非常丰富的扩展生态,下面列举一些常用的扩展。
- 代码生成工具:github.com/go-gorm/gen
- 分片分库方案:github.com/go-gorm/sha…
- 手动索引:github.com/go-gorm/hin…
- 乐观锁:github.com/go-gorm/opt…
- 读写分离:github.com/go-gorm/dbr…
- OpenTelemetry扩展:github.com/go-gorm/ope…
引用
字节内部视频——Go框架三件套详解(Web/RPC/ORM)