Gorm基本使用方法总结 | 青训营

432 阅读10分钟

记录一下gorm的使用,结合课件和、官方文档合自己的数据库案例,整合了gorm的基本使用方法。 在课件提供的mysql案例基础上,增加了Postgresql的相关使用案例。

1 连接数据库

1.1 Mysql连接

GORM连接数据库采用驱动的形式实现。如

import (
  "gorm.io/driver/mysql"
  "gorm.io/gorm"
)
func main {
    dsn := "root:tmdgnnwscjl@tcp(127.0.0.1:3306)/douyin?charset=utf8mb4&parseTime=True&loc=Local"
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	// 自定义配置
	if err != nil {
		panic("failed connect!")
	}}

MySQl 驱动程序提供了 一些高级配置 可以在初始化过程中使用,具体如下:

  • ConnMaxLifetime: 设置连接的最大生命周期。即连接在空闲一段时间后会被自动关闭和重置。例如:ConnMaxLifetime: time.Hour。

  • InterpolateParams: 启用或禁用参数插值。默认情况下,GORM 会在执行查询时自动插入参数。例如:InterpolateParams: true。

  • AllowNativePasswords: 允许使用 MySQL 8 之前的本地密码进行身份验证。例如:AllowNativePasswords: true。

  • Collation: 设置字符集的排序规则。例如:Collation: "utf8mb4_general_ci"。

  • ClientFoundRows: 指定在更新或删除操作中返回受影响的行数,而不是匹配的行数。例如:ClientFoundRows: true。

  • Loc: 设置默认时区。例如:Loc: time.UTC。

  • MultiStatements: 允许一次执行多个语句。例如:MultiStatements: true。

  • ColumnsWithAlias: 使用别名时是否将列名包含在字段名称中。例如:ColumnsWithAlias: true。

1.2 Postgresql连接

package main

import (
	"fmt"

	"gorm.io/driver/postgres"
	"gorm.io/gorm"
)

func main() {
	dsn := "user=toni password=tmdgnnwscjl dbname=douyin port=5432 sslmode=disable TimeZone=Asia/Shanghai"
	db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
	// 自定义配置
	if err != nil {
		panic("failed connect!")
	}
}

GORM使用 pgx 作为 postgres 的 database/sql 驱动。默认情况下,它会启用 prepared statement 缓存,可以使用postgres.Config打开连接,并指定PreferSimpleProtocol为true来禁用:

db, err := gorm.Open(postgres.New(postgres.Config{
  DSN:  "user=toni password=tmdgnnwscjl dbname=douyin port=5432 sslmode=disable TimeZone=Asia/Shanghai",
  PreferSimpleProtocol: true, // disables implicit prepared statement usage
}), &gorm.Config{})

2 声明模型

2.1 GORM约定

GORM 倾向于约定,而不是配置。默认情况下,GORM 使用 ID 作为主键,使用结构体名的 蛇形复数 作为表名,字段名的 蛇形 作为列名,并使用 CreatedAt、UpdatedAt 字段追踪创建、更新时间.

举例来说,假如我有一张users表,其在Mysql中的schema如下:

mysql> DESCRIBE users;
+------------------+--------------+------+-----+---------+----------------+
| Field            | Type         | Null | Key | Default | Extra          |
+------------------+--------------+------+-----+---------+----------------+
| id               | int          | NO   | PRI | NULL    | auto_increment |
| username         | varchar(255) | NO   | UNI | NULL    |                |
| nickname         | varchar(255) | YES  |     | NULL    |                |
| passwd           | varchar(255) | NO   |     | NULL    |                |
| follow_count     | int          | YES  |     | NULL    |                |
| follower_count   | int          | YES  |     | NULL    |                |
| work_count       | int          | YES  |     | NULL    |                |
| favorite_count   | int          | YES  |     | NULL    |                |
| token            | int          | YES  |     | NULL    |                |
| avatar           | varchar(255) | YES  |     | NULL    |                |
| background_image | varchar(255) | YES  |     | NULL    |                |
| signature        | varchar(255) | YES  |     | NULL    |                |
| total_favorited  | int          | YES  |     | NULL    |                |
+------------------+--------------+------+-----+---------+----------------+
13 rows in set (0.00 sec)

