GORM基本使用 | 青训营笔记

415 阅读11分钟

GORM

gorm 是一款用 Golang 开发的 orm 框架,目前已经成为在 Golang Web 开发中最流行的 orm 框架之一。

gorm 可以连接多种数据库,只需要不同的驱动即可。官方目前仅支持 MySQL、PostgreSQL、SQlite、SQL Server 四种数据库,不过可以通过自定义的方式接入其他数据库。

基础

相比于使用database/sql来连接数据库,使用gorm代码量少了很多而且也更加方便

 import (
     "gorm.io/driver/mysql" // gorm mysql 驱动包
     "gorm.io/gorm"// gorm
 )
 // MySQL 配置信息
 username := "root"              // 账号
 password := "xxxxxx" // 密码
 host := "127.0.0.1"             // 地址
 port := 3306                    // 端口
 DBname := "gorm1"               // 数据库名称
 timeout := "10s"                // 连接超时,10秒
 dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8&parseTime=True&loc=Local&timeout=%s", username, password, host, port, DBname, timeout)
 // Open 连接
 db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
 if err != nil {
     panic("failed to connect mysql.")
 }
 ​

表与结构体对应

image-20230523150610483.png

约定大于配置

比如会根据结构体的复数寻找表名,会使用 ID 作为主键,会根据 CreateAt、UpdateAt 和 DeletedAt 表示创建时间、更新时间和删除时间

gorm 提供了一个 Model 结构体,可以将它嵌入到自己的结构体中,省略以上几个字段。

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

嵌入到 goods 结构体中。

 type Goods struct {
     gorm.Model
     Id    int
     Name  string
     Price int
     Title string `gorm:"column:t, size:256, unique:true"`    // 后面反引号中的字符串表示对字段进行约束,这个表示 希望将Title 映射为 t,设置最大长度为 256,该字段唯一
 }

这样在每次创建不同的结构体时就可以省略创建 ID、CreatedAt、UpdatedAt、DeletedAt 这几个字段。

具体的tags的功能

标签名说明
column指定 db 列名
type列数据类型,推荐使用兼容性好的通用类型,例如:所有数据库都支持 bool、int、uint、float、string、time、bytes 并且可以和其他标签一起使用,例如:not nullsize, autoIncrement… 像 varbinary(8) 这样指定数据库数据类型也是支持的。在使用指定数据库数据类型时,它需要是完整的数据库数据类型,如:MEDIUMINT UNSIGNED not NULL AUTO_INSTREMENT
size指定列大小,例如:size:256
primaryKey指定列为主键
unique指定列为唯一
default指定列的默认值
precision指定列的精度
scale指定列大小
not null指定列为 NOT NULL
autoIncrement指定列为自动增长
embedded嵌套字段
embeddedPrefix嵌入字段的列名前缀
autoCreateTime创建时追踪当前时间,对于 int 字段,它会追踪时间戳秒数,您可以使用 nano/milli 来追踪纳秒、毫秒时间戳,例如:autoCreateTime:nano
autoUpdateTime创建/更新时追踪当前时间,对于 int 字段,它会追踪时间戳秒数,您可以使用 nano/milli 来追踪纳秒、毫秒时间戳,例如:autoUpdateTime:milli
index根据参数创建索引,多个字段使用相同的名称则创建复合索引,查看 索引 获取详情
uniqueIndexindex 相同,但创建的是唯一索引
check创建检查约束,例如 check:age > 13,查看 约束 获取详情
<-设置字段写入的权限, <-:create 只创建、<-:update 只更新、<-:false 无写入权限、<- 创建和更新权限
->设置字段读的权限,->:false 无读权限
-忽略该字段,- 无读写权限

自动迁移

在数据库的表尚未初始化时,gorm 可以根据指定的结构体自动建表。

通过 db.AutoMigrate 方法根据 User 结构体,自动创建 user 表。如果表已存在,该方法不会有任何动作。

