GORM 学习实践笔记 | 青训营

118 阅读15分钟

1 GORM简介

1.1 什么是GORM?

The fantastic ORM library for Golang aims to be developer friendly.——官网

Gorm是一个Go语言的ORM(对象关系映射)库,全称为Go Object Relational Mapping。它提供了一种简单、灵活的方式来操作数据库,使得开发者可以通过面向对象的方式来操作数据库,而不需要直接编写SQL语句。

那么什么是ORM?

对象-关系映射(ORM),是随着面向对象的软件开发方法发展而产生的。它的作用是将对象模型映射到关系模型数据库结构中,从而实现对象与数据库之间的无缝交互。举例来说就是,我定义一个对象,那就对应着一张表,这个对象的实例,就对应着表中的一条记录。

1.2 GORM的特性

以下是GORM的一些主要特性:

  1. 全功能ORM: GORM提供了一个全功能的ORM框架,可以通过结构体和标签定义数据库表和字段,并提供了丰富的API来进行数据库操作。
  2. 关联: GORM支持多种关联关系,包括Has One、Has Many、Belongs To、Many To Many、多态和单表继承等。通过定义关联关系,可以方便地进行跨表查询和操作。
  3. 钩子方法: GORM支持在Create、Save、Update、Delete和Find等操作前后执行自定义的钩子方法,可以在这些钩子方法中添加额外的逻辑。
  4. 预加载: GORM支持使用Preload和Joins方法进行预加载,可以在查询数据时一次性加载关联表的数据,提高查询效率。
  5. 事务支持: GORM支持事务操作,可以保证多个数据库操作的原子性。还支持嵌套事务、Save Point和Rollback To Saved Point等高级事务功能。
  6. 上下文和预编译模式: GORM支持使用上下文和预编译模式,可以在数据库操作中传递上下文信息,并使用预编译SQL语句提高性能。
  7. 批量插入和批量查询: GORM支持批量插入数据和批量查询数据的操作,可以提高数据库操作的效率。
  8. SQL构建器和命名参数: GORM提供了SQL构建器和命名参数的功能,可以方便地构建复杂的SQL查询语句。
  9. Auto Migration: GORM提供了Auto Migration功能,可以根据定义的结构体自动创建或更新数据库表结构。
  10. 自定义Logger: GORM允许开发者自定义日志记录器,可以根据需要记录和处理数据库操作的日志。
  11. 可扩展插件API: GORM提供了灵活的可扩展插件API,可以自定义插件来实现不同的功能,如多数据库支持、读写分离、Prometheus监控等。
  12. 经过测试的重重考验: GORM经过了大量的测试和实践验证,保证了其稳定性和可靠性。
  13. 开发者友好: GORM具有简洁、直观的API设计,易于使用和理解,使得开发者可以快速上手并进行数据库操作。

总的来说,GORM是一个功能丰富、稳定可靠的ORM库,提供了许多方便的特性和功能,可以大大简化和加速开发者对数据库的操作。

1.3 安装依赖

 go get -u gorm.io/gorm
 go get -u gorm.io/driver/sqlite

2 模型

模型是标准的 struct,由 Go 的基本数据类型、实现了 ScannerValuer 接口的自定义类型及其指针或别名组成。 ——官网

2.1 约定

GORM 倾向于约定优于配置。默认情况下GORM 使用 ID 作为主键,使用结构体名的蛇形复数作为表名,字段名的蛇形作为列名,并使用 CreatedAtUpdatedAt 字段追踪创建、更新时间。如果遵循 GORM 的约定,就可以少写一些配置和代码。如果约定不符合实际要求,可以进行自定义配置。

假设有以下结构体定义:

 goCopy codepackage main
 ​
 import (
     "time"
     "gorm.io/gorm"
 )
 ​
 type ProductTest struct {
     ID int
     CodeTest  string
     NameTest int
 }