则应定义的结构体为:

type User struct {
    ID              uint   `gorm:"primaryKey"`
    Username        string `gorm:"unique"`
    Nickname        string
    Passwd          string
    FollowCount     int
    FollowerCount   int
    WorkCount       int
    FavoriteCount   int
    Token           int
    Avatar          string
    BackgroundImage string
    Signature       string
    TotalFavorited  int
}

注意,定义的结构体为User,在数据库中对应的Table名为users,因此其TableName方法需要返回users:

func (u User) TableName() string {
	return "users"
}

2.2 gorm.Model结构体与结构体嵌入

在 GORM 中,gorm.Model 是一个内置的结构体,用于简化模型结构体的定义,特别是对于包含常用字段(如 ID、创建时间、更新时间和删除时间)的模型。通过将 gorm.Model 嵌入到你的模型结构体中,你可以自动获得这些常用字段和相关功能,而无需显式地定义它们。

// gorm.Model 的定义
type Model struct {
  ID        uint           `gorm:"primaryKey"`
  CreatedAt time.Time
  UpdatedAt time.Time
  DeletedAt gorm.DeletedAt `gorm:"index"`
}

Model可以嵌入其他结构体,如:

type User struct {
  gorm.Model
  Name string
}
// 等效于
type User struct {
  ID        uint           `gorm:"primaryKey"`
  CreatedAt time.Time
  UpdatedAt time.Time
  DeletedAt gorm.DeletedAt `gorm:"index"`
  Name string
}

结构体的嵌入是一种将一个结构体类型嵌入到另一个结构体类型中的方式,以便在一个结构体中使用另一个结构体的字段和方法。gorm中对于其他的正常结构体,需要使用embedded 将其嵌入,如:

type Author struct {
    Name  string
    Email string
}

type Blog struct {
  ID      int
  Author  Author `gorm:"embedded"`
  Upvotes int32
}
// 等效于
type Blog struct {
  ID    int64
    Name  string
    Email string
  Upvotes  int32
}

2.3 结构体标签描述列信息

GORM允许使用结构体标签描述如主键、unique等信息 如使用gorm:"primaryKey"设置主键,使用gorm:"unique"设置唯一列,使用gorm:"default设置默认值等。

type User struct {
    ID              uint   `gorm:"primaryKey"`
    Username        string `gorm:"unique"`
    Nickname        string `gorm:"default:'xwd'"`
    Passwd          string
    FollowCount     int
    FollowerCount   int
    WorkCount       int
    FavoriteCount   int
    Token           int
    Avatar          string
    BackgroundImage string
    Signature       string
    TotalFavorited  int
}

2.4 权限控制

权限控制同样使用结构体标签实现

type User struct {
  Name string `gorm:"<-:create"` // 允许读和创建
  Name string `gorm:"<-:update"` // 允许读和更新
  Name string `gorm:"<-"`        // 允许读和写(创建和更新)
  Name string `gorm:"<-:false"`  // 允许读,禁止写
  Name string `gorm:"->"`        // 只读(除非有自定义配置,否则禁止写)
  Name string `gorm:"->;<-:create"` // 允许读和写
  Name string `gorm:"->:false;<-:create"` // 仅创建(禁止从 db 读)
  Name string `gorm:"-"`  // 读写操作均会忽略该字段
}

2.5 TableName 方法的意义

TableName 方法的意义在于可以更灵活地控制数据库表名的命名规则,以适应不同的项目需求或数据库命名约定。这样就可以使用更具描述性的表名,或者根据应用程序逻辑来指定表名。 以下是一个示例,展示如何在 GORM 中使用 TableName 方法来自定义表名:

type User struct {
    ID       uint
    Username string
    Email    string
}

// TableName specifies the custom table name for the User model.
func (User) TableName() string {
    return "custom_users"
}

GORM在数据库中执行sql查询时,将使用TableName返回的字符串作为表面,而非Table对应的结构体名字。

3 ORM基本操作

