Gorm | 青训营笔记

96 阅读12分钟

这是我参与[第五届青训营]伴学笔记创作活动的第3天

Gorm背景介绍

Gorm简单理解和数据库进行交互的框架 类似于java中的mybatis

阅读官方文档GORM 指南 | GORM - The fantastic ORM library for Golang, aims to be developer friendly.

官方文档是最好的学习资料,看不懂的时候可以再看视频

什么叫做csn?

参考 github.com/go-sql-driv… 获取详情

安装

go get -u gorm.io/gorm
go get -u gorm.io/driver/sqlite
package main
​
import (
  "gorm.io/gorm"
  "gorm.io/driver/sqlite"
)
​
type Product struct {
  gorm.Model
  Code  string
  Price uint
}
func (p Product) TableName()string{
    return "product"  //定义表名
}
​
func main() {
  db, err := gorm.Open("user:pass@tcp(ip:port)数据库名"?charset=utf8mb4&parseTime=True&loc=Local, &gorm.Config{})//连接数据库
  if err != nil {
    panic("failed to connect database")
  }
​
  // 迁移 schema
  db.AutoMigrate(&Product{})
​
  // Create
  db.Create(&Product{Code: "D42", Price: 100})  //如果是单个字段,可以使用map,多个字段使用左边这种类型// 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)
}

【注意】Gorm约定定义的实体类和数据库字段的对应关系

使用名为ID的字段作为主键

如果未定义表明,则使用结构体的蛇形负数作为表名

字段名的蛇形作为列名

插入数据

gorm是链式调用:返回的数据获取error,获取插入数据的主键


package main
​
import (
    "fmt"
    "gorm.io/driver/mysql"
    "gorm.io/gorm"
)
type User struct{
    gorm.Model
    ID    uint     `gorm:primarykey`      //ID默认为主键
    Name  string   `gorm:"column:name"`   //默认实体类Name与数据库name对应
    Age   uint     `gorm"column:age"`     //默认实体类Name与数据库name对应
}
​
func main()  {
    dsn := "root:root@tcp(127.0.0.1:3306)/zjqx?charset=utf8mb4&parseTime=True&loc=Local"
    db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{})
    user := User{ID:1,Name: "limingjie", Age: 23}
​
result := db.Create(&user)                       // 通过数据的指针来创建
​
fmt.Println(user.ID)                             // 返回插入数据的主键
fmt.Println(result.Error)                        // 返回 error
fmt.Println(result.RowsAffected)                // 返回插入记录的条数
​
}

打印结果


1
<nil>
1

数据库中结果

1674832504619.png 【注意】

(1)要带指针,否则会报错result := db.Create(&user)

(2)两者的区别

  • user.ID 和操作数据相关
  • result.Error result.RowsAffected 操作结果相关

(3)插入数据默认会带有created_at、updated_at以及deleted_at ,因此设计表时应添加这些字段,否则会报错

(4)如何设置默认值?

这个在插入数据的时候生效而在插入数据的时候不会生效

type User struct{
    gorm.Model
    ID    uint     `gorm:primarykey`
    Name  string   `gorm:"default:galeone"`
    Age   uint     `gorm:"default:18"`
}

当插入数据的时候如果没有定义值,就会自动插入默认数据

(5)type User struct对应的数据库为users,这是因为没有指定表名,那么如何修改为user呢?


func ( u User) TableName()string{
    return "user"  //定义表名
}

查询数据

Gorm提供了FirstTakeLast方法来查询数据库,并且这三个都能会自动添加Limit 1的条件,如果没有找到数据,它会返回 ErrRecordNotFound 错误

检索单个对象

问题1:limit 1这个条件意味着什么?

只查询一条数据,以此为基础,不难理解如下代码


func main()  {
    dsn := "root:root@tcp(127.0.0.1:3306)/zjqx?charset=utf8mb4&parseTime=True&loc=Local"
    db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{})
    
user := User{}
// 获取第一条记录(主键升序)
db.First(&user)
// SELECT * FROM users ORDER BY id LIMIT 1;// 获取一条记录,没有指定排序字段
db.Take(&user)
// SELECT * FROM users LIMIT 1;// 获取最后一条记录(主键降序)
db.Last(&user)
//SELECT * FROM users ORDER BY id DESC LIMIT 1;
result := db.Last(&user)                     // 通过数据的指针来创建
​
fmt.Println(user.ID)                             // 返回查询数据的主键
fmt.Println(result.Error)                        // 返回 error
fmt.Println(result.RowsAffected)                // 返回插入记录的条数
// 检查 ErrRecordNotFound 错误
 errors.Is(result.Error, gorm.ErrRecordNotFound)
}

