【Gorm】如何通过Gorm实现对数据库操作

时间:2025-02-28 08:20:41

文章目录

  • 教程-基本操作
    • 1、链式操作
      • 1.1、链式操作
      • 1.2、创建方法
      • 1.3、Scopes方法
      • 1.4、多个创建方法
      • 1.5、线程安全
    • 2、错误处理
      • 2.1、错误处理
      • 2.2、错误
      • 2.3、RecordNotFound错误
    • 3、钩子
      • 3.1、对象的生命周期
      • 3.2、钩子
      • 3.3、更新一个对象
      • 3.4、删除一个对象
      • 3.5、查询一个对象
    • 4、事务
      • 4.1、事务
      • 4.2、具体例子
    • 5、数据库迁移
      • 5.1、自动迁移
      • 5.2、其他数据库迁移宫剧
      • 5.3、表结构的方法
    • 6、原生SQL和SQL生成器
      • 6.1、运行原生SQL
    • 7、通用数据接口
    • 8、复合主键
    • 9、自定义Logger
      • 9.1、Logger
      • 9.2、自定义Logger

教程-基本操作

1、链式操作

1.1、链式操作

​        Gorm继承了链式操作接口,可以像拼接似的进行代码的书写,如下:

db, err := gorm.Open("postgres", "user=gorm dbname gomr sslmode=disable")

// 创建一个新的关系
tx := db.Where("name = ?", "zhihui")

// 新增更多的筛选条件
if someCondition {
  tx = tx.Where("age = ?", 22)
} else {
  tx = tx.Where("age = ?", 18)
}

if yetAnotherCondition {
  tx = tx.Where("active = ?", 1)
}

​​        这样的写法,直到调用立即方法之前都不会产生查询,在某些场景下会十分方便实用,就像我们可以封装一个包来处理常见的逻辑一样!

1.2、创建方法

​​        创建方法就是那些会产生SQL查询并且发送到数据库,通常就是一些CRUD方法,就像:Create、First、Find、Take、Save、UpdateXXX、Delete、Scan、Row、Rows...

在下方,我们通过创建方法的举一个例子:

tx.Find(&user)  // 相当于生成: select * from user where name = "zhihui" and age = 18 and active = 1;

1.3、Scopes方法

​​        Scope方法是基于链式操作理论创建的,使用其可以提取一些通用逻辑,创建一些实用性更强的库。

func PaidWithCreditCard(db *gorm.DB) *gorm.DB {
  return db.Where("pay_mode_sign = ?", "C")
}

func PaidWithCreditCard(db *gorm.DB) *gorm.DB {
    return db.Where("pay_mode_sign = ?", "C")
}

func PaidWithCod(db *gorm.DB) *gorm.DB {
    return db.Where("pay_mode_sign = ?", "C")
}

func OrderStatus(status []string) func (db *gorm.DB) *gorm.DB {
    return func (db *gorm.DB) *gorm.DB {
        return db.Scopes(AmountGreaterThan1000).Where("status in (?)", status)
    }
}

// ----------------------具体实操-------------------------- //

db.Scopes(AmountGreaterThan1000, PaidWithCreditCard).Find(&orders)
// 查找所有大于1000的信用卡订单和金额

db.Scopes(AmountGreaterThan1000, PaidWithCod).Find(&orders)
// 查找所有大于1000的 COD 订单和金额

db.Scopes(AmountGreaterThan1000, OrderStatus([]string{"paid", "shipped"})).Find(&orders)
// 查找大于1000的所有付费和运单

1.4、多个创建方法

​​        当使用Gorm的创建方法,后面创建的方法将复用前面的创建方法的搜索条件(不包含内联条件)

db.Where("name like ?", "zhihui%").Find(&user, "id in (?)", []int{1, 2, 3}).Count(&count)

​​        相当于执行如下的SQL操作:

select * from users where name like "zhihui%" and id in (1, 2, 3)
select count(*) from users where name like "zhihui%"

1.5、线程安全

​​        所有的链式操作都将会克隆并创建一个新的数据库对象(共享一个连接池),Gorm对于多个goroutines的并发使用是安全的。

2、错误处理

​​        在Go语言中,错误处理是尤为重要的,鼓励人们在任何创建方法之后去检查错误!