建表的规则会把 user 调整为复数,并自动添加 gorm.Model 中的几个字段。由于很多数据库是不区分大小写的,如果采用 camelCase 风格命名法,在迁移数据库时会遇到很多问题,所以数据库的字段命名风格都是采用 underscorecase 风格命名法,gorm 会自动帮我们转换。

 type User struct {
     gorm.Model
     UserName string
     Password string
 }
 ​
 db.AutoMigrate(&User{})  // 可以根据定义的User结构体来自动创建 对应的user表

image.png

基础用法-CRUD

image-20230519215048797.png

image-20230519215239869.png

创建

使用 db.Create 方法,传入结构体的指针创建。

 user := User{UserName: "l", Password: "ddd"}
 result := db.Create(&user)
 ​
 // 返回的常用的字段
 fmt.Println("ID:", user.ID)                       // 插入的主键
 fmt.Println("error:", result.Error)               // 返回的 error
 fmt.Println("rowsAffected:", result.RowsAffected) // 插入的条数

等同于以下 SQL。

 INSERT INTO
     `users` ( `created_at`, `updated_at`, `deleted_at`, `user_name`, `password` )
 VALUES
     (
         '2020-12-03 17:19:00.249',
         '2020-12-03 17:19:00.249',
         NULL,
     'l',
     'ddd')

gorm 会自动维护 created_at、updated_ad 和 deleted_at 三个字段。

插入

只插入指定字段

 user := User{UserName: "lzq", Password: "ccc"}
 result := db.Select("UserName").Create(&user)

等同于以下 SQL。

 INSERT INTO `users` (`user_name`) VALUES ('lzq')

_需要注意:使用 select 时不会自动维护 created_at、updated_ad 和 deleted_at。

不插入指定字段

使用 Omit 方法过滤一些字段。

 result := db.Omit("UserName").Create(&user)

批量插入

当需要批量插入时,传入一个切片即可。

 users := []User{
     {UserName: "lzq", Password: "aaa"},
     {UserName: "qqq", Password: "bbb"},
     {UserName: "gry", Password: "ccc"},
 }
 db.Create(&users)

等同于以下 SQL。

 INSERT INTO `users` ( `created_at`, `updated_at`, `deleted_at`, `user_name`, `password` )
 VALUES
     ( '2020-12-03 18:08:47.478', '2020-12-03 18:08:47.478', NULL, 'lzq', 'aaa' ),(
         '2020-12-03 18:08:47.478',
         '2020-12-03 18:08:47.478',
         NULL,
         'qqq',
         'bbb'
         ),(
         '2020-12-03 18:08:47.478',
         '2020-12-03 18:08:47.478',
         NULL,
     'gry',
     'ccc')

分批批量插入

在某些情况下,users 的数量可能非常大,此时可以使用 CreateInBatches 方法分批次批量插入。

假设有 6 条 user 数据,你想每次插入 2 条,这样就会执行 3 次 SQL。

 users := []User{
     {UserName: "lzq", Password: "aaa"},
     {UserName: "qqq", Password: "bbb"},
     {UserName: "gry", Password: "ccc"},
     {UserName: "lzq", Password: "aaa"},
     {UserName: "qqq", Password: "bbb"},
     {UserName: "gry", Password: "ccc"},
 }
 ​
 db.CreateInBatches(&users, 2)

等同于依次执行以下 3 句 SQL。

 sql复制代码INSERT INTO `users` ( `created_at`, `updated_at`, `deleted_at`, `user_name`, `password` )
 VALUES
     ( '2020-12-03 18:15:20.602', '2020-12-03 18:15:20.602', NULL, 'lzq', 'aaa' ),(
         '2020-12-03 18:15:20.602',
         '2020-12-03 18:15:20.602',
         NULL,
     'qqq',
     'bbb')
 sql复制代码INSERT INTO `users` ( `created_at`, `updated_at`, `deleted_at`, `user_name`, `password` )
 VALUES
     ( '2020-12-03 18:15:20.616', '2020-12-03 18:15:20.616', NULL, 'gry', 'ccc' ),(
         '2020-12-03 18:15:20.616',
         '2020-12-03 18:15:20.616',
         NULL,
     'lzq',
     'aaa')
 sql复制代码INSERT INTO `users` ( `created_at`, `updated_at`, `deleted_at`, `user_name`, `password` )
 VALUES
     ( '2020-12-03 18:15:20.621', '2020-12-03 18:15:20.621', NULL, 'qqq', 'bbb' ),(
         '2020-12-03 18:15:20.621',
         '2020-12-03 18:15:20.621',
         NULL,
         'gry',
     'ccc'
     )