3.1 创建记录

  1. Create创建一条记录 Create 方法接受一个结构体实例作为参数,并将其插入到数据库中作为新记录。以下是使用 Create 方法创建记录的示例:
	// 创建一条数据
	user := User{
		Username:        "toni",
		Nickname:        "xwd",
		Passwd:          "tmdgnnwscjl",
		FollowCount:     0,
		FollowerCount:   0,
		WorkCount:       0,
		FavoriteCount:   0,
		Token:           777,
		Avatar:          "tmp_av",
		BackgroundImage: "tmp_bg",
		Signature:       "A happy man",
		TotalFavorited:  0,
	}
	db.Create(&user)

注意,在执行 Create 后,newUser 实例的 ID 字段会被数据库自动填充,以反映数据库中的实际值。所以在创建后,你可以通过 newUser.ID 获取新记录的 ID。 2. 选定或排除字段创建

  • Select用于选定字段的来创建
db.Select("Username", "Passwd", "Token").Create(&user)
  • Omit用于创建时排除选定字段
db.Omit("FollowCount", "FollowerCount", "Avatar").Create(&user)
  1. 创建多条记录 使用切片创建多条记录
users := []*User{
		{
			Username:        "Dante",
			Nickname:        "dt",
			Passwd:          "123456",
			FollowCount:     0,
			FollowerCount:   0,
			WorkCount:       0,
			FavoriteCount:   0,
			Token:           555,
			Avatar:          "tmp_av",
			BackgroundImage: "tmp_bg",
			Signature:       "A happy man",
			TotalFavorited:  0,
		},
		{
			Username:        "Vergil",
			Nickname:        "vg",
			Passwd:          "123456",
			FollowCount:     0,
			FollowerCount:   0,
			WorkCount:       0,
			FavoriteCount:   0,
			Token:           555,
			Avatar:          "tmp_av",
			BackgroundImage: "tmp_bg",
			Signature:       "A happy man",
			TotalFavorited:  0,
		},
	}
	res := db.Create(users)
	fmt.Println(res.Error)

Create会将结果返回指针指向的切片,可以遍历切片一访问结果。

  1. 冲突处理 clause.OnConflict可与Clauses结合以处理冲突数据:
user2 := user
db.Clauses(clause.OnConflict{DoNothing: true}).Create(&user2)

clause.OnConflict 允许你在插入数据时指定冲突解决策略,而 gorm.Clauses 则可以用于在查询中添加额外的条件或修改操作。

3.2 查询

  1. First查询一条数据 First查询可以使用主键查询:
db.First(&user, 1)
fmt.Printf("%v", user)

查询时可以指定条件,即sql中的where字句:

// First 指定条件
db.First(&user, "Username = ?", "GHOST")
fmt.Printf("%v", user)

GORM会根据此前定义的TableName方法,在数据库中执行sql指令,所以TableName方法需要返回数据库中实际的表名。

  1. Find结合Where查询多条数据 Find 方法用于检索满足给定条件的多个记录,而 Where 方法用于指定查询条件。结果会返回到传入的切片。
// Find查询多条数据
db.Where("token > ? AND id != ?", 600, 0).Find(&users)
for idx, user := range users {
    fmt.Printf("第%v个结果:\n", idx)
    fmt.Printf("\t%v\n\n", user)
}
  1. 使用结构体作为查询条件 需要在 Where 方法中传递一个结构体实例,并在结构体中定义要用于筛选的字段。
// 使用结构体作为查询条件
db.Where(&User{Username: "Dante"}).First(&user)
fmt.Printf("%v\n", user)
  1. 其他注意事项
  • First查询不到数据会返回ErrRecordNotFound,而Find没有查询到数据不报错
  • 结构体作为查询条件时,默认值将被忽略,只查询非默认值字段
    // 使用结构体作为查询条件
    db.Where(map[string]interface{}{"username": "Dante"}).Find(&users)
    fmt.Printf("%v\n", users[0])
    
    需要注意的是,Mysql查询时,map的字段既可以是username,也可以是Username,但Postgresql中只能是username,即数据库真实的Table名字。

3.3 更新数据

  1. 更新单个列