2.1、错误处理

​​        由于Gorm的链式API,GORM中的错误处理与惯用的Go代码不同,但它仍然相当容易 => 如果发生任何错误,Gorm会将其设置为*Error字段,你可以在这样检查:

if err := db.Where("name = ?", "zhihui").First(&user).Error; err != nil {
  // error handing...
}

或者

if result := db.Where("name = ?", "zhihui").First(&user); result.Error != nil {
  // error handing...
}

2.2、错误

​​        在处理数据期间,发生几个错误很普遍,Gorm提供了一个API来讲所有发生的错误作为切片返回!

// 多个错误产生,GetErrors 返回一个 []errors 切片
db.First(&user).Limit(10).Find(&users).GetErrors()

fmt.Println(len(errors))

for _, err := range errors {
  fmt.Println(err)
}

2.3、RecordNotFound错误

​​        Gorm提供了一个处理RecordNotFound错误的快捷方法,如果发生错误,它将检查每个错误,如果他们中的任何一个是RecordNotFound错误。

// 检查是否返回RecordNotFound错误
db.Where("name = ?", "hello world").First(&user).RecordNotFound()

if db.Model(&user).Related(&credit_card).RecordNotFount() {
  // 数据没有找到
}

if err := db.Where("name = ?", "zhihui").First(&user).Error; gorm.IsRecordNotFoundError(err) {
  // 数据没有找到
}
在 GORM 中,如果你使用 First 或 Find 等方法查询记录,当没有找到对应记录时,GORM 将返回一个 RecordNotFound 错误。
为了处理这种情况,我们可以使用 RecordNotFound 方法来检查是否返回了这个错误。

在以上代码片段中:
1、第一个示例演示了如何使用 RecordNotFound 方法来检查是否返回了 RecordNotFound 错误。
2、第二个示例中,我们使用 Related 方法来检索关联记录,并使用 RecordNotFound 方法检查是否找到了记录。
3、最后一个示例展示了如何使用 gorm.IsRecordNotFoundError 函数来检查 First 或 Find 方法是否返回了 RecordNotFound 错误。

3、钩子

3.1、对象的生命周期

​​        钩子是一个在 插入/查询/更新/删除 之前或之后被调用的方法。

​​        如果在一个模型中定义了特殊的方法,它将会在插入、查询、更新、删除的时候被自动调用,如果任何的回调抛出错误,Gorm将会停止将要的执行的操作并且回滚当前的改变。

3.2、钩子

1、创建一个对象

// 开启事务
BeforeSave
BeforeCreate
// 连表前的创建
// 更新时间戳 CreateAt、UpdateAt
// 保存自己本身
// 重载那些有默认值和空的字段
// 连表后的操作
AfterCreate
AfterSave
// 提交或回滚事务

2、例子

func (u *User) BeforeSave() (err error) {
  if u.IsVaild() {
    err = error.New("can't save invalid date")
  }
  return
}

func (u *User) AfterCrete(scope *gorm.Scope) (er error) {
  if u.ID == 1 {
    scope.DB.Model(u).Update("role", "admin")
  }
  return 
}
这是两个 GORM 钩子函数的例子。
1、BeforeSave 是一个 GORM 钩子函数,它在保存之前对数据进行处理。在这个例子中,它检查了用户的数据是否有效,如果无效就返回错误。
2、AfterCreate 是另一个 GORM 钩子函数,它在创建完数据之后对数据进行处理。在这个例子中,它检查了用户是否为第一个创建的用户,如果是的话就将其角色设置为 "admin"。

这两个钩子函数都是在模型方法中定义的,并且它们的名称必须符合 GORM 的命名规则,以便 GORM 可以正确地调用它们。

3、注意事项

​​        在Gorm中 保存/删除 操作会默认进行事务处理,所有在事务中,所有的改变都是无效的,直到它被提交为止!

func (u *User) AfterCreate(tx *gorm.DB) (err error) {
  tx.Model(u).Update("role", "admin")
  return
}
这段代码是在 GORM 中使用事务处理的一个示例。在 GORM 中,当进行保存/删除操作时,默认会启动事务处理。因此,在使用钩子函数进行操作时,需要将 gorm.DB 对象作为参数传递给钩子函数,以便在事务中进行操作。