【注意】

(1)同样需要连接数据库

(2)默认是只查询一条数据,First会主键升序,Last会主键降序 ,Take未指定排序字段,但都只会有一条记录(和主键为1要区别开来)

(3)我按照以上代码进行查询,发现db.First和db.Last查询结果一致,这是为什么呢?

1
<nil>
1

当一个函数中利用结构体指针为目标同时调用Last与First时,查询结果相同,为先执行的方法结果。

如果要使用Last就把First注释掉

(4)如果未指定主键,则按照第一个字段进行升序、降序或者不排序


// 根据第一个字段排序
type Language struct {
  Code string
  Name string
}
DB.First(&Language{})
// SELECT * FROM `languages` ORDER BY `languages`.`code` LIMIT 1

根据主键进行检索


db.First(&user, 10)
// SELECT * FROM users WHERE id = 10;
​
db.First(&user, "10")
// SELECT * FROM users WHERE id = 10;
​

【注意】find和first的区别?

查询多个对象

​
​
func main()  {
    dsn := "root:root@tcp(127.0.0.1:3306)/zjqx?charset=utf8mb4&parseTime=True&loc=Local"
    db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{})
    
//查询多条数据
users :=make([]*User,0)
// 1.获取全部记录 SELECT * FROM users;
result := db.Find(&users)
//2.条件查询
//SELECT * FROM users WHERE age > 10;
// result := db.Where("id > 1").Find(&users)
​
//SELECT * FROM users WHERE name Like '%ming%';
// result := db.Where("name Like ?","%ming%").Find(&users)
​
//SELECT * FROM users WHERE   age In (18.23);
// result := db.Where("age In ?",[]int{18,23}).Find(&users)
​
//SELECT * FROM users WHERE   name ='limingjie'  AND age >= 22;
// result := db.Where("age >= ? AND  name = ?",18,"limingjie").Find(&users)
​
​
//SELECT * FROM users WHERE  name ='limingjie'  
// result := db.Where(User{Name:"limingjie",Age:0}).Find(&users)
​
//SELECT * FROM users WHERE  name ='limingjie'  AND age = 0
result := db.Where(map[string]interface{}{"Name":"limingjie","Age":0}).Find(&users)
​
fmt.Println(users)                             // 返回操作数据的主键
fmt.Println(result.Error)                       // 返回error
fmt.Println(result.RowsAffected)                // 返回操作记录的条数
​
​
​

【注意】使用结构体作为查询条件,查询非零值字段,这意味着当gorm中设置查询条件为0,false或其他零值将不会生效

如果想要生效:可以使用map来构建查询条件

更新数据

更新一列

//  UPDATE users SET name='hello' WHERE id = 1; 
    db.Model(User{ID:1}).Update("name","lizhian")
//  UPDATE users SET name='hello' WHERE age < 18; 
    db.Model(User{}).Where("age < ?",18).Update("name","lizhian")

【注意】

第一行代码中的User ID为查询条件 Model(User)表示指定更新表为user

第二行代码中使用where作为条件,那么User只充当指定表的作用,放一个空的User

更新多列

//UPDATE users SET name='limingjie', age=20, updated_at = '2013-11-17 21:34:10' WHERE id = 2;
db.Model(User{ID: 2}).Updates(User{Name: "limingjie", Age: 20})
///根据map
db.Model(User{ID: 2}).Updates(map[string]interface{}{"name": "hello", "age": 18})
//选定字段   UPDATE users SET name='limingjie', updated_at = '2013-11-17 21:34:10' WHERE id = 2;
db.Model(User{ID: 2}).Select("name").Updates(map[string]interface{}{"name": "hello", "age": 18})

【注意】课程中说得只能非零值字段有歧义:意思是在.go文件中的结构体中设为0的话是不会生效的,而不是数据库中数据为0不能进行操作

如果要生效使用map来进行更新

删除数据