CreateInBatches 方法的内部是使用 for 进行切割切片的,并没有使用 goroutine。

查询

查询单个对象

gorm 提供了 First、Take、Last 方法。它们都是通过 LIMIT 1 来实现的,分别是主键升序、不排序和主键降序。

 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;

如果没有查询到对象,会返回 ErrRecordNotFound 错误。

 result := db.First(&user)
 errors.Is(result.Error, gorm.ErrRecordNotFound)
 result.RowsAffected

根据主键查询

在 First/Take/Last 等函数中设置第二个参数,该参数被认作是 ID。可以选择 int 或 string 类型。

 db.First(&user, 10)   // 根据主键查询
 db.First(&user, "10")

选择 string 类型的变量时,需要注意 SQL 注入问题。

查询多个对象(列表)

使用 Find 方法查询多个对象。

 users := []User{}
 result := db.Find(&users)

返回值会映射到 users 切片上。

设置查询条件 Where

gorm 提供了万能的 Where 方法,可以实现 =、<>、IN、LIKE、AND、>、<、BETWEEN 等方法,使用 ? 来占位。

 db.Where("name = ?", "l").First(&user)
 // SELECT * FROM users WHERE user_name = 'l' ORDER BY id LIMIT 1;
 ​
 // 获取全部匹配的记录
 db.Where("name <> ?", "l").Find(&users)
 // SELECT * FROM users WHERE user_name <> 'l';
 ​
 // IN
 db.Where("name IN ?", []string{"lzq", "qqq"}).Find(&users)
 // SELECT * FROM users WHERE user_name IN ('lzq','qqq');
 ​
 // LIKE
 db.Where("name LIKE ?", "%l%").Find(&users)
 // SELECT * FROM users WHERE user_name LIKE '%l%';
 ​
 // AND
 db.Where("name = ? AND age = ?", "lzq", "aaa").Find(&users)
 // SELECT * FROM users WHERE user_name = 'lzq' AND password = aaa;
 ​
 // BETWEEN
 db.Where("created_at BETWEEN ? AND ?", lastWeek, today).Find(&users)
 // SELECT * FROM users WHERE created_at BETWEEN '2020-11-01 00:00:00' AND '2020-11-08 00:00:00';

Where 快速设置条件的方法

传递 Struct、Map 和 切片时,可以实现更简便的设置条件。

 db.Where(&User{UserName:"lzq", Password:"aaa"}).Find(&user)
 db.Where(map[string]interface{}{"user_name": "lzq", "password": "aaa"}).Find(&user)

结构体和 Map 的效果几乎是相等的。

 SELECT
     *
 FROM
     `users`
 WHERE
     `users`.`user_name` = 'lzq'
     AND `users`.`password` = 'aaa'
     AND `users`.`deleted_at` IS NULL

两者唯一的不同之处在于 struct 中的零值字段不会查询。比如 0、""、false。

切片是查询主键。

 db.Where([]int{10, 11}).Find(&user)

等同于如下 SQL。

 sql复制代码SELECT
     *
 FROM
     `users`
 WHERE
     `users`.`id` IN ( 10, 11 )
     AND `users`.`deleted_at` IS NULL

所有的查询,gorm 都会默认设置 tabel.deleted_at IS NULL 查询条件。

除了 Where 方法外,还有内联查询的方式,但是不推荐同时使用两种风格。

 db.Find(&user, "user_name = ?", "lzq")
 // SELECT * FROM users WHERE user_name = "lzq";

其他查询 Not & Or

gorm 还提供了 Not 和 Or 方法,但不推荐使用,因为 Where 同样可以实现两者的功能,记忆额外的 API 无疑会增加心智负担。

 db.Where("password = ?", "aaa").Not("user_name", "l").Or("id > ?", 10).Find(&users)

