这是我参与【第五届青训营】伴学笔记创作活动的第5天
Gorm
简介
gorm是面向golang语言的一种ORM(持久层)框架,支持多种数据库的接入,例如MySQL,PostgreSQL,SQLite,SQL Server,Clickhouse。此框架的特点,弱化了开发者对于sql语言的掌握程度,使用提供的API进行底层数据库的访问。
约定
- GORM 倾向于约定,而不是配置。
- 默认情况下,GORM 使用 ID 作为主键,使用结构体名的 蛇形复数 作为表名,字段名的 蛇形 作为列名,
- 并使用 CreatedAt、UpdatedAt 字段追踪创建、更新时间
gorm.Model
GORM 定义一个 gorm.Model 结构体,其包括字段 ID、CreatedAt、UpdatedAt、DeletedAt
// gorm.Model 的定义
type Model struct {
ID uint `gorm:"primaryKey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
}
您可以将它嵌入到您的结构体中,以包含这几个字段
连接数据库
package main
import (
"gorm.io/gorm"
"gorm.io/driver/mysql"
)
type Product struct {
gorm.Model
Code string
Price uint
}
func main() {
dsn := "root:123456@tcp(127.0.0.1:3306)/golang_db?charset=utf8mb4&parseTime=True&loc=Local"
if err != nil {
panic("failed to connect database")
}
// 迁移 schema
db.AutoMigrate(&Product{})
// Create
db.Create(&Product{Code: "D42", Price: 100})
// Read
var product Product
db.First(&product, 1) // 根据整形主键查找
db.First(&product, "code = ?", "D42") // 查找 code 字段值为 D42 的记录
// Update - 将 product 的 price 更新为 200
db.Model(&product).Update("Price", 200)
// Update - 更新多个字段
db.Model(&product).Updates(Product{Price: 200, Code: "F42"}) // 仅更新非零值字段
db.Model(&product).Updates(map[string]interface{}{"Price": 200, "Code": "F42"})
// Delete - 删除 product
db.Delete(&product, 1)
}
其中 dsn 中的 user,pass,dbname 分别替换成你自己的数据库连接账号,密码,以及默认连接的哪个数据库。ip,port 则替换成数据库实例的 ip地址与端口号。
基本用法
import (
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
func main() {
// ex: root:rootpass@tcp(127.0.0.1:3306)/mydb?charset=utf8mb4&parseTime=True&loc=Local&timeout=10s
dsn := "user:pass@tcp(ip:port)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{ })
}
进阶用法 - 支持各种高级配置,以及自定义数据库驱动
import (
"my_mysql_driver"
"gorm.io/gorm"
)
func main() {
db, err := gorm.Open(mysql.New(mysql.Config{
DSN: "gorm:gorm@tcp(127.0.0.1:3306)/gorm?charset=utf8&parseTime=True&loc=Local",
DefaultStringSize: 256, // string 类型字段的默认长度
DisableDatetimePrecision: true, // 禁用 datetime 精度,MySQL5.6之前的数据库不支持
DontSupportRenameIndex: true, // 重命名索引时采用删除并新建的方式,MySQL5.7之前的数据库和 MariaDB 不支持重命名索引
DontSupportRenameColumn: true, // 用 `change` 重命名列,MySQL 8 之前的数据库和 MariaDB 不支持重命名列
SkipInitializeWithVersion: false, // 根据当前 MySQL 版本自动配置
DriverName: "my_mysql_driver",
}), &gorm.Config{})
}
连接池
数据库操作都是通过连接去执行的,频繁创建与销毁连接,是需要花费较大代价的,因此一般都采用连接池对连接进行复用。GORM 使用 database/sql 维护连接池
sqlDB, err := db.DB()
// SetMaxIdleConns 设置空闲连接池中连接的最大数量
sqlDB.SetMaxIdleConns(10)
// SetMaxOpenConns 设置打开数据库连接的最大数量。
sqlDB.SetMaxOpenConns(100)
// SetConnMaxLifetime 设置了连接可复用的最大时间。
sqlDB.SetConnMaxLifetime(time.Hour)
模型定义
模型定义就是将数据库中的表结构映射为代码层面的model 例如数据库表 user
CREATE TABLE `sys_user_info` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键id',
`user_id` varchar(32) COLLATE utf8mb4_general_ci NOT NULL COMMENT '用户id',
`user_name` varchar(255) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '用户名',
`user_addr` varchar(255) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '住址',
`user_age` int NOT NULL COMMENT '年龄',
`user_sex` tinyint NOT NULL DEFAULT '0' COMMENT '性别0男1女',
`sys_ctime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`sys_utime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`is_delete` tinyint NOT NULL DEFAULT '0' COMMENT '是否删除',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
对应的model
type BaseModel struct {
Id int64 `gorm:"primary_key"`
SysCtime time.Time `gorm:"autoCreateTime"` //在新增记录时可以自动填充当前时间
SysUtime time.Time `gorm:"autoUpdateTime"` //在新增和更新记录时可以自动填充当前时间
IsDelete int8
}
type SysUserInfo struct {
BaseModel
UserID string
UserName string
UserAddr string
UserAge int16 `gorm:"default:18"` //通过使用default标签为字段定义默认值
UserSex int8
}
func (SysUserInfo) TableName() string {
//实现TableName接口,以达到结构体和表对应,如果不实现该接口,并未设置全局表名禁用复数,gorm会自动扩展表名为sys_user_infos(结构体+s)
return "sys_user_info"
}
SQL操作
新增记录
常规指针创建
user := User{Name: "Jinzhu", Age: 18, Birthday: time.Now()}
result := db.Create(&user) // 通过数据的指针来创建
user.ID // 返回插入数据的主键
result.Error // 返回 error
result.RowsAffected // 返回插入记录的条数
批量创建
// 批量创建
//users = []User{{Name: "zhz1"}, {Name: "zhz2"}, {Name: "zhz3"}}
//db.Create(&users)
users := []*User{{Name: "zhz1"}, {Name: "zhz2"}, {Name: "zhz3"}}
db.Create(users)
// 数量为 100
db.CreateInBatches(users, 100)
for _, user := range users {
print(user.ID)
}
指定某些字段插入
//指定插入某些字段插入
db.Select("Name", "Age", "CreatedAt").Create(&user)
//指定某些字段不插入
db.Omit("Name", "is_delete", "CreatedAt").Create(&user)
//针对,创建时间与更新时间,也可以使用模型定义tag来定义默认值
删除记录
物理删除
一旦执行删除操作,该数据真没有了
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 ?","%jinzh%").Delete(User{}) // DELETE from users where name LIKE "%iinzhu%".
db.Delete(User{},"email LIKE ?","%iinzhu%") // DELETE from users where name LIKE "%jinzhu%".
软删除
GORM 提供了 gorm.DeletedAt 用于帮助用户实现软删
拥有软删除能力的 Model 调用 Delete 时,记录不会被从数据库中真正删除。但 GORM 会将 DeletedAt 置为当前时间并且你不能再通过正常的查询方法找到该记录。使用 Unscoped 可以查询到被软删的数据
type User struct {
ID int64
Name string `gorm:"default:galeone"`
Age int64 `gorm:"default:18"`
Deleted gorm.DeletedAt
}
func main() {
db,err := gorm.0pen(mysql.0pen( dsn: "username:password@tcp(locahost:9910)/database?charset=utf8")
&gorm,Config{})
if err != nil {
panic( v:"failed to connect database")
}
// 删除一条
u:= User{ID: 111} // user 的ID是111
db.Delete(&u)// UPDATE users SET deleted_at="2013-10-29 10:23” WHERE id = 111;
// 批量到除
db.Where("age = ?",20).Delete(&User())// UPDATE users SET deleted-at-"2013-10-29 10:23" WHERE age = 20
users := make([]+User,0)//在查询时会忽略被软别除的记录
db.Where("age = 20").Find(&users) // SELECT + FRON users WHERE oge = 20 AND deleted_at IS NULL;
//在查询时不会忽路被软删除的记录
db.Unscoped().Where("age = 20").Find(&users) // SELECT * FROM USePS WHERE age = 20;
}
更新记录
根据主键修改
user := &model.SysUserInfo{}
user.ID = 1
// UPDATE `sys_user_info` SET `sys_utime`='2021-08-08 16:46:15.752',`user_name`='小麻皮',`user_addr`='深圳' WHERE `id` = 1
db.Model(&user).Updates(model.SysUserInfo{UserName: "小麻皮", UserAddr: "深圳"})
更新单列
// 条件更新
db.Model(&User{}).Where("active = ?", true).Update("name", "hello")
// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE active=true;
// User 的 ID 是 `111`
db.Model(&user{ID:111}).Update("name", "hello")
// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE id=111;
// 根据条件和 model 的值进行更新
db.Model(&user).Where("active = ?", true).Update("name", "hello")
// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE id=111 AND active=true;
更新多列
当通过 struct 更新时,GORM 只会更新非零字段。如果您想确保指定字段被更新,你应该使用 Select 更新选定字段,或使用 map 来完成更新操作
// 根据 `struct` 更新属性,只会更新非零值的字段
db.Model(&user).Updates(User{Name: "hello", Age: 18, Active: false})
// UPDATE users SET name='hello', age=18, updated_at = '2013-11-17 21:34:10' WHERE id = 111;
// 根据 `map` 更新属性
db.Model(&user).Updates(map[string]interface{}{"name": "hello", "age": 18, "active": false})
// UPDATE users SET name='hello', age=18, active=false, updated_at='2013-11-17 21:34:10' WHERE id=111;
更新选定字段
如果您想要在更新时选定、忽略某些字段,您可以使用 Select、Omit
// 使用 Map 进行 Select
// User's ID is `111`:
db.Model(&user).Select("name").Updates(map[string]interface{}{"name": "hello", "age": 18, "active": false})
// UPDATE users SET name='hello' WHERE id=111;
db.Model(&user).Omit("name").Updates(map[string]interface{}{"name": "hello", "age": 18, "active": false})
// UPDATE users SET age=18, active=false, updated_at='2013-11-17 21:34:10' WHERE id=111;
// 使用 Struct 进行 Select(会 select 零值的字段)
db.Model(&user).Select("Name", "Age").Updates(User{Name: "new_name", Age: 0})
// UPDATE users SET name='new_name', age=0 WHERE id=111;
// Select 所有字段(查询包括零值字段的所有字段)
db.Model(&user).Select("*").Update(User{Name: "jinzhu", Role: "admin", Age: 0})
// Select 除 Role 外的所有字段(包括零值字段的所有字段)
db.Model(&user).Select("*").Omit("Role").Update(User{Name: "jinzhu", Role: "admin", Age: 0})
// SQL 表达式更新
// UPDATE "products" SET "price" = price * 2 + 100,"updated_at" = '2013-11-17 21:34:10' WHERE "id" = 3
db.Model(&User{ID: 111)).Update("age",gorm.Expr( expr: "age * ? + ?" args....2, 100))
查询记录
- 检索单个对象
GORM 提供了 First、Take、Last 方法,以便从数据库中检索单个对象。需要注意查询不到数据会返回 ErrRecordNotFound。
// 获取第一条记录(主键升序)
db.First(&user)
// SELECT * FROM users ORDER BY id LIMIT 1;
db.First(&user,"code=?","D42") //查找code字段值为42的记录
// 获取一条记录,没有指定排序字段
db.Take(&user)
// SELECT * FROM users LIMIT 1;
// 获取最后一条记录(主键降序)
db.Last(&user)
// SELECT * FROM users ORDER BY id DESC LIMIT 1;
result := db.First(&user)
result.RowsAffected // 返回找到的记录数
result.Error // returns error or nil
// 检查 ErrRecordNotFound 错误
errors.Is(result.Error, gorm.ErrRecordNotFound)
First 和 Last 会根据主键排序,分别查询第一条和最后一条记录。只有在目标 struct 是指针或者通过 db.Model() 指定 model 时,该方法才有效。此外,如果相关 model 没有定义主键,那么将按 model 的第一个字段进行排序。例如:
var user User
var users []User
// 有效,因为目标 struct 是指针
db.First(&user)
// SELECT * FROM `users` ORDER BY `users`.`id` LIMIT 1
// 有效,因为通过 `db.Model()` 指定了 model
result := map[string]interface{}{}
db.Model(&User{}).First(&result)
// SELECT * FROM `users` ORDER BY `users`.`id` LIMIT 1
// 无效
result := map[string]interface{}{}
db.Table("users").First(&result)
// 配合 Take 有效
result := map[string]interface{}{}
db.Table("users").Take(&result)
// 未指定主键,会根据第一个字段排序(即:`Code`)
type Language struct {
Code string
Name string
}
db.First(&Language{})
// SELECT * FROM `languages` ORDER BY `languages`.`code` LIMIT 1
- 检索全部对象
使用 Find 查询多条数据,查询不到数据不会返回错误
// 获取全部记录
result := db.Find(&users)
// SELECT * FROM users;
- 附加条件
var userList []*model.SysUserInfo
//返回的是全部字段 使用 Find 查询多条数据,查询不到数据不会返回错误。
result:=db.Where("user_name = ?", userName).Find(&userList)
//SELECT * FROM sys_user_info WHERE name = ?
// 获取全部匹配的记录
db.Where("name <> ?", "jinzhu").Find(&users)
// SELECT * FROM users WHERE name <> 'jinzhu';
// IN
db.Where("name IN ?", []string{"jinzhu", "jinzhu 2"}).Find(&users)
// SELECT * FROM users WHERE name IN ('jinzhu','jinzhu 2');
// LIKE
db.Where("name LIKE ?", "%jin%").Find(&users)
// SELECT * FROM users WHERE name LIKE '%jin%';
// AND
db.Where("name = ? AND age >= ?", "jinzhu", "22").Find(&users)
// SELECT * FROM users WHERE name = 'jinzhu' AND age >= 22;
scan类似Find都是用于执行查询语句,然后把查询结果赋值给结构体变量,区别在于scan不会从传递进来的结构体变量提取表名。使用 Scan 方法的时候需要我们显示指定数据库的表名。
// 原生 SQL
db.Raw("SELECT * FROM sys_user_info WHERE name = ?", userName).Scan(& userList)
// Struct
db.Where(&User{Name: "jinzhu", Age: 20}).First(&user)
// SELECT * FROM users WHERE name = "jinzhu" AND age = 20 ORDER BY id LIMIT 1;
// Map
db.Where(map[string]interface{}{"name": "jinzhu", "age": 20}).Find(&users)
// SELECT * FROM users WHERE name = "jinzhu" AND age = 20;
// 主键切片条件
db.Where([]int64{20, 21, 22}).Find(&users)
// SELECT * FROM users WHERE id IN (20, 21, 22);
注意 当使用结构作为条件查询时,GORM 只会查询非零值字段。这意味着如果您的字段值为 0、‘’、false 或其他 零值,该字段不会被用于构建查询条件,例如:
db.Where(&User{Name: "jinzhu", Age: 0}).Find(&users)
// SELECT * FROM users WHERE name = "jinzhu";
如果想要包含零值查询条件,你可以使用 map,其会包含所有 key-value 的查询条件,例如:
db.Where(map[string]interface{}{"Name": "jinzhu", "Age": 0}).Find(&users)
// SELECT * FROM users WHERE name = "jinzhu" AND age = 0;
Gorm hook
GORM 在 提供了 CURD 的 Hook 能力 Hook 是在创建、查询、更新、删除等操作之前、之后自动调用的函数。如果任何 Hook 返错误,GORM 将停止后续的操作并回滚事务。
hook只能定义在model上。
假设我们有User表,对应model如下,则可以定义BeforeCreate hook,用于插入数据前的检查。
type User struct {
ID int64
Name string
Age int32
IsAdmin bool
IsValid bool
LoginTime time.Time
}
func (u *User) BeforeCreate(tx *gorm.DB) (err error) {
if u.Age < 10 || u.Name == ""{
return errors.New("invalid Age or Name")
}
return nil
}
能定义的所有hook接口:
//gorm/callbacks/interface.go
type BeforeCreateInterface interface {
BeforeCreate(*gorm.DB) error
}
type AfterCreateInterface interface {
AfterCreate(*gorm.DB) error
}
type BeforeUpdateInterface interface {
BeforeUpdate(*gorm.DB) error
}
type AfterUpdateInterface interface {
AfterUpdate(*gorm.DB) error
}
type BeforeSaveInterface interface {
BeforeSave(*gorm.DB) error
}
type AfterSaveInterface interface {
AfterSave(*gorm.DB) error
}
type BeforeDeleteInterface interface {
BeforeDelete(*gorm.DB) error
}
type AfterDeleteInterface interface {
AfterDelete(*gorm.DB) error
}
type AfterFindInterface interface {
AfterFind(*gorm.DB) error
}
| 方法 | 调用hoook | 触发次数 |
|---|---|---|
| Save | BeforeCreate/AfterCreate/BeforeSave/AfterSave | 一次 |
| Create | BeforeCreate/AfterCreate/BeforeSave/AfterSave | 数组形式插入触发多次,create from map方式不会触发 |
| Update | BeforeUpdate/AfterUpdate/BeforeSave/AfterSave | 一次 |
| Delete | BeforeDelete/AfterDelete | 一次 |
| Find/First/Last/Take | AfterFind | 查出几条数据则触发几次 |
- AfterFind只在Find时可能调多次,因为只有Find可能返回多条数据。
- 在没查出数据时,AfterFind不会触发。
- BeforeSave,AfterSave在Create和Update时也会调用。这意味着,如果你同时定义了BeforeSave和BeforeCreate,那么在执行Create时,两者都会被触发。
Gorm事务
db,err ;= gorm.0pen(
mysql.0pen("username:password@tcp(localhost:9910)/databasecharset=utf8"),&gorm.Config{})
if err != nil {
panic("failed to connect database")
}
tx := db.Begin() // 开始事务
//在事务中执行一些 db 操作 (从这里开始,您应该使用tx而不是db)
if err = tx.Create(&User{Name: "name"}).Error; err != nil {
tx.Rollback()//遇到错误时回滚事务
return
}
if err = tx.Create(&User(Name: "name1"}).Error; err != nil{
tx.Rollback()
return
}
//以上语句都没有问题的话进行提交事务
tx.Commit()
以上写法当遇到情况复杂的时候可能会漏掉写Rollback或Commit,导致数据库链接泄露,因此
Gorm 提供了 Tansaction 方法用于自动提交事务,避免用户漏写 Commit、Rollbcak.
db,err ;= gorm.0pen(
mysql.0pen("username:password@tcp(localhost:9910)/databasecharset=utf8"),&gorm.Config{})
if err != nil {
panic("failed to connect database")
}
if err=db.Transaction(func(tx *gorm.DB) error {
// 在事务中执行一些 db 操作(从这里开始,您应该使用 'tx' 而不是 'db')
if err := tx.Create(&User{Name: "zhz"}).Error; err != nil {
// 返回任何错误都会回滚事务
return err
}
if err := tx.Create(&User{Name: "zhz1"}).Error; err != nil {
return err
}
// 返回 nil时自动提交事务
return nil
});err!=nil{
return
}
禁用默认事务 为了确保数据一致性,GORM 会在事务里执行写入操作(创建、更新、删除)。如果没有这方面的要求,您可以在初始化时禁用它,这将获得大约 30%+ 性能提升。
// 全局禁用
db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{
SkipDefaultTransaction: true, //关闭默认事务
PrepareStmt:true, //缓存预编译
})
// 持续会话模式
tx := db.Session(&Session{SkipDefaultTransaction: true})
tx.First(&user, 1)
tx.Find(&users)
tx.Model(&user).Update("Age", 18)