在上面的示例中,AfterCreate 钩子函数会在保存 User 对象后被调用,并且会将保存的对象作为参数传递给它。在这个钩子函数中,我们使用了传递的 tx 对象来更新 User 对象的角色为 "admin",并且没有返回任何错误。在事务中的任何操作都需要使用传递的 tx 对象进行操作。

需要注意的是,在钩子函数中进行操作时,一定要小心不要引起死锁或无限循环的问题。此外,如果在事务中操作失败,应该回滚事务并返回相应的错误。

3.3、更新一个对象

// 开启事务
BeforeSave
BeforeUpdate
// 连表前的保存
// 更新时间戳UpdateAt
// 保存自己本身
// 连表后的操作
AfterUpdate
AfterSave
// 提交或回滚事

1、示例:

func (u *User) BeforeUpdate() (err error) {
  if u.readonly() {
    err = error.New("read only user")
  }
  return
}

// 在事务结束后,更新数据
func (u *User) AfterUpdate(tx *gorm.DB) (err error) {
  if u.Confirmed {
    tx.Model(&Address{}).Where("user_id = ?", u.ID).Update("verfied", true)
  }
  return
}
1、这段代码实现了在 GORM 中使用钩子函数控制 更新 操作。在 BeforeUpdate 钩子函数中,会检查是否是只读用户,如果是,则会返回一个错误,导致更新操作失败。
2、在 AfterUpdate 钩子函数中,会检查用户是否已经被确认,如果是,则会将与该用户相关联的地址信息标记为已验证。
需要注意的是,在 AfterUpdate 钩子函数中使用了 tx 参数来执行数据更新操作,这是因为在钩子函数中无法直接访问数据库连接对象,需要通过参数传递进来。

3.4、删除一个对象

// 开启事务
BeforeDelete
// 删除自身
AfterDelete
// 提交或回滚事务

1、示例:

// 在事务结束后进行更新操作
func (u *User) AfterDelete(tx *gorm.DB) (err error) {
  if u.Confirmed {
    tx.Model(&Address{}).Where("user_id = ?", u.ID).Update("invaild", false)
  }
  return
}

3.5、查询一个对象

// 从数据库中读取数据
// 加载之前
AfterFind

1、示例

func (u *User) AfterFind() (err error) {
  if u.MemberShip == "" {
    u.MemberShip = "user"
  }
  return
}

4、事务

​​        Gorm中默认在事务中执行单个create、update、delete操作,以确保数据库完整性,如果想把多个create、update、delete当成一个原子性操作,那么trnsaction就是为了这个而创建的!

4.1、事务

​​        要在事务中执行一组操作,正常的流程如下所示:

// 开启事务
tx := db.Begin()

// 在事务中执行一些数据库操作(从这里开始使用‘tx’,而不是‘db’)
tx.Create(...)

// ...

// 发生错误回滚事务
tx.Rollback()

// 或者提交这个事务
tx.Commit()

4.2、具体例子

func CreateAnimals(db *gorm.DB) err {
  // 注意在事务中要使用 tx 作为数据库句柄
  tx := db.Begin()
  defer func() {
    if r := recover(), r != nil {
      tx.Rollback()
    }
  }()
  
  if tx.Error != nil {
    return err
  }
  
  if err := tx.Create(&Animal{Name: "Giraffe"}).Error; err != nil {
    tx.Rollback()
    return err
  }
  
  if err := tx.Create(&Animals{Name:"Lion"}).Error; err != nil {
    tx.Rollback()
    return err
  }
}
以上代码是一个创建动物的函数,采用了 GORM 中的事务机制,确保在创建动物时,要么全部创建成功,要么全部失败。

这个函数中采用了 db.Begin() 方法创建事务,如果在函数执行过程中出现了 panic 异常,会调用 defer 语句中的 tx.Rollback() 方法,撤销当前事务。如果在函数执行过程中没有出现异常,tx.Commit() 方法会提交事务。

在函数的主体中,使用了 tx.Create() 方法创建动物,如果出现错误,tx.Rollback() 方法会撤销当前事务。

5、数据库迁移

5.1、自动迁移