在上面的示例中,GORM 会根据约定自动进行以下映射:

  • 表名:使用结构体名的蛇形复数,因此表名为 product_tests
  • 列名:使用字段名的蛇形,例如 CodeTest 字段对应的列名为 code_test
  • 主键:默认使用名为 ID 的字段作为主键。

这意味着在大多数情况下,不需要显式地为每个模型指定表名、列名、主键等信息,只需要遵循约定即可。

2.2 自定义配置

2.2.1 自定义配置主键

我们可以通过标签 primaryKey 将其它字段设为主键

 // 将 `UUID` 设为主键
 type Animal struct {
   UUID   string `gorm:"primaryKey"`
   Name   string
   Age    int64
 }

此外我们还可以通过将多个字段设为主键,以创建复合主键,例如:

 // 将 `UUID` 设为主键
 type Animal struct {
   ID     int64
   UUID   string `gorm:"primaryKey"`
   Name   string `gorm:"primaryKey"`
   Age    int64
 }

2.2.2 自定义配置表名

我们可以实现对应结构体的TableName 方法来更改默认表名,例如:

 type User struct{
     ID int64
     Name string
 }
 // TableName 会将 User 的表名重写为 `profiles`
 func (User) TableName() string {
   return "profiles"
 }

2.2.3 临时指定表名

我们可以使用 Table 方法临时指定表名,例如:

 // 根据 User 的字段创建 `deleted_users` 表
 db.Table("deleted_users").AutoMigrate(&User{})

在这里,我们使用 Table("deleted_users") 指定了表名为 "deleted_users",然后使用 AutoMigrate 方法创建了与 User 结构体对应的 deleted_users 表。这意味着在 "deleted_users" 表中,将使用与 User 结构体相匹配的字段来创建表结构。

 // 从另一张表查询数据
 var deletedUsers []User
 db.Table("deleted_users").Find(&deletedUsers)
 // SELECT * FROM deleted_users;

在这个部分,我们使用 Table("deleted_users")"deleted_users" 表中查询数据并存储到 deletedUsers 切片中。GORM 将生成一个对应的 SELECT 查询,从 "deleted_users" 表中检索所有数据。

 db.Table("deleted_users").Where("name = ?", "jinzhu").Delete(&User{})
 // DELETE FROM deleted_users WHERE name = 'jinzhu';

在这个示例中,我们使用 Table("deleted_users") 删除了 "deleted_users" 表中 name 字段为 "jinzhu" 的记录。GORM 将生成一个对应的 DELETE 查询,从 "deleted_users" 表中删除符合条件的记录。

总之,Table 方法允许我们在特定的数据库操作中指定表名,从而可以针对不同的表执行操作,而不会影响全局的表名映射规则。这对于需要对不同表进行操作的情况非常有用,例如从不同的数据表中读取数据或执行删除操作。

2.2.4 自定义配置列名

我们可以使用 column 标签或 命名策略 来覆盖列名

type Animal struct {
  AnimalID int64     `gorm:"column:beast_id"`         // 将列名设为 `beast_id`
  Birthday time.Time `gorm:"column:day_of_the_beast"` // 将列名设为 `day_of_the_beast`
  Age      int64     `gorm:"column:age_of_the_beast"` // 将列名设为 `age_of_the_beast`
}

2.3 时间戳追踪

在 GORM 中,时间戳追踪是自动管理表中的 CreatedAtUpdatedAt 字段的功能。

下面是如何使用时间戳追踪以及如何手动设置和更新 CreatedAt 字段的示例。

2.3.1 CreatedAt

对于有 CreatedAt 字段的模型,创建记录时,如果该字段值为零值,则将该字段的值设为当前时间

// 使用时间戳追踪,在创建记录时,`CreatedAt` 字段会被设置为当前时间
db.Create(&user)

// 另一种方式,手动指定 `CreatedAt` 字段的值,GORM 不会修改它
user2 := User{Name: "jinzhu", CreatedAt: time.Now()}
db.Create(&user2)

// 如果需要修改 `CreatedAt` 字段的值,可以使用 `Update` 方法
db.Model(&user).Update("CreatedAt", time.Now())

