Gorm详解 | 青训营笔记

1,715 阅读7分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 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 中钩子方法
  • 支持 PreloadJoins 的预加载
  • 事务,嵌套事务,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)

}

image.png

增加

// 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) // 添加是默认返回主键的 我们添加的时候是没有加主键的,这里传值用的是引用
   }
}

image.png

image.png

两个问题

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
}

注意点:

  1. gorm.DeletedAt用语帮助用户实现软删
  2. 拥有软删能力的Model调用Delete时,记录不会被从数据库真正的删除。但是GORM默认查询delete_at为null的记录
  3. 使用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生态

image.png

补充

image.png 一些声明模型的约定: gorm.io/zh_CN/docs/…

资料

Gorm官方文档