最近开发过程中总结的Gorm的一些使用心得

快速上手

创建数据库表, 并插入一条数据

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
CREATE TABLE `users` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` int(10) unsigned NOT NULL COMMENT '用户id',
  `user_name` varchar(24) NOT NULL DEFAULT '' COMMENT '昵称',
  `user_phone` varchar(30) NOT NULL DEFAULT '' COMMENT '用户手机号',
  `age` tinyint(4) NOT NULL COMMENT '年龄',
  `create_time` int(10) unsigned NOT NULL DEFAULT '0',
  `update_time` int(10) unsigned NOT NULL DEFAULT '0',
  PRIMARY KEY (`id`),
  KEY `idx_uid` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

insert users (user_id,user_name) VALUES (10001, "孙悟空");

创建模型

1
2
3
4
5
6
7
8
9
type User struct {
	ID         int
	UserID     int
	UserName   string
	UserPhone  string
	Age        int
	CreateTime int
	UpdateTime int
}

从数据库查询user列表

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// user.go
func (u User) GetList(ctx *gin.Context, ids []int) (userList []User, err error) {
	db := helpers.MysqlClientFtp.WithContext(ctx).Where(ids).Find(&userList)
	err = db.Error
	if err != nil {
		return
	}

	return userList, nil
}

// user_test.go
func TestList(t *testing.T) {
	user := models.User{}
	userList, err := user.GetList(ctx, []int{1,2,3})

	if err != nil {
		fmt.Println("err:", err)
	} else {
		fmt.Printf("userList:%+v\n", userList)
	}
}

// 执行结果 userList:[{ID:1 UserID:10001 UserName:孙悟空 UserPhone: Age:0 CreateTime:0 UpdateTime:0}]

// sql: SELECT * FROM `users` WHERE `users`.`id` IN (1,2,3)

模型

表名

按照gorm的约定,表名是把模型名称驼峰转成下划线的形式,后面加上表示复述的s。比如模型名称是User,对应的表名称为users,模型名称UserInfo,对应的表名user_infos。

可以通过实现Tabler接口修改表名

1
2
3
4
// TableName 把 User 的表名重写为 `user`
func (User) TableName() string {
  return "user"
}

列名

根据gorm的约定,数据表的列名是模型字段名称转成下划线形式。可以在定义模型结构体的时候通过column标签重新修改列的名称。

结构体字段名称必须是大写字母开头。

1
2
3
4
5
6
7
8
9
type User struct {
	ID         int         `gorm:"column:id"`
	UserID     int         `gorm:"column:user_id"`
	UserName   string      `gorm:"column:user_name"`
	UserPhone  string      `gorm:"column:user_phone"`
	Age        int         `gorm:"column:age"`
	CreateTime int         `gorm:"column:create_time"`
	UpdateTime int         `gorm:"column:create_time"`
}

CURD

详见官方文档 https://gorm.io/zh_CN/docs/create.html

创建

查询

修改

删除

实践和总结

自定义字段类型

除了int,varchar等数据库类型,gorm还支持自定义数据类型。举个例子,user表有个字段user_phone,为了数据安全,数据表里存的是加密过的手机号,通过定义一个自定义类型可以做到加密解密过程对上层无感。

定义新类型

自定义的数据类型必须实现 Scanner 和 Valuer 接口,以便让 GORM 知道如何将该类型接收、保存到数据库。 定义Phone类型,自动加密解密。 方便演示这里用base64编码代替加密方法。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
type Phone string

func (phone Phone) Value() (driver.Value, error) {
	return base64.StdEncoding.EncodeToString([]byte(phone)), nil
}

func (phone *Phone) Scan(value interface{}) error {
	bytesValue, ok := value.([]byte)
	if !ok {
		return errors.New("format error")
	}
	p, err := base64.StdEncoding.DecodeString(string(bytesValue))
	if err == nil {
		*phone = Phone(p)
	}

	return err
}

结构体

修改结构体的UserPhone字段为上面定义的Phone类型

1
2
3
4
5
6
7
8
9
type User struct {
	ID         int         `gorm:"primary_key;column:id"`
	UserID     int         `gorm:"primary_key;column:user_id"`
	UserName   string      `gorm:"column:user_name"`
	UserPhone  Phone       `gorm:"column:user_phone"`
	Age        int         `gorm:"column:age"`
	CreateTime int         `gorm:"column:create_time"`
	UpdateTime int         `gorm:"column:update_time"`
}

写入数据表

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
func TestUserCreate(t *testing.T) {
	user := models.User{}

	user.UserID = 10002
	user.UserName = "猪八戒"
	user.UserPhone = "13800138001"

	err := user.Create(ctx)
	if err != nil {
		t.Error(err)
	} else {
		t.Logf("%+v\n", user)
	}
}

执行结果

1
2
3
4
// {ID:3 UserID:10002 UserName:猪八戒 UserPhone:13800138001 Age:0 CreateAt:0 UpdatedAt:0}

// sql
INSERT INTO `users` (`user_id`,`user_name`,`user_phone`,`age`,`create_time`,`update_time`) VALUES (10002,'猪八戒','MTM4MDAxMzgwMDE=',0,0,0)

从表中读取数据

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func TestFindById(t *testing.T) {
	user := models.User{}

	err := user.FindById(ctx, 3)
	if err != nil {
		t.Error(err)
	} else {
		t.Logf("%+v\n", user)
	}
}

执行结果

1
2
3
// sql
SELECT * FROM `users` WHERE `users`.`id` = 3 ORDER BY `users`.`id` LIMIT 1
// {ID:3 UserID:10002 UserName:猪八戒 UserPhone:13800138001 Age:0 CreatedAt:0 UpdatedAt:0}

批量查询和批量插入

最近遇到一个需求,需要一次性插一批数据到数据库,数量条数可能到几十万量级,为了避免数据库压力,一般需要分批插入,gorm支持批量插入

1
result := db.Session(&gorm.Session{CreateBatchSize: 1000}).Create(&records)

只需要把数据传入,gorm会按照设置的批次自动把sql拆成多条执行。

同理,批量查询

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
func (record Record) LabelList(ctx *gin.Context, projectId int) (recordList []Record, total int64, err error) {
	db := helpers.MysqlClientFtp.WithContext(ctx).Model(&record)
	db = db.Where("project_id = ? AND completed_status != 0", projectId)

	db = db.Select([]string{"id", "text", "completed_status", "completed_result"})

	var result []Record
	db = db.FindInBatches(&result, 1000, func(tx *gorm.DB, batch int) error {
		total += tx.RowsAffected
		recordList = append(recordList, result...)
		return nil
	})

	err = db.Error

	if err != nil {
		return
	}

	return
}

自动更新创建和更新时间

方法1:数据库字段类型

mysql数据库的timestamp类型和datetime类型支持自动更新时间,如果定义字段类型时指定DEFAULT CURRENT_TIMESTAMP,则插入数据时不指定字段值,插入结果字段值自动更新成当前时间戳。定义字段时加上ON UPDATE CURRENT_TIMESTAMP,则记录更新时会自动更新字段值为当前时间戳。

方法2:gorm约定

根据gorm的约定,会对以下字段进行自动操作:

  • CreatedAt:创建时间
  • UpdatedAt:更新时间
  • DeletedAt: 删除时间

如果模型定义中有以上字段,在新增、更新和删除的时候这些字段会自动更新成当前时间。

定义数据库表结构的时候按照上面字段的小些加下划线的形式即可实现时间自动更新。

在模型定义中嵌入gorm.Model可以避免在每一个模型定义中写重复字段,修改User的模型定义

1
2
3
4
5
6
7
type User struct {
	gorm.Model
	UserID     int
	UserName   string
	UserPhone  Phone
	Age        int
}

gorm.Model定义如下

1
2
3
4
5
6
type Model struct {
	ID        uint `gorm:"primarykey"`
	CreatedAt time.Time
	UpdatedAt time.Time
	DeletedAt DeletedAt `gorm:"index"`
}

如果数据库定义需要遵守统一规范,创建时间和更新时间字段名称不能定义成create_at和update_at,或者字段类型不能为datetime,这种情况下可以自己新建一个model用来嵌入到各个模型的定义中。

1
2
3
4
type Model struct {
	CreatedAt int64 `gorm:"column:create_time"`
	UpdatedAt int64 `gorm:"column:update_time"`
}

或者

1
2
3
4
type Model struct {
	CreateTime int64 `gorm:"column:create_time;autoCreateTime"`
	UpdateTime int64 `gorm:"column:update_time;autoUpdateTime"`
}

更新字段为零值

看一下下面的代码有什么问题

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// user.go
func (u *User) Update(ctx *gin.Context) (rows int64, err error)  {
	db := helpers.MysqlClientFtp.WithContext(ctx)
	db = db.Updates(u)

	err = db.Error
	rows = db.RowsAffected

	return
}
// user_test.go
func TestUser_Update(t *testing.T) {
	user := models.User{}

	err := user.FindById(ctx, 3)
	if err != nil {
		t.Error(err)
	}

	user.UserName = user.UserName + "123"
	user.Age = 0

	_, err = user.Update(ctx)
	if err != nil {
		t.Error(err)
	} else {
		fmt.Println("ok")
	}
}

代码执行结果

1
UPDATE `users` SET `user_id`=10002,`user_name`='猪八戒123',`user_phone`='MTM4MDAxMzgwMDE=',`update_time`=1643190403 WHERE `id` = 3

执行的sql语句中set值没有age,age修改没有生效。在使用struct更新时,gorm默认只更新非零值的字段。 更新零值字段有以下几种方法:

  • 用Select选中所有需要更新的字段,上面代码修改为

    1
    2
    3
    
    db = db.Select("user_id", "user_name", "user_phone", "age").Updates(u)
    // 或者
    db = db.Select("*").Updates(u)
    
  • map更新属性

    1
    
    
    
    1
    
    
    
  • Save方法,Save会保持所有字段,包括零值

    1
    2
    3
    
    user.UserName = "猪八戒456"
    user.Age = 0
    db.Save(&user)
    

SQL表达式

设想这样一个需求,有一个汇总表记录任务完成情况,其中有一个字段total_finish记录任务完成数,每完成一个任务total_finish数需要加1。

1
2
3
4
task := Task{}
task.FindById(1)
task.TotalFinish = task.TotalFinish + 1
task.Update()

只有一个进程完成任务时这样写没有问题,如果有多个人同时在完成任务,就会导致并发,导致total_finish字段不准确。

可以通过加锁的方式来解决,乐观锁和悲观锁。也可以用set total_finish = total_finish+1避免加锁。

gorm中叫SQL表达式,例如:

1
2
db.Model(&task).Update("total_finish", gorm.Expr("total_finish + ?", 1))
// UPDATE task SET total_finish = total_finish + 1 WHERE id = 1

分页

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
func NormalPaginate(page *NormalPage) func(db *gorm.DB) *gorm.DB {
	return func(db *gorm.DB) *gorm.DB {
		pageNo := 1
		if page.No > 0 {
			pageNo = page.No
		}

		pageSize := page.Size
		switch {
		case pageSize > 1000:
			pageSize = 1000
		case pageSize <= 0:
			pageSize = 10
		}

		offset := (pageNo - 1) * pageSize
		return db.Offset(offset).Limit(pageSize)
	}
}

func (record Record) SampleList(ctx *gin.Context, projectId int) (sampleList []Record, total int64, err error) {
	db := helpers.MysqlClientFtp.WithContext(ctx).Model(&record)
	db = db.Where("project_id = ?", projectId)
	db = db.Count(&total)
	db = db.Order("sample_index").Scopes(models.NormalPaginate(page))

	db = db.Find(&sampleList)


	err = db.Error

	if err != nil {
		return
	}

	return
}