​​        使用migrate来维持表结构一直处于最新状态(migrate仅支持创建表、添加表中没有的字段和索引),为了保护数据,它不支持改变已有的字段类型或删除未被使用的字段!

db.AutoMigrate(&User{})

db.AutoMigrate(&User{}, &Product{}, &Order{})

// 创建表的时候,添加表的后缀
db.Set("gorm:table_options", "ENGINE=InnoDB").AutoMigrate(&User{})
这些代码是用于在 GORM 中执行数据库迁移的。GORM 支持通过代码自动创建表格并同步数据库模型。其中:

1、db.AutoMigrate(&User{}) 创建名为 "users" 的表格,该表格包含模型 "User" 中定义的所有字段,如果表格不存在,则创建表格。如果模型中有新增的字段,则自动同步到表格中。

2、db.AutoMigrate(&User{}, &Product{}, &Order{}) 与上面的语句类似,但是在一个语句中可以创建多个表格。

3、db.Set("gorm:table_options", "ENGINE=InnoDB").AutoMigrate(&User{}) 在创建表格时可以设置表格选项。在这个例子中,表格选项是 "ENGINE=InnoDB",意思是使用 InnoDB 存储引擎来创建表格。

这些代码主要用于将 GORM 模型映射到数据库表格中。通过这些语句,我们可以自动创建数据库表格,并将它们与我们的 GORM 模型同步。这样,我们就可以方便地使用 GORM 进行数据库操作。

5.2、其他数据库迁移宫剧

​​        Gorm的数据库迁移工具能够支持主要的数据库,但如果要寻找更多的迁移工具,Gorm会提供数据库结构,可能会有所帮助。

// 返回 *
db.DB()

5.3、表结构的方法

1、Has Table

// 检查模型中 User 表是否存在
db.HasTable(&user{})

// 检查users表是否存在
db.HasTable("users")

2、Create Table

// 通过模型 User 创建表
db.CreateTable(&user{})

// 在创建users表的时候,会在SQL语句中拼接上`ENGINE=InnoDB`
db.Set("gorm:table_options", "ENGINE=InnoDB").CreateTable(&User{})

3、Drop table

// 删除模型 User 表
(&User{})

// 删除 users 表
("users")

// 删除模型 User 表和 products 表
(&User{}, "products")

4、ModifyColumn

以给定的值来定义字段类型

// User 模型,改变 description 字段的数据类型为 `text`
(&User{}).ModifyColumn("description", "text")

5、DropColumn

// User 模型,删除  description 字段
(&User{}).DropColumn("description")

6、Add Indexes

// 为 `name` 字段建立一个名叫 `idx_user_name` 的索引
(&User{}).AddIndex("idx_user_name", "name")

// 为 `name`, `age` 字段建立一个名叫 `idx_user_name_age` 的索引
(&User{}).AddIndex("idx_user_name_age", "name", "age")

// 添加一条唯一索引
(&User{}).AddUniqueIndex("idx_user_name", "name")

// 为多个字段添加唯一索引
(&User{}).AddUniqueIndex("idx_user_name_age", "name", "age")

7、Remove Index

// 移除索引
(&User{}).RemoveIndex("idx_user_name")

8、Add Foreign Key

// 添加主键
// 第一个参数 : 主键的字段
// 第二个参数 : 目标表的 ID
// 第三个参数 : ONDELETE
// 第四个参数 : ONUPDATE
(&User{}).AddForeignKey("city_id", "cities(id)", "RESTRICT", "RESTRICT")

9、Remove ForeignKey

(&User{}).RemoveForeignKey("city_id", "cities(id)")

6、原生SQL和SQL生成器

6.1、运行原生SQL

​​        执行原生SQL的时候不能通过链式调用其他方法:

db.Exec("drop table users;")
db.Exec("update orders set shipped_at = ? where id in (?)", time.Now(), []int64{11, 22, 33})

//--------------------分割--------------------//
// Scan
type Result struct {
    Name string
    Age  int
}

var result Result
db.Raw("SELECT name, age FROM users WHERE name = ?", 3).Scan(&result)


这段代码使用了 GORM 的 Raw 方法,可以直接执行 SQL 命令。Scan 方法将查询结果扫描到指定的变量中,以便后续使用。

