这是我参与「第五届青训营 」伴学笔记创作活动的第3天
今天上课学习了Gorm, Kitex, hertz三个框架, 今天先总结Gorm框架
Gorm简介
Gorm是一个orm框架, 主要用于减少访问数据层的代码量, 实现程序对象到关系数据库的映射关系. 与java中的MyBatis类似.
orm(Object-Relational Mapping), 对象关系映射
简单来说, orm框架让我们避免了冗余的sql语句, 转而以面向对象的方式取编写sql.
与其他orm框架, gorm也有两步必须的操作, 一是定义数据模型, 二是连接数据库
连接数据库
gorm原生支持的数据库共有四种, 分别是mysql, sqlserver, postgresql, sqllite四种
对于其他没有原生支持的数据库, gorm支持驱动复用, 即其数据库连接协议与以上四种相同时, 可以直接服用上述四种sql的连接驱动
gorm连接四种数据库, 主要区别时dsn(data source name)的不同.
以mysql为例, dsn各个部分分别表示为:
[username[:password]@] [protocol[(address)]]/dbname[?param1=value1&...paramN=valueN]
eg:
dsn := "cjs:123456@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
dsn := "cjs:123456@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
db,err := gorm.Open(mysql.Open(dsn),&gorm.Config{})
连接池
- gorm连接数据库采用连接池的方式实现
- 连接池中存放有限的活跃连接, 减少创建连接和关闭连接的时间
- gorm连接池中的连接具有存活时间
gorm的连接池与java线程池的概念很接近, 都具有降低资源消耗, 提高响应速度等特点
除去线程池和连接池, 还有内存池以及实例池的概念与其较为接近.
简而言之, 连接池就是在里面存放一些个活跃的连接, 当需要时直接从连接池中取, 若没有可用的活跃连接便创建一个新的连接, 而若是当前池子已经满了, 那么就进入阻塞, 等待有连接可用.
这里的池子大小, 个人感觉和可同时连接服务器的最大数量是一个概念(存疑)
另外, 我们需要使用setMaxIdleConns(x int)手动设置活跃连接的最大数量, 一般默认为2.
并且, 我们需要使用setMaxOpenConns()设置打开数据库连接的最大数量, 默认值为0, 即表示可以无限连接数据库.
另外, 两个函数通常绑定在一起使用
因为gorm的每次sql操作都是从连接池中取出连接, 所以每次操作时连接不一定相同, 因此在事务等使用场景下, 需要固定某一连接, 即使用tx:=db.Begin()等操作固定连接, 在事务结束前不将其放入到连接中
共享会话连接
因为每次执行sql操作后, gorm返回一个初始化的*gorm.DB示例, 其不能安全地重复使用, 并且可能会被先前使用该实例的条件污染.
因此可以使用新建会话的方法, 创建一个可以共享的实例, 该是每次使用完都会初始化, 不必担心被先前条件污染
db:= DB.Where("name = ?","test").Session(&gorm.Session{})
数据模型
约定
与定义json等数据模型类似, gorm的数据模型也是通过struct来进行定义
type User struct{
ID uint
Name string
}
对于gorm而言, 约定大于配置
gorm使用ID作为主键, 将结构体的蛇形复数设定为表名, 将字段的蛇形设定为列名, 并使用UpdatedAt, CreatedAt等字段表示更新时间和创建时间(约定)
type UserLog struct{...} -> user_logs //表名, 蛇形复数
若是需要另外配置表名, 只需要为User实现TableName() string接口即可(应当遵循蛇形复数的约定)
默认值
gorm也是使用tag为字段定义默认值, 当插入时, 若是某字段插入值为0值, 则将会使用默认值替代0值
type User struct {
ID int64
Name string `gorm:"default:galeone"`
Age int64 `gorm:"default:18"`
}
gorm.Model
gorm定义了一个gorm.Model的结构体, 可以直接将gorm.Model作为匿名结构体导入到自己定义的结构体中
//gorm.Model
type Model struct {
ID uint `gorm:"primaryKey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
}
而对于CreatedAt, UpdatedAt字段, gorm会自动记录填充时间, 若需要更换时间戳方式/类型, 需要使用tag手动标注填充类型, 默认采用time.Time类型进行填充
而对于结构体嵌套, gorm会默认将匿名子结构体中的字段放入到父结构体中, 或是在非匿名结构体后打上tag标志将其拆分到父结构体中.
type Author struct{
Name string
Email string
}
type Blog struct{
Author Author `gorm:"embedded"`
}
//=======等价于=========
type Blog struct{
Author
}
//=====================
type Blog struct{
Author Author 'gorm:"embedded;embeddedPrefix_prefix"'
}
//=======等价于========
type Blog struct{
PrefixName string
PrefixEmail string
}
事务
Gorm默认在CUD等操作时开启事务, 主动关闭默认事务, 约会获得30%的性能提升
//全局禁用默认事务
db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{
SkipDefaultTransaction: true,
})
通常情况下, 为了简便代码, 推荐使用db.Transaction() 来进行事务操作, 在db.Transaction中, 返回err时会自动回滚, 而返回nil时会自动提交事务.
db.Transaction(func(tx *gorm.DB) error {
// 在事务中执行一些 db 操作(从这里开始,您应该使用 'tx' 而不是 'db')
if err := tx.Create(&Animal{Name: "Giraffe"}).Error; err != nil {
// 返回任何错误都会回滚事务
return err
}
if err := tx.Create(&Animal{Name: "Lion"}).Error; err != nil {
return err
}
// 返回 nil 提交事务
return nil
})
Hook
Hook主要是在某些操作创建前后执行的函数
主要有BeforeSave,BeforeCreate,AfterSave,AfterCreate, BeforeUpdate,AfterUpdate, BeforeDelete,AfterDelete, AfterFind等
若是在整个生命周期过程中出现任何错误, 将立即停止后续操作并回滚事务(也将会放弃修改)
若是需要跳过Hook方法, 可以开启SkipHooks会话模式
DB.Session(&gorm.Session{SkipHooks: true}).Create(&user)
CRUD
Create
//根据数据模型将数据插入到表中
user := User{Name: "Jinzhu", Age: 18, Birthday: time.Now()}
//插入成功后, 将表中, 改行对应的信息重新写回到user中, 主要是为了重写某些隐藏的字段
result := db.Create(&user) // 通过数据的指针来创建
//result.Error记录error信息, result.RowsAffected返回插入的记录数量
db.Create()中的参数可以是struct结构体, 也可以是结构体数组(用以批量插入), 也能通过map[string]interface{}{}进行插入
值得注意的是, 在使用map进行插入时, gorm会忽略掉需要自动填充的字段, 并且关联association也不会被触发
gorm也允许使用SQL语句进行插入, 通常用clause.Expr{SQL:"这里写sql语句",Vars:[]interface{}}替换map中对应的值
关联创建
关联先可以简单视为数据模型中的非匿名嵌套结构体
在创建关联数据时, 如果关联数据值非0, 这些字段会被upsert, 且相应的Hook方法也会触发
可以通过Omit等跳过关联数据的保存
db.Omit(clause.Associations).Create(&uset)//跳过所有关联
Upsert
Upsert是update与insert的结合体, 执行时二者取其一, 优先取update
Read
查找单个对象
db.First(&user)//按照主键升序, 获取第一条记录, 不推荐使用, 因为会返回err
db.Find(&user,id)//推荐使用, 因为不回返回err, 并且可以接收slice, 查找时也可以指明id
db.First(&user, 10)
// SELECT * FROM users WHERE id = 10;
db.First(&user, "10")
// SELECT * FROM users WHERE id = 10;
db.Find(&users, []int{1,2,3})
// SELECT * FROM users WHERE id IN (1,2,3);
查找全部对象
db.Find(&([]User{}))
//SELECT * FROM users;
条件查询
gorm可以db后任意叠加where()来添加查询条件, 对于where语句中出现的数值, 可以使用?充当占位符, 并在后方填写对应value(与printf()类似)
// Get first matched record
db.Where("name = ?", "jinzhu").First(&user)
// SELECT * FROM users WHERE name = 'jinzhu' ORDER BY id LIMIT 1;
// Get all matched records
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;
// Time
db.Where("updated_at > ?", lastWeek).Find(&users)
// SELECT * FROM users WHERE updated_at > '2000-01-01 00:00:00';
// BETWEEN
db.Where("created_at BETWEEN ? AND ?", lastWeek, today).Find(&users)
// SELECT * FROM users WHERE created_at BETWEEN '2000-01-01 00:00:00' AND '2000-01-08 00:00:00';
同时, gorm也可以使用struct和map进行条件查找.
// 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;
// Slice of primary keys
db.Where([]int64{20, 21, 22}).Find(&users)
// SELECT * FROM users WHERE id IN (20, 21, 22);
排序
db.Order("age desc, name").Find(&users)
// SELECT * FROM users ORDER BY age desc, name;
// Multiple orders
db.Order("age desc").Order("name").Find(&users)
// SELECT * FROM users ORDER BY age desc, name;
db.Clauses(clause.OrderBy{
Expression: clause.Expr{SQL: "FIELD(id,?)", Vars: []interface{}{[]int{1, 2, 3}}, WithoutParentheses: true},
}).Find(&User{})
// SELECT * FROM users ORDER BY FIELD(id,1,2,3)
Update
Save会保存所有字段, 包括零值字段.
db.First(&user)
user.Name = "jinzhu 2"
user.Age = 100
db.Save(&user)
// UPDATE users SET name='jinzhu 2', age=100, birthday='2016-01-01', updated_at = '2013-11-17 21:34:10' WHERE id=111;
而使用struct{}更新时, 只会更新非零值, 若是需要保存零值, 请使用map或是'save'进行保存
Delete
删除单条记录时, 需要指明主键, 否则会删除满足条件的所有记录
那么当delete执行时不含主键, 便会删除所有匹配的记录
但是gorm执行批量删除时, 必须加一些额外的条件, 否则会返回ErrMissingWhereClause错误
db.Delete(&User{}).Error // gorm.ErrMissingWhereClause
db.Where("1 = 1").Delete(&User{})
// DELETE FROM `users` WHERE 1=1
db.Exec("DELETE FROM users")
// DELETE FROM users
db.Session(&gorm.Session{AllowGlobalUpdate: true}).Delete(&User{})
// DELETE FROM users
与read中相似, delete指定主键时, 主键可以是数字, 字符串或者是整型数组
软删除
当数据结构中包含DeletedAt字段时, 将默认开启软删除, 此时再执行delete语句, 不回将其真正删除, 而是会为DeletedAt填充上当前时间
并且软删除之后, 无法通过寻常的查询方法获取到该记录
只能使用Unscoped寻找被软删除的数据
//查找软删除的数据
db.Unscoped().Where("age = 20").Find(&users)
// SELECT * FROM users WHERE age = 20;
那么, 再开启软删除后, 也只能通过Unscoped对数据进行永久删除
//永久删除
db.Unscoped().Delete(&order)
// DELETE FROM orders WHERE id=10;