删除主要分为两类,一类是硬删除,另一类是软删除

  • 硬删除:直接删除(从数据库中删除)

  • 软删除:放入回收站,还未删除(记录不会从数据库中真正删除。但是会将DeletedAt 置为当前时间并且无法通过正常的方法找到该记录)

    如何实现软删除?

    在定义模型的时候添加一个gorm.deletedat 字段

    type User struct {
        Deleted  gorm.Deltedat    //或者使用gorm.Model,因为这个模型已经包含了这个字段
        ID   uint   `gorm:primarykey`
        Name string `gorm:"default:galeone"`
        Age  uint   `gorm:"default:18"`
        }
    
    // DELETE FROM users WHERE id = 10;
    db.Delete(&User{},10)
    // DELETE FROM users WHERE id IN (1,2,3);
    db.Delete(&User{},[]int{1,2,3})
    // UPDATE users SET deleted_at="2013-10-29 10:23" WHERE id = 111;
    db.Delete(&User{ID :1})
    // UPDATE users SET deleted_at="2013-10-29 10:23" WHERE age = 20;
    db.Where("age = 20").Delete(&User{})
    

事务

为什么要使用事务?

事务是为了保证数据一致性,多个操作要具有原子性,要不都成功,要不都不成功,事务中的操作不能割裂开来

为什么要使用transaction?

在设置事务时,如果操作出错需要事务回滚,如果操作都没问题,进行事务提交,但是在开发过程中,不可避免会忘记写回滚和提交的代码

因此使用transaction能够自动提交事务,避免用户漏泄,很好地解决这一问题

//函数返回的错误给到err,然后再对err进行if判断
if err = tx.Create(&User{Name:"name"}).Error; err != nil{
    return err  return nil
}
//利用db.Transaction给err赋值,如果里面的事务操作报错,则给error返回的不是nil,那么自动进行回滚,外层的err也不是nil;如果事务操作正常,则给error返回nil,事务自动提交,外层err为nil
if err = db.Transaction(func(tx *gorm.DB) error{
    
});err != nil

性能优化

使用PrepareStmt来进行性能优化

其他的一些性能优化方案可以阅读官方文档性能优化方案性能 | GORM - The fantastic ORM library for Golang, aims to be developer friendly.

Gorm生态

Gorm拥有非常丰富的生态 阅读官方文档GORM 指南 | GORM - The fantastic ORM library for Golang, aims to be developer friendly.

官方文档是最好的学习资料,看不懂的时候可以再看视频

什么叫做csn? // 参考 github.com/go-sql-driv… 获取详情

安装

go get -u gorm.io/gorm
go get -u gorm.io/driver/sqlite
package main
​
import (
  "gorm.io/gorm"
  "gorm.io/driver/sqlite"
)
​
type Product struct {
  gorm.Model
  Code  string
  Price uint
}
func (p Product) TableName()string{
    return "product"  //定义表名
}
​
func main() {
  db, err := gorm.Open("user:pass@tcp(ip:port)数据库名"?charset=utf8mb4&parseTime=True&loc=Local, &gorm.Config{})//连接数据库
  if err != nil {
    panic("failed to connect database")
  }
​
  // 迁移 schema
  db.AutoMigrate(&Product{})
​
  // Create
  db.Create(&Product{Code: "D42", Price: 100})  //如果是单个字段,可以使用map,多个字段使用左边这种类型// 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)
}

【注意】Gorm约定定义的实体类和数据库字段的对应关系

使用名为ID的字段作为主键

如果未定义表明,则使用结构体的蛇形负数作为表名

字段名的蛇形作为列名

插入数据

gorm是链式调用:返回的数据获取error,获取插入数据的主键


package main
​
import (
    "fmt"
    "gorm.io/driver/mysql"
    "gorm.io/gorm"
)
type User struct{
    gorm.Model
    ID    uint     `gorm:primarykey`      //ID默认为主键
    Name  string   `gorm:"column:name"`   //默认实体类Name与数据库name对应
    Age   uint     `gorm"column:age"`     //默认实体类Name与数据库name对应
}
​
func main()  {
    dsn := "root:root@tcp(127.0.0.1:3306)/zjqx?charset=utf8mb4&parseTime=True&loc=Local"
    db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{})
    user := User{ID:1,Name: "limingjie", Age: 23}