等同于如下 SQL。

 SELECT * FROM `users` WHERE (( PASSWORD = 'aaa' )
     AND `user_name` <> 108
     OR id > 10
 )
 AND `users`.`deleted_at` IS NULL

选取特定字段 Select

使用 Select 方法。

 go
 复制代码db.Select("password").Where(&User{UserName:"lzq"}).Find(&user)

等同于以下 SQL。

 sql复制代码SELECT
     `password`
 FROM
     `users`
 WHERE
     `users`.`user_name` = 'lzq'
     AND `users`.`deleted_at` IS NULL

排序 Order

 db.Order("user_name desc, password").Find(&users)

等同于以下 SQL。

 SELECT
     *
 FROM
     `users`
 WHERE
     `users`.`deleted_at` IS NULL
 ORDER BY
     user_name DESC,
 PASSWORD

分页 Limit Offset

Limit 和 Offset 可以单独使用,也可以组合使用。

 db.Limit(3).Find(&users)
 db.Offset(3).Find(&users)
 db.Limit(2).Offset(3).Find(&users)

等同于以下 SQL。

 SELECT
     *
 FROM
     `users`
 WHERE
     `users`.`deleted_at` IS NULL
     LIMIT 2 OFFSET 3

分组 Group Having

根据 username 统计用户名的重复。

 result := []map[string]interface{}{}
 db.Model(&User{}).
   Select("user_name, SUM( id ) AS nums").
   Group("user_name").
   Find(&result)

等同于以下 SQL。

 SELECT
     user_name,
     SUM( id ) AS nums
 FROM
     users
 GROUP BY
     user_name;

去重 Distinct

 result := []string{}
 db.Model(&User{}).   // User{} 表示user对应的表
   Distinct("user_name").
   Find(&result)  // result表示需要存储结果的变量

等同于以下 SQL。

 SELECT DISTINCT
     user_name
 FROM
     users

连表 Join

在业务中不太建议使用 Join,而是使用多条查询来做多表关联。

更新

使用 Save 方法更新所有字段,即使是零值也会更新。

 db.First(&user)
 user.UserName = ""
 db.Save(&user)

等同于以下 SQL。

 sql复制代码UPDATE `users`
 SET `created_at` = '2020-12-03 15:12:08.548',
 `updated_at` = '2020-12-04 09:17:40.891',
 `deleted_at` = NULL,
 `user_name` = '',
 `password` = 'ddd'
 WHERE
     `id` = 1

更新单列

使用 Model 和 Update 方法更新单列。

可以使用结构体作为选取条件,仅选择 ID。

 user.ID = 12
 db.Model(&user).Update("user_name", "lzq")

等同于以下 SQL。

 UPDATE `users`
 SET `user_name` = 'lzq',
 `updated_at` = '2020-12-04 09:16:45.263'
 WHERE
     `id` = 12

也可以在 Model 中设置空结构体,使用 Where 方法自己选取条件。

 db.Model(&User{}).Where("user_name", "gry").Update("user_name", "gry2")

等同于以下 SQL。

 UPDATE `users`
 SET `user_name` = 'gry2',
 `updated_at` = '2020-12-04 09:21:17.043'
 WHERE
     `user_name` = 'gry'

还可以组合选取条件。

 user.ID = 20
 db.Model(&user).Where("username", "gry").Update("password", "123")

等同于以下 SQL。

 UPDATE `users`
 SET `password` = '123',
 `updated_at` = '2020-12-04 09:25:30.872'
 WHERE
     `username` = 'gry'
     AND `id` = 20

更新多列

使用 Updates 方法进行更新多列。支持 struct 和 map 更新。当更新条件是 struct 时,零值不会更新,如果确保某列必定更新,使用 Select 选择该列。

批量更新

如果在 Model 中没有设置 ID,默认是批量更新。

删除数据 Delete

删除单条

使用 Delete 方法删除单条数据。但需要指定 ID,不然会批量删除。

 user.ID = 20
 db.Delete(&user)

等同于以下 SQL。

 UPDATE `users`
 SET `deleted_at` = '2020-12-04 09:45:32.389'
 WHERE
     `users`.`id` = 20
     AND `users`.`deleted_at` IS NULL