// 更新单个列
db.Model(&User{
    Username: "Dante",
}).Where("username = ?", "Dante").Update("token", "999")

注意,.Model中传入的结构体只是为了确定表名,不能替代Where传入条件,仍然需要Where的链式调用。

  1. 更新多个列 此处仍然需要Where的链式调用,课件存在错误!!!
// 更新多个列,使用.Model传入条件
db.Model(&User{
    Username: "Dante",
}).Where("username = ?", "Dante").Updates(&User{Nickname: "DD", Token: 1000})
  1. 使用map更新
// 使用map更新
db.Model(&User{
    Username: "Nero",
}).Where("username = ?", "Nero").Updates(map[string]interface{}{"nickname": "telephone booth"})

3.4 删除

  1. 物理删除 语法与更新、查询区别不大,此处只进行简单举例,其余map、Where等的用法与更新查询一致
// 硬删除
db.Where("username = ?", "toni").Delete(&User{})
fmt.Printf("%v\n", user)
  1. 软删除 软删除是一种数据删除策略,它不是直接从数据库中删除记录,而是通过标记记录的状态来表示该记录已被删除。这通常涉及添加一个额外的字段来表示记录的删除状态。软删除可以有助于保留数据的完整性并支持恢复操作。

下面是一个使用 GORM 进行软删除的示例:

User 添加一个名为 DeletedAt 的时间字段,用于表示记录被删除的时间戳:

type User struct {
	ID              uint   `gorm:"primaryKey"`
	Username        string `gorm:"unique"`
	Nickname        string `gorm:"default:'xwd'"`
	Passwd          string
	FollowCount     int
	FollowerCount   int
	WorkCount       int
	FavoriteCount   int
	Token           int
	Avatar          string
	BackgroundImage string
	Signature       string
	TotalFavorited  int
	Deleted         gorm.DeletedAt `gorm:"default:NULL"` // 新增
}

由于gorm实际上是将Deleted字段作为日期类型存储在数据库中,因此我们需要再数据库中相应地增加该字段,以Postgresql为例:

douyin=# ALTER TABLE users
ADD COLUMN deleted date DEFAULT Null;

在删除前,我们先看一下users表:

douyin=# SELECT * FROM users;
 id | username |    nickname     |   passwd    | follow_count | follower_count | work_count | favorite_count | token | avatar | background_image |  signature  | total_favorited | deleted
----+----------+-----------------+-------------+--------------+----------------+------------+----------------+-------+--------+------------------+-------------+-----------------+---------
  2 | GHOST    | gf              | 123456      |            0 |              0 |          0 |              0 |   666 | tmp_av | tmp_bg           | A happy man |               0 |
  5 | Vergil   | vg              | 123456      |            0 |              0 |          0 |              0 |   555 | tmp_av | tmp_bg           | A happy man |               0 |
  4 | Dante    | DD              | 123456      |            0 |              0 |          0 |              0 |  1000 | tmp_av | tmp_bg           | A happy man |               0 |
  6 | Nero     | telephone booth | 444         |              |                |            |                |   989 |        |                  |             |                 |
  1 | toni     | xwd             | tmdgnnwscjl |            0 |              0 |          0 |              0 |   111 | tmp_av | tmp_bg           | A happy man |               0 |
(5 rows)

可以看到,所有记录中deleted字段为空,表明未删除的状态,接下来在gorm中执行删除,假设我们要删除名为toni的列:

// 软删除
db.Where("username = ?", "toni").Delete(&User{})
fmt.Printf("%v\n", user)

删除后再次查看users表:

douyin=# SELECT * FROM users;
 id | username |    nickname     |   passwd    | follow_count | follower_count | work_count | favorite_count | token | avatar | background_image |  signature  | total_favorited |  deleted