​
result := db.Create(&user)                       // 通过数据的指针来创建
​
fmt.Println(user.ID)                             // 返回插入数据的主键
fmt.Println(result.Error)                        // 返回 error
fmt.Println(result.RowsAffected)                // 返回插入记录的条数
​
}

打印结果


1
<nil>
1

数据库中结果

【注意】

(1)要带指针,否则会报错result := db.Create(&user)

(2)两者的区别

  • user.ID 和操作数据相关
  • result.Error result.RowsAffected 操作结果相关

(3)插入数据默认会带有created_at、updated_at以及deleted_at ,因此设计表时应添加这些字段,否则会报错

(4)如何设置默认值?

这个在插入数据的时候生效而在插入数据的时候不会生效

type User struct{
    gorm.Model
    ID    uint     `gorm:primarykey`
    Name  string   `gorm:"default:galeone"`
    Age   uint     `gorm:"default:18"`
}

当插入数据的时候如果没有定义值,就会自动插入默认数据

(5)type User struct对应的数据库为users,这是因为没有指定表名,那么如何修改为user呢?


func ( u User) TableName()string{
    return "user"  //定义表名
}

查询数据

Gorm提供了FirstTakeLast方法来查询数据库,并且这三个都能会自动添加Limit 1的条件,如果没有找到数据,它会返回 ErrRecordNotFound 错误

检索单个对象

问题1:limit 1这个条件意味着什么?

只查询一条数据,以此为基础,不难理解如下代码


func main()  {
    dsn := "root:root@tcp(127.0.0.1:3306)/zjqx?charset=utf8mb4&parseTime=True&loc=Local"
    db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{})
    
user := User{}
// 获取第一条记录(主键升序)
db.First(&user)
// SELECT * FROM users ORDER BY id LIMIT 1;// 获取一条记录,没有指定排序字段
db.Take(&user)
// SELECT * FROM users LIMIT 1;// 获取最后一条记录(主键降序)
db.Last(&user)
//SELECT * FROM users ORDER BY id DESC LIMIT 1;
result := db.Last(&user)                     // 通过数据的指针来创建
​
fmt.Println(user.ID)                             // 返回查询数据的主键
fmt.Println(result.Error)                        // 返回 error
fmt.Println(result.RowsAffected)                // 返回插入记录的条数
// 检查 ErrRecordNotFound 错误
 errors.Is(result.Error, gorm.ErrRecordNotFound)
}

【注意】

(1)同样需要连接数据库

(2)默认是只查询一条数据,First会主键升序,Last会主键降序 ,Take未指定排序字段,但都只会有一条记录(和主键为1要区别开来)

(3)我按照以上代码进行查询,发现db.First和db.Last查询结果一致,这是为什么呢?

1
<nil>
1

当一个函数中利用结构体指针为目标同时调用Last与First时,查询结果相同,为先执行的方法结果。

如果要使用Last就把First注释掉

(4)如果未指定主键,则按照第一个字段进行升序、降序或者不排序


// 根据第一个字段排序
type Language struct {
  Code string
  Name string
}
DB.First(&Language{})
// SELECT * FROM `languages` ORDER BY `languages`.`code` LIMIT 1

根据主键进行检索


db.First(&user, 10)
// SELECT * FROM users WHERE id = 10;
​
db.First(&user, "10")
// SELECT * FROM users WHERE id = 10;
​

【注意】find和first的区别?

查询多个对象

​
​
func main()  {
    dsn := "root:root@tcp(127.0.0.1:3306)/zjqx?charset=utf8mb4&parseTime=True&loc=Local"
    db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{})
    
//查询多条数据
users :=make([]*User,0)
// 1.获取全部记录 SELECT * FROM users;
result := db.Find(&users)
//2.条件查询
//SELECT * FROM users WHERE age > 10;
// result := db.Where("id > 1").Find(&users)
​
//SELECT * FROM users WHERE name Like '%ming%';
// result := db.Where("name Like ?","%ming%").Find(&users)
​
//SELECT * FROM users WHERE   age In (18.23);
// result := db.Where("age In ?",[]int{18,23}).Find(&users)
​
//SELECT * FROM users WHERE   name ='limingjie'  AND age >= 22;
// result := db.Where("age >= ? AND  name = ?",18,"limingjie").Find(&users)
​
​
//SELECT * FROM users WHERE  name ='limingjie'  
// result := db.Where(User{Name:"limingjie",Age:0}).Find(&users)
​
//SELECT * FROM users WHERE  name ='limingjie'  AND age = 0
result := db.Where(map[string]interface{}{"Name":"limingjie","Age":0}).Find(&users)
​
fmt.Println(users)                             // 返回操作数据的主键
fmt.Println(result.Error)                       // 返回error
fmt.Println(result.RowsAffected)                // 返回操作记录的条数
​
​
​