设置删除条件

使用 Where 方法进行设置条件。

 db.Where("user_name", "lzq").Delete(&user)

等同于以下 SQL。

 UPDATE `users`
 SET `deleted_at` = '2020-12-04 09:47:30.544'
 WHERE
     `user_name` = 'lzq'
     AND `users`.`deleted_at` IS NULL

根据主键删除

第二个参数可以是 int、string。使用 string 时需要注意 SQL 注入。

 db.Delete(&User{}, 20)

等同于以下 SQL。

 sql复制代码UPDATE `users`
 SET `deleted_at` = '2020-12-04 09:49:05.161'
 WHERE
     `users`.`id` = 20
     AND `users`.`deleted_at` IS NULL

也可以使用切片 []int、[]string 进行根据 ID 批量删除。

 db.Delete(&User{}, []string{"21", "22", "23"})

等同于以下 SQL。

 sql复制代码UPDATE `users`
 SET `deleted_at` = '2020-12-04 09:50:38.46'
 WHERE
     `users`.`id` IN ( '21', '22', '23' )
     AND `users`.`deleted_at` IS NULL

批量删除

空结构体就是批量删除。

软删除(逻辑删除)

如果结构体包含 gorm.DeletedAt 字段,会自动获取软删除的能力。

在调用所有的 Delete 方法时,会自动变为 update 语句。

 UPDATE users SET deleted_at="2020-12-04 09:40" WHERE id = 31;

在查询时会自动忽略软删除的数据。

 SELECT * FROM users WHERE user_name = 'gry' AND deleted_at IS NULL;

查询软删除的数据

使用 Unscoped 方法查找被软删除的数据。

 db.Unscoped().Where("user_name = gry").Find(&users)

永久删除(硬删除 物理删除)

使用 Unscoped 方法永久删除数据。

 user.ID = 14
 db.Unscoped().Delete(&user)

原生 SQL

除了上面的封装方法外,gorm 还提供了执行原生 SQL 的能力。

执行 SQL 并将结果映射到变量上

使用 Raw 方法配合 Scan 方法。

可以查询单条数据扫描并映射到结构体或 map 上。

 db.
   Raw("SELECT id, record_id, user_name, password FROM users WHERE id = ?", 25).
   Scan(&user)

也可以映射到其他类型上。

 var userCount int
 db.
   Raw("SELECT count(id) FROM users").
   Scan(&userCount)

如果返回结果和传入的映射变量类型不匹配,那么变量的值不会有变化。

只执行 SQL 不使用结果

使用 Exec 方法执行 SQL。

 db.Exec("UPDATE users SET password=? WHERE id = ?", "abcdefg", 22)

钩子 Hook

gorm 提供了 Hook 功能。可以在创建、查询、更新和删除之前和之后自动执行某些逻辑。

创建

gorm 提供了 4 个创建钩子,BeforeCreate、AfterCreate 和 BeforeSave、AfterSave。

假设现在需要添加一个 RecordID,并且在每次创建时生成一个 16 位的 uuid。

 type User struct {
     gorm.Model
     RecordID string
     UserName string
     Password string
 }

除此之外,还希望在存储之前打印生成的 uuid,在存储之后打印创建后的 id。

实现方式就是给模型结构体 User 添加 BeforeCreate 和 AfterCreate 两个方法。

 func (u *User) BeforeCreate(tx *gorm.DB) error {
     u.RecordID = uuid.New().String()
     fmt.Println("创建 User 开始,UUID 为:", u.RecordID)
     return nil
 }
 ​
 func (u *User) AfterCreate(tx *gorm.DB) error {
     fmt.Println("创建 User 完毕,ID 为:", u.ID)
     return nil
 }

更新

更新的 Hook 是 BeforeUpdate、AfterUpdate 和 BeforeSave、AfterSave,用法与创建一致。

查询

查询的 Hook 是 AfterFind,用法与创建一致。

删除

删除的 Hook 是 BeforeDelete 和 AfterDelete,用法与创建一致。

除了查询的 Hook 外,其他 Hook 都是在事务上运行的,一旦在函数中 return error 时,就会触发事务回滚。