----+----------+-----------------+-------------+--------------+----------------+------------+----------------+-------+--------+------------------+-------------+-----------------+------------
  2 | GHOST    | gf              | 123456      |            0 |              0 |          0 |              0 |   666 | tmp_av | tmp_bg           | A happy man |               0 |
  5 | Vergil   | vg              | 123456      |            0 |              0 |          0 |              0 |   555 | tmp_av | tmp_bg           | A happy man |               0 |
  4 | Dante    | DD              | 123456      |            0 |              0 |          0 |              0 |  1000 | tmp_av | tmp_bg           | A happy man |               0 |
  6 | Nero     | telephone booth | 444         |              |                |            |                |   989 |        |                  |             |                 |
  1 | toni     | xwd             | tmdgnnwscjl |            0 |              0 |          0 |              0 |   111 | tmp_av | tmp_bg           | A happy man |               0 | 2023-08-15
(5 rows)

同预测一样,GORM 标记记录为已删除状态而不是物理删除记录,以便在需要时进行恢复或跟踪记录的删除历史。如果要恢复软删除的记录,可以使用 GORM 的恢复方法。

db.Unscoped().Model(&User{}).Where("username = ?", "toni").Update("deleted", nil)

4 GORM事务

4.1 基本事务操作

在 GORM 中,事务(Transaction)是一种用于执行一系列数据库操作的机制,这些操作被当作一个单独的工作单元来处理。事务是数据库操作的一种重要概念,它可以确保一组操作要么全部成功,要么全部失败,从而保持数据的一致性和完整性。 以下为gorm的事务流程:

  1. Begin开启事务 使用 Begin 方法来开启一个事务。Begin 方法返回一个指向 gorm.DB 的指针,它表示了当前的事务。
    tx := db.Begin()
    
  2. 执行事务操作 在事务中执行数据库操作。在事务中,你可以执行 Create、Update、Delete 等操作。
    	result = tx.Model(&User{}).Where("token < ?", 1000).Updates(map[string]interface{}{"token": gorm.Expr("token + ?", 1)})
    

PS:该代码使用了orm.Expr实现了SQL表达式进行更新。 3. 出错时回滚 如果在事务中的任何操作失败,你可以使用 Rollback 方法来回滚事务,从而取消已经进行的操作。

if result.Error != nil {
 	 tx.Rollback()
 	 return
  }
  1. 提交事务 Commit()提交事务
    tx.Commit()
    

4.2 自动提交事务

在 GORM 中,默认情况下,使用 Begin 方法开启的事务不会自动提交。你需要显式调用 Commit 方法来提交事务,或者调用 Rollback 方法来回滚事务。这确保了你可以在所有操作完成后,显式地决定是否要将事务的更改持久化到数据库。

然而,如果你希望在事务中的操作全部成功时自动提交事务,而一旦出现错误就自动回滚事务,GORM 提供了一个 Transaction 方法来支持自动提交和回滚事务。这样可以更方便地处理事务,减少了手动处理提交和回滚的代码量。

以下是使用 Transaction 方法进行自动提交和回滚事务的示例:

err = db.Transaction(func(tx *gorm.DB) error {
  res := tx.Model(&User{}).Where("token < ?", 1000).Update("token", gorm.Expr("token + ?", 1)) // 单条更新
  if res.Error != nil {
    return res.Error
  }
  res = tx.Model(&User{}).Where("token = ?", 1000).Update("token", gorm.Expr("token * ?", 3)) // 单条更新
  if res.Error != nil {
    return res.Error
  }
  return nil
})
if err != nil {
  panic("tx faield!")
}

在 db.Transaction 函数中执行一系列操作。如果在事务中的任何操作失败,事务将会自动回滚,否则,事务会在函数返回时自动提交。这样,无需手动调用 Commit 或 Rollback 来处理事务。

5 Hook

在 GORM 中,Hooks 是一种用于在数据库操作期间执行回调函数的机制。Hooks 可以让你在模型的不同生命周期阶段执行自定义逻辑,比如在创建、更新、删除等操作之前或之后执行特定的代码。这可以用于在数据库操作前后执行验证、记录日志、更新时间戳等任务。

方法名约定为:时间顺序+操作类型,例如:AfterCreate、AfterFind、BeforeDelete等。

以下为一个在插入users前判断密码格式的案例,要求密码长度至少为8:

func (u *User) BeforeCreate(tx *gorm.DB) (err error) {
	if len(u.Username) < 8 {
		return errors.New("length of password must be greater than 8")
	}
	return
}