这是我参与「第五届青训营 」伴学笔记创作活动的第 7 天
(内容根据字节跳动青训营课程内容以及自己的理解编写)
近期将日更这几个主题的文章,欢迎关注!
- Kitex
- Hertx
- go的测试环节
- goFrame
前面带大家已经理解了go如何连接mysql并且使用一些简单操作(Go与Mysql),今天带大家用Gorm实现对数据库的操作
Gorm的优点
Gorm是一个已经迭代了10年+的功能强大的ORM框架,在字节内部被广泛使用并且拥有非常丰富的开源拓展
摘录官方文档的话:
特性:
- 全功能 ORM
- 关联 (Has One,Has Many,Belongs To,Many To Many,多态,单表继承)
- Create,Save,Update,Delete,Find 中钩子方法
- 支持
Preload、Joins的预加载 - 事务,嵌套事务,Save Point,Rollback To Saved Point
- Context、预编译模式、DryRun 模式
- 批量插入,FindInBatches,Find/Create with Map,使用 SQL 表达式、Context Valuer 进行 CRUD
- SQL 构建器,Upsert,数据库锁,Optimizer/Index/Comment Hint,命名参数,子查询
- 复合主键,索引,约束
- Auto Migration
- 自定义 Logger
- 灵活的可扩展插件 API:Database Resolver(多数据库,读写分离)、Prometheus…
- 每个特性都经过了测试的重重考验
- 开发者友好
Gorm 代码
有时间的话一定要手敲一遍!
基本使用
Gorm支持连接各种各样的数据库,想要快速连接可以直接看官方文档
// User 定义了gorm model
type User struct {
Id string
Name string
}
// TableName 为model定义表名
func (user User) TableName() string {
return "user"
}
func main() {
// 参考 https://github.com/go-sql-driver/mysql#dsn-data-source-name 获取详情
dsn := "root:abc123@tcp(127.0.0.1:3306)/test?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
panic("fail to connnect database!")
}
// Create
db.Create(&User{Id: "1", Name: "sc"})
// Read
var user User
db.First(&user, 1)
fmt.Println(user)
db.First(&user, "name=?", "sc")
fmt.Println(user)
// Update - 将user的name更新为"scsc"
db.Model(&user).Update("name", "scsc")
// Update 多个字段
db.Model(&user).Updates(User{Id: "11", Name: "scsc"})
db.Model(&user).Updates(map[string]interface{}{"Id": "111", "Name": "scscsc"})
// Delete - 删除user
db.Delete(&user, 1)
}
增加
// User 定义了gorm model
type User struct {
Id int64
Name string
}
// TableName 为model定义表名
func (user User) TableName() string {
return "user"
}
func main() {
// 参考 https://github.com/go-sql-driver/mysql#dsn-data-source-name 获取详情
dsn := "root:abc123@tcp(127.0.0.1:3306)/test?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
panic("fail to connnect database!")
}
// 创建一条
user := &User{Name: "maiqu"}
res1 := db.Create(user)
fmt.Println(res1.Error)
fmt.Println(res1)
fmt.Println(user.Id) // 添加是默认返回主键的 我们添加的时候是没有加主键的,这里传值用的是引用
// 创建多条
users := []*User{{Name: "yhh1"}, {Name: "yhh2"}, {Name: "yhh3"}}
res2 := db.Create(users)
fmt.Println(res2.Error)
fmt.Println(res2)
for _, user := range users {
fmt.Println(user.Id) // 添加是默认返回主键的 我们添加的时候是没有加主键的,这里传值用的是引用
}
}
两个问题
1.Upsert
使用clause.OnConflict处理数据冲突
user:=&User{Id:111,Name:"scscsc"
db.Clause(clause.OnConflict{DoNothing: true}).Create(&user)
2.如何使用默认值
通过使用default标签为字段定义默认值
// User 定义了gorm model
type User struct {
Id int64
Name string `gorm:"default: noname"`
}
删除
物理删除
db.Delete(&User{}, 10) // DELETE FROM user WHERE id = 10;
db.Delete(&User{}, "10") // DELETE FROM user WHERE id = 10;
db.Delete(&User{}, []int{1, 2, 3}) // DELETE FROM user WHERE id IN (1, 2, 3);
db.Where("name LIKE ?", "%jinzhu%").Delete(User{}) // DELETE from user where name LIKE %zhizhu%
db.Delete(User{}, "email LIKE ?", "%jinzhu%")// DELETE from user where name LIKE %jinzhu%;
软删除
// User 定义了gorm model
type User struct {
Id int
Name string
Age int
Deleted gorm.DeletedAt
}
// TableName 为model定义表名
func (user User) TableName() string {
return "user"
}
func main() {
db, err := gorm.Open(mysql.Open("root:abc123@tcp(127.0.0.1:3306/test?charset=utf8"),
&gorm.Config{})
if err != nil {
panic(err)
}
// 删除一条
u := User{Id: 111}
db.Delete(&u) // UPDATE user SET deleted_at='2023-1-25 21:31:31' WHERE id =111;
// 批量删除
db.Where("age = ?", 20).Delete(&User{}) // UPDATE user SET deleted_at='2023-1-25 21:31:31' WHERE age =20
users := make([]*User, 0)
// 在查询的时候会忽略被软删除的记录
db.Where("age=20").Find(&users) // SELECT * FROM user WHERE age =20 AND deleted_at is NULL;
// 在查询时不会忽略被软删除的记录
db.Unscoped().Where("age=20").Find(&users) // SELECT * FROM user WHERE age = 20
}
注意点:
- gorm.DeletedAt用语帮助用户实现软删
- 拥有软删能力的Model调用Delete时,记录不会被从数据库真正的删除。但是GORM默认查询delete_at为null的记录
- 使用Unscoped可以查询到被软删的数据
更新
这里的例子有点疏漏,没有加时间字段,具体可以看下面的补充里所写
// User 定义了gorm model
type User struct {
Id int
Name string
}
// TableName 为model定义表名
func (user User) TableName() string {
return "user"
}
func main() {
// 参考 https://github.com/go-sql-driver/mysql#dsn-data-source-name 获取详情
dsn := "root:abc123@tcp(127.0.0.1:3306)/test?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
panic("fail to connnect database!")
}
// 条件更新单个列
// UPDATE user SET name = 'sc123', updated_at='2023-1-25 21:31:31' WHERE id>100;
db.Model(&User{Id: 111}).Where("id>?", 100).Update("name", "sc123")
// 更新多个列
// 根据`struct`更新属性, 只会更新非零值的字段
// UPDATE user SET name='sc123', updated_at = '2023-1-25 21:31:31' WHERE id =111
db.Model(&User{Id: 111}).Updates(User{Name: "sc123"})
// 根据map更新属性
// UPDATE user SET name = 'sc123' updated_at='2023-1-25 21:31:31' WHERE id = 111;
db.Model(&User{Id: 111}).Updates(map[string]interface{}{"name": "123"})
// 更新选定字段
// UPDATE user SET name = 'sc123' WHERE id = 111
db.Model(&User{Id: 111}).Select("name").Updates(map[string]interface{}{
"name": "sc",
"age": 1, // 不在select里面的字段不会被更新
})
// SQL表达式更新 这里只是举个例子,没有age字段
// UPDATE user Set 'age' = age*2+100 ,'updated_at' = '2023-1-25 21:31:31' WHERE id = 111
db.Model(&User{Id: 111}).Update("age", gorm.Expr("age*? +?", 2, 100))
}
使用结构体当做条件来查询的时候,GORM只会查询非零字段,如果字段是0、""、false、或者其他零值,该字段不会被用于构建查找条件,使用map创建则不会
查询
// User 定义了gorm model
type User struct {
Id int
Name string
}
// TableName 为model定义表名
func (user User) TableName() string {
return "user"
}
func main() {
// 参考 https://github.com/go-sql-driver/mysql#dsn-data-source-name 获取详情
dsn := "root:abc123@tcp(127.0.0.1:3306)/test?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
panic("fail to connnect database!")
}
// 获取第一条记录(主键升序),查询不到数据则返回ErrRecordNotFound
u := &User{}
db.First(u) // SELECT * FROM users ORDER BY id LIMIT 1
// 查询多条数据
users := make([]*User, 0)
result := db.Where("id>100").Find(&users) // SELECT * FROM user where id>100
fmt.Println(result.RowsAffected)
fmt.Println(result.Error)
// IN SELECT * FROM user WHERE name IN ("scscsc", "yhh");
db.Where("name IN ?", []string{"scscsc", "yhh"}).Find(users)
// LIKE SELECT * FROM user WHERE name LIKE '%sc%';
db.Where("name LIKE ?", "%sc%").Find(users)
// AND SELECT * FROM user WHERE name="scscsc" AND id>=100;
db.Where("name =? AND id>=?", "scscsc", "100").Find(users)
// SELECT * FROM user WHERE name = "scscsc"; 这里为啥没有id看下面的注意点里有写
db.Where(&User{Name: "scscsc", Id: 0}).Find(users)
// SELECT * FROM user WHERE name = "scscsc" AND id=1;
db.Where(map[string]interface{}{"Name": "scscsc", "Id": 1}).Find(users)
}
两个注意点
1.使用First的时候,查询不到数据会返回ErrRecordNotFound,使用Find查找多条数据的时候,查询不到不会返回错误
2.使用结构体当做条件来查询的时候,GORM只会查询非零字段,如果字段是0、""、false、或者其他零值,该字段不会被用于构建查找条件,使用map创建则不会
Gorm事务
Gorm提供了Begin、 Commit、RollBack方法用于事务
func main() {
db, err := gorm.Open(mysql.Open("root:abc123@tcp(127.0.0.1:3306/test?charset=utf8"),
&gorm.Config{})
if err != nil {
panic(err)
}
tx:=db.Begin() // 开始执行事务
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()
}
这么写容易遗忘某个分支,推荐这么写:
func main() {
db, err := gorm.Open(mysql.Open("root:abc123@tcp(127.0.0.1:3306/test?charset=utf8"),
&gorm.Config{})
if err != nil {
panic(err)
}
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提供了一些类似于AOP切面操作的东西,可以在执行CRUD之前后自动调用的函数
如果Hook内部的东西返回错误,或者Hook之间的东西发生错误,都会回滚
func (u *User)BeforeCreate(tx *gorm.DB) (err error) {
if u.Age>10{
return errors.New("age>10")
}
return
}
func (u *User)AfterCreate(tx *gorm.DB) (err error) { // 比如这里可以执行完sql之后给user创建一个邮箱
return tx.Create(&Email{ID:u.ID,......}).Error
}
对于写操作,为了确保数据的完整性,GORM会将他们封装在事务内运行。但是这个会降低性能
可以关闭默认事务
db, err := gorm.Open(mysql.Open("root:abc123@tcp(127.0.0.1:3306/test?charset=utf8"),
&gorm.Config{
SkipDefaultTransaction: true,// 关闭默认事务
PrepareStmt: true},// 缓存预编译语句 可以提高调用性能,大概优化35%
)
if err != nil {
panic(err)
}
Gorm生态
补充
一些声明模型的约定:
gorm.io/zh_CN/docs/…