具体来说,上面的代码执行了一个查询语句,查询名字为 3 的用户的姓名和年龄。查询结果扫描到了 Result 结构体变量 result 中。Result 结构体定义了两个字段 Name 和 Age,对应查询结果中的两个字段。

需要注意的是,Scan 方法的参数是一个指针,指向需要扫描的结果变量。如果查询结果不为空,Scan 方法会将结果扫描到这个变量中。如果查询结果为空,则不会进行任何操作。

1、 和

​​        使用* 或者 *获得查询结果

row := db.Table("users").Where("name = ?", "zhihui").Select("name, age").Row() // *
row.Scan(&name, &age)

rows, err := db.Model(&User{}).Where("name = ?", "zhihui").Select("name, age, email").Rows() // (*, error)
defer rows.Close()
for rows.Next() {
    ...
    rows.Scan(&name, &age, &email)
    ...
}

// 原生SQL
rows, err := db.Raw("select name, age, email from users where name = ?", "zhihui").Rows() // (*, error)
defer rows.Close()
for rows.Next() {
    ...
    rows.Scan(&name, &age, &email)
    ...
}
这段代码演示了如何使用Gorm查询数据,并使用Scan方法将查询结果映射到结构体中,或使用原生SQL查询数据。

首先,我们定义了一个Result结构体来存储查询结果,它包括Name和Age两个字段。`接着,使用Raw方法来执行原生SQL查询,将查询结果使用Scan方法映射到Result结构体中。`

此外,还演示了如何使用Model方法和Rows方法来查询数据。Model方法指定要查询的模型,Where方法指定查询条件,Select方法指定要查询的字段,并使用Rows方法执行查询并返回一个结果集。

无论是使用Rows方法还是使用Raw方法,都需要在查询完毕后关闭结果集。在Rows方法中,我们使用defer语句来确保在函数结束前关闭结果集。在使用Raw方法时,也应该在处理完查询结果后及时关闭结果集。

总之,这些演示了Gorm中常用的查询方法,包括使用结构体映射查询结果和使用原生SQL查询数据。这些方法可以大大简化数据库操作,提高开发效率。

2、扫描数据到模型

rows, err := db.Model(&User{}).Where("name = ?", "zhihui").Select("name, age, email").Rows() // (*, error)
defer rows.Close()

for rows.Next() {
  var user User
  // ScanRows 扫描一行到 user 模型
  db.ScanRows(rows, &user)

  // do something
}

7、通用数据接口

​​        Gorm提供了当前连接中返回通用的数据库接口方法DB(如果底层数据库连接不是*,就像在事务中,他将返回nil)

// 获取数据库对象  来使用他的 () 方法
// ping
db.DB().Ping()

1、连接池

// SetMaxIdleConns 设置空闲连接池中的最大连接数。
db.DB().SetMaxIdleConns(10)

// SetMaxOpenConns 设置数据库连接最大打开数。
db.DB().SetMaxOpenConns(100)

// SetConnMaxLifetime 设置可重用连接的最长时间
db.DB().SetConnMaxLifetime(time.Hour)

8、复合主键

​​        通过设置多个字段为主键,开启复合主键功能。

type Product struct {
  ID string gorm:"primary_key"
  LanguageCode string gorm:"primary_key"
  Code string
  Name string
}

9、自定义Logger

9.1、Logger

​​        Gorm建立了对Logger的支持,默认模式只会在错误发生的时候打印日志。

// 开启Logger,以展示详细的日志
db.Logger(ture)

// 关闭Logger
db.Logger(false)

// 对某个操作展示详细的日志,用来排查该操作的问题
db.Debug().Where("name = ?", "zhihui").First(&User{})

9.2、自定义Logger

​​        例如使用Revel的Logger作为Gorm的输出

db.SetLogger(gorm.Logger{revel.TRACE})

​​        使用作为输出

db.SetLogger(log.New(os.Stdout, "\r\n", 0))

至此,我们完成了Gorm部分的学习,能够大致明白,如何通过Gorm来实现对数据库的操作,简化我们开发,提高开发效率。当然需要我们进一步在实际开发过程中去实践,才能更好的将这些知识转变为自己的东西!