我们可以通过将 autoCreateTime 标签置为 false 来禁用时间戳追踪,例如:

type User struct {
  CreatedAt time.Time `gorm:"autoCreateTime:false"`
}

这意味着GORM将不会自动设置该字段的值,而是需要我们手动指定

2.3.2 UpdatedAt

对于有 UpdatedAt 字段的模型,更新记录时,将该字段的值设为当前时间。创建记录时,如果该字段值为零值,则将该字段的值设为当前时间

// 使用 `Save` 方法更新记录,会将 `UpdatedAt` 字段设为当前时间
db.Save(&user)

// 使用 `Update` 方法更新记录,会将 `UpdatedAt` 字段设为当前时间
db.Model(&user).Update("name", "jinzhu")

// 使用 `UpdateColumn` 方法更新记录,`UpdatedAt` 字段不会被修改
db.Model(&user).UpdateColumn("name", "jinzhu")

// 创建记录时,手动指定 `UpdatedAt` 字段的值,GORM 不会对其进行修改
user2 := User{Name: "jinzhu", UpdatedAt: time.Now()}
db.Create(&user2)

// 更新记录时,手动指定 `UpdatedAt` 字段的值,GORM 会将其更新为当前时间
user3 := User{Name: "jinzhu", UpdatedAt: time.Now()}
db.Save(&user3)

你可以通过将 autoUpdateTime 标签置为 false 来禁用时间戳追踪,例如:

type User struct {
  UpdatedAt time.Time `gorm:"autoUpdateTime:false"`
}

注意 GORM 支持拥有多种类型的时间追踪字段。可以根据 UNIX(毫/纳)秒,查看 Model 获取详情

2.4 gorm.Model

gorm.Model 是 GORM 提供的一个预定义的结构体,它包含了一些常用的字段,用于帮助我们更方便地管理数据库记录的创建、更新和软删除。我们可以将 gorm.Model 结构体嵌入到我们的自定义结构体中,以便从中继承这些字段和功能。

以下是关于 gorm.Model 的解释:

goCopy code// gorm.Model 的定义
type Model struct {
  ID        uint           `gorm:"primaryKey"`    // 主键字段
  CreatedAt time.Time                              // 创建时间字段
  UpdatedAt time.Time                              // 更新时间字段
  DeletedAt gorm.DeletedAt `gorm:"index"`          // 软删除时间字段
}
  • ID 字段是主键字段,用于唯一标识每条记录。
  • CreatedAt 字段记录了记录的创建时间。
  • UpdatedAt 字段记录了记录的最近更新时间。
  • DeletedAt 字段是用于软删除的时间戳字段。当您删除一条记录时,GORM 不会真正删除该记录,而是在 DeletedAt 字段中记录删除的时间。这样做的好处是,您可以随时恢复被删除的记录,或者根据业务需要执行永久删除。

通过将 gorm.Model 嵌入到我们的自定义结构体中,我们可以从中继承这些字段,而不必重复定义。以下是一个示例:

goCopy codeimport (
    "gorm.io/gorm"
    "time"
)

type User struct {
    gorm.Model      // 嵌入 gorm.Model 结构体,继承其中的字段和功能
    Name     string
    Age      int
}

在上述示例中,User 结构体嵌入了 gorm.Model,因此会继承其中的 IDCreatedAtUpdatedAtDeletedAt 字段,使我们能够更方便地进行数据库操作,包括记录的创建、更新和软删除。

3 连接数据库

GORM 官方支持的数据库类型有:MySQL, PostgreSQL, SQLite, SQL Server 和 TiDB

连接 MySQL 数据库使用 GORM 非常简单,我们只需要导入适当的数据库驱动程序和 GORM 包,并提供正确的连接信息。 以下是一个使用 GORM 连接 MySQL 数据库的示例:

  1. 导入包:首先,您需要导入 GORM 和 MySQL 驱动程序的包。