【注意】使用结构体作为查询条件,查询非零值字段,这意味着当gorm中设置查询条件为0,false或其他零值将不会生效

如果想要生效:可以使用map来构建查询条件

更新数据

更新一列

//  UPDATE users SET name='hello' WHERE id = 1; 
    db.Model(User{ID:1}).Update("name","lizhian")
//  UPDATE users SET name='hello' WHERE age < 18; 
    db.Model(User{}).Where("age < ?",18).Update("name","lizhian")

【注意】

第一行代码中的User ID为查询条件 Model(User)表示指定更新表为user

第二行代码中使用where作为条件,那么User只充当指定表的作用,放一个空的User

更新多列

//UPDATE users SET name='limingjie', age=20, updated_at = '2013-11-17 21:34:10' WHERE id = 2;
db.Model(User{ID: 2}).Updates(User{Name: "limingjie", Age: 20})
///根据map
db.Model(User{ID: 2}).Updates(map[string]interface{}{"name": "hello", "age": 18})
//选定字段   UPDATE users SET name='limingjie', updated_at = '2013-11-17 21:34:10' WHERE id = 2;
db.Model(User{ID: 2}).Select("name").Updates(map[string]interface{}{"name": "hello", "age": 18})

【注意】课程中说得只能非零值字段有歧义:意思是在.go文件中的结构体中设为0的话是不会生效的,而不是数据库中数据为0不能进行操作

如果要生效使用map来进行更新

删除数据

删除主要分为两类,一类是硬删除,另一类是软删除

  • 硬删除:直接删除(从数据库中删除)

  • 软删除:放入回收站,还未删除(记录不会从数据库中真正删除。但是会将DeletedAt 置为当前时间并且无法通过正常的方法找到该记录)

    如何实现软删除?

    在定义模型的时候添加一个gorm.deletedat 字段

    type User struct {
        Deleted  gorm.Deltedat    //或者使用gorm.Model,因为这个模型已经包含了这个字段
        ID   uint   `gorm:primarykey`
        Name string `gorm:"default:galeone"`
        Age  uint   `gorm:"default:18"`
        }
    
    // DELETE FROM users WHERE id = 10;
    db.Delete(&User{},10)
    // DELETE FROM users WHERE id IN (1,2,3);
    db.Delete(&User{},[]int{1,2,3})
    // UPDATE users SET deleted_at="2013-10-29 10:23" WHERE id = 111;
    db.Delete(&User{ID :1})
    // UPDATE users SET deleted_at="2013-10-29 10:23" WHERE age = 20;
    db.Where("age = 20").Delete(&User{})
    

事务

为什么要使用事务?

事务是为了保证数据一致性,多个操作要具有原子性,要不都成功,要不都不成功,事务中的操作不能割裂开来

为什么要使用transaction?

在设置事务时,如果操作出错需要事务回滚,如果操作都没问题,进行事务提交,但是在开发过程中,不可避免会忘记写回滚和提交的代码

因此使用transaction能够自动提交事务,避免用户漏泄,很好地解决这一问题

1674894778675.png

//函数返回的错误给到err,然后再对err进行if判断
if err = tx.Create(&User{Name:"name"}).Error; err != nil{
    return err  return nil
}
//利用db.Transaction给err赋值,如果里面的事务操作报错,则给error返回的不是nil,那么自动进行回滚,外层的err也不是nil;如果事务操作正常,则给error返回nil,事务自动提交,外层err为nil
if err = db.Transaction(func(tx *gorm.DB) error{
    
});err != nil

性能优化

使用PrepareStmt来进行性能优化

其他的一些性能优化方案可以阅读官方文档性能优化方案性能 | GORM - The fantastic ORM library for Golang, aims to be developer friendly.

Gorm生态

Gorm拥有非常丰富的生态,比如代码生成工具、分片库方案、手动索引、乐观锁、读写分离等等