import (
    "gorm.io/gorm"
    "gorm.io/driver/mysql"
)
  1. 配置连接信息:您需要提供正确的 MySQL 连接信息,包括用户名、密码、主机、端口和数据库名等。将这些信息拼接成连接字符串。
dsn := "user:password@tcp(host:port)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
  1. 连接数据库:使用 gorm.Open 方法连接到 MySQL 数据库。这将返回一个 *gorm.DB 对象,我们将使用它来执行数据库操作。
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
    panic("failed to connect to database")
}

完整的连接 MySQL 数据库的示例代码如下:

goCopy codepackage main

import (
    "gorm.io/gorm"
    "gorm.io/driver/mysql"
)

func main() {
    // 配置数据库连接字符串
    dsn := "user:password@tcp(host:port)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
    
    // 打开数据库连接
    db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
    if err != nil {
        panic("failed to connect to database")
    }
    // 执行数据库操作
    //...
}

在上述示例中,将 userpasswordhostportdbname 替换为我们的 MySQL 数据库的实际连接信息。一旦成功连接到数据库,就可以使用 db 对象执行各种数据库操作,如创建表、插入数据、查询数据等。

4 CURD接口

4.1 什么是CRUD?

"CRUD" 是一个首字母缩写词,代表了数据库操作的四个基本操作:Create(创建)、Read(读取)、Update(更新)和Delete(删除)。这些操作是在数据库管理中非常常见的,用于对数据进行各种操作和管理。

具体解释如下:

  1. Create(创建) :将新的数据记录插入到数据库中,创建新的数据行。
  2. Read(读取) :从数据库中检索数据记录,获取已存在的数据行。
  3. Update(更新) :修改数据库中已存在的数据记录,更新数据行的内容。
  4. Delete(删除) :从数据库中移除数据记录,删除数据行。

这四个操作通常是数据库管理和应用程序开发中的基本需求,几乎所有的应用程序都需要能够执行这些操作来对数据进行增删改查。在关系型数据库中,这些操作对应了 SQL 语言中的 INSERT、SELECT、UPDATE 和 DELETE 语句。

4.2 Create(创建)

4.2.1 创建一条记录

示例:

// 结构体
type User struct{
    gorm.Model
    Name string
    Age int
    Birthday time.Time
}

user := User{Name: "Jinzhu", Age: 18, Birthday: time.Now()}

result := db.Create(&user) // 通过数据的指针来创建

fmt.println(user.ID)             // 返回插入数据的主键
fmt.println(result.Error)        // 返回 error
fmt.println(result.RowsAffected) // 返回插入记录的条数

上面代码中,使用 db.Create(&user) 语句来通过数据的指针创建数据库记录。&user 表示传递了 user 变量的指针给 Create 方法,这是因为 Create 方法需要一个指向数据的指针,以便在数据库中创建相应的记录。

resultCreate 方法的返回值,它包含了一些有用的信息。下面是对代码中各部分的解释:

  • user.ID: 在成功插入数据后,这个字段会被数据库赋予自动生成的主键值。
  • result.Error: 如果插入过程中发生了错误,这个字段会包含错误信息;如果没有错误,它会是 nil
  • result.RowsAffected: 这个字段包含插入记录的条数,即受影响的行数。

4.2.2 创建多条记录

users := []User{
    User{Name: "Jinzhu", Age: 18, Birthday: time.Now()},
    User{Name: "Jackson", Age: 19, Birthday: time.Now()},
}
result := db.Create(&users) 

这段代码演示了如何通过传递一个切片的指针来一次性插入多条数据库记录。

4.2.3 创建记录并为指定的字段分配值

db.Select("Name", "Age", "CreatedAt").Create(&user)
// INSERT INTO `users` (`name`,`age`,`created_at`) VALUES ("jinzhu", 18, "2020-07-04 11:05:21.775")

其中Select中的选择的字段为结构体中的名字或数据库中表中的字段名都可以。

4.2.4 创建记录并忽略要省略的传递字段的值

db.Omit("Name", "Age", "CreatedAt").Create(&user)
// INSERT INTO `users` (`birthday`,`updated_at`) VALUES ("2020-01-01 00:00:00.000", "2020-07-04 11:05:21.775")

4.2.5 创建钩子

创建钩子是指在创建记录时执行的自定义函数。在 GORM 中,你可以通过在你的模型结构体中定义特定的方法来实现这些钩子。这些方法将在插入记录之前或之后被自动调用,让你有机会执行一些自定义的逻辑。GORM 允许用户定义的钩子有 BeforeSave, BeforeCreate, AfterSave, AfterCreate

一个常见的用法是在创建记录之前生成一个唯一的标识,例如 UUID。你可以通过在模型结构体中实现 BeforeCreate 方法来完成这个操作,如下所示:

goCopy codefunc (u *User) BeforeCreate(tx *gorm.DB) (err error) {
  u.UUID = uuid.New().String()
  return
}

这个 BeforeCreate 方法将在插入记录之前被调用,你可以在其中进行一些自定义的操作。在这个例子中,每次插入记录之前都会为 UUID 字段生成一个新的唯一标识。

4.2.6 根据 Map 创建

在 GORM 中,你可以使用 Map 来创建数据库记录,这在一些场景下非常有用。你可以通过传递一个包含字段名和对应值的 Map 来实现,如下所示:

db.Model(&User{}).Create(map[string]interface{}{
  "Name": "jinzhu",
  "Age": 18,
})

这个方法在一些临时的操作中很有用,但需要注意的是,使用 Map 创建记录时,钩子方法不会被调用,关联也不会被保存,且主键值不会被填充。

当涉及到GORM的操作,读取(Read)、更新(Update)和删除(Delete)是非常常见且重要的操作。下面我将详细解释每种操作并举例说明:

4.3 Read(读取)

在GORM中,读取操作是从数据库中检索数据的过程。以下是一些常见的读取操作以及相应的示例:

  • Find

Find 方法用于根据条件查询多条记录。例如:

var users []User
db.Find(&users, "age > ?", 20)
  • First

First 方法用于根据条件查询第一条记录。例如:

var user User
db.First(&user, "name = ?", "Alice")
  • Last

Last 方法用于根据条件查询最后一条记录。例如:

var user User
db.Last(&user, "name = ?", "Bob")
  • Take

Take 方法用于获取满足条件的一条记录。例如:

var user User
db.Take(&user, "age > ?", 25)

4.4 Update(更新)

更新操作用于修改数据库中现有的记录。以下是一些常见的更新操作以及相应的示例:

  • Update

Update 方法用于更新符合条件的记录的指定字段。例如:

db.Model(&User{}).Where("age < ?", 30).Update("name", "Young")
  • Updates

Updates 方法用于更新符合条件的记录的多个字段。例如:

db.Model(&User{}).Where("name = ?", "Alice").Updates(User{Name: "Alicia", Age: 30})
  • Save

Save 方法用于更新记录的所有字段,包括关联字段。例如:

var user User
db.First(&user, "name = ?", "Alice")
user.Age = 28
db.Save(&user)

4.5 Delete(删除)

删除操作用于从数据库中移除记录。以下是一些常见的删除操作以及相应的示例:

  • Delete

Delete 方法用于根据条件删除记录。例如:

db.Delete(&User{}, "age < ?", 18)
  • Unscoped

Unscoped 方法在查询时取消软删除限制,允许删除被标记为已删除的记录。例如:

db.Unscoped().Delete(&User{}, "deleted_at < ?", time.Now())
  • Delete

Delete 方法用于硬删除记录,从数据库中永久移除记录。例如:

db.Unscoped().Delete(&User{}, "name = ?", "Deleted User")

参考文献

  1. GORM指南
  2. 《GORM 中文文档》 | Go 技术论坛 (learnku.com)
  3. ORM是什么?如何理解ORM - 换行程序员 - 博客园 (cnblogs.com)