Gorm框架-CRUD操作
linshukai Lv2

简介

关于ORM(Object-Relational Mapping)

ORM其实指的是将关系型数据库中的数据和面向对象程序中对象模型进行映射的技术;
ORM可以用来自动化处理SQL语句的生成和执行,程序员可以更专注于业务逻辑的实现而不是数据的细节。

为什么需要ORM?(参考gpt,结合自身使用过程)

  • 提高开发效率(自动化生成SQL,减少手动编写SQL时间)
  • 只需要定义好模型,可以自动处理不同数据库之间的差异;(如果传统编写,换数据库相当于需要吧原来的逻辑重写一遍)
  • 易于维护,方便拓展(传统编写SQL如果多起来,排查问题和重构时很痛苦);
  • 安全,参数化查询避免SQL注入

关于Gorm

Gorm是一个全功能的ORM框架,主要针对Go语言而开发,支持处理主流的关系型数据库(MySQL、PostgreSQL、SQL Server)
以及一些NoSQL数据库。由于这边是直接使用的是Gorm 2,关于Gorm的重要特性可以以gorm官网为准,可以直接参考:

https://gorm.io/zh_CN/docs/index.html#%E5%AE%89%E8%A3%85

安装和设置

快速安装Gorm2,Mysql驱动

1
2
go get gorm.io/gorm
go get gorm.io/driver/mysql

初始化Mysql连接(参考官网)

1
2
3
4
5
6
7
8
9
10
import (
"gorm.io/driver/mysql"
"gorm.io/gorm"
)

func main() {
// 参考 https://github.com/go-sql-driver/mysql#dsn-data-source-name 获取详情
dsn := "user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
}

备注:

  • 定义DSN(Data Source Name)连接数据库字符串dsn;
  • 传入MySQL驱动,初始化gorm数据库连接;

这是我自己定义初始化连接,读取对应配置文件

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
38
39
40
41
42
43
44
45
type Config struct {
MysqlConfig DBConfig `json:"mysql"`
}

type DBConfig struct {
Username string `json:"username"`
Password string `json:"password"`
Host string `json:"host"`
Port int `json:"port"`
DBName string `json:"prefix"`
}

func initDB() (db *gorm.DB, err error) {
confPath := "conf.json"
if _, err = os.Stat(confPath); err != nil {
return
}

var config Config
if err = configor.Load(&config, confPath); err != nil {
return
}

// 新建Database Gorm连接
mysqlConfig := &config.MysqlConfig
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4",
mysqlConfig.Username, mysqlConfig.Password, mysqlConfig.Host,
mysqlConfig.Port, mysqlConfig.DBName)
db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})

return
}

func main() {

db, err := initDB()

if err != nil {
log.Fatalf(err.Error())
return
}

db = db.Debug()
execSQL(db)
}

备注:

  • 这边我在项目中定义了关于数据库连接的结构体,通过读取预设好的config.json文件,加载好预设的配置信息;
  • 另外这边的db = db.Debug(),可以开启db的DEBUG模式,能够打印出执行的SQL语句以及对应的参数,以方便调试;

config.json参考

1
2
3
4
5
6
7
8
9
{
"mysql": {
"username": "root",
"password": "root",
"host": "127.0.0.1",
"prefix": "GormDemo",
"port": 3306
}
}

定义模型

由于Gorm模型的结构体名的蛇形复数作为表名,字段名的蛇形作为列名,如果Mysql表的设计遵循了GORM约定,则可少写很多代码,
但是实际情况,往往不是这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type User struct {
ID string `gorm:"column:id" faker:"-"`
Email string `gorm:"column:email" faker:"email"`
Password string `gorm:"column:password" faker:"password"`
PhoneNumber string `gorm:"column:phone_number" faker:"phone_number"`
UserName string `gorm:"column:username" faker:"username"`
FirstName string `gorm:"first_name" faker:"first_name"`
LastName string `gorm:"last_name" faker:"last_name"`
Century string `gorm:"century" faker:"century"`
Date string `gorm:"date" faker:"date"`
}

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

备注:

  • 这是我定义的结构体User,通过实现Tabler接口来更改默认表名,TableName()方法会将Music表名重写为user(如果不修改则会默认为users);
  • 修改对应字段对应数据的列名,修改标签column;
  • 模型中的faker的标签可以忽略,这边主要是生成假数据才会使用到的;

CRUD

创建

单挑记录插入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// insertOneRow 单挑记录插入
func insertOneRow(db *gorm.DB){
var tmpUser *User

// 生成随机数据
err := faker.FakeData(&tmpUser)
if err != nil {
log.Fatalf(err.Error())
}

result := db.Create(tmpUser)
if result.Error != nil {
log.Fatalf(result.Error.Error())
}

fmt.Println("result RowsAffected: ", result.RowsAffected)
fmt.Printf("%+v\n", tmpUser)

}

备注:

  • 由于想生成写测试数据,所以使用了github.com/go-faker/faker生成假数据,有兴趣可以了解,像前面的结构体定义Tag之后就能生成对应的假数据;
  • 基本创建单条记录没啥好说的,只要定义模型的结构体,补充数据,然后插入即可;

批量记录插入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// insertRows 批量插入
func insertRows(db *gorm.DB) {
var users []*User
for i := 0; i < 10; i++ {
tmpUser := User{}
err := faker.FakeData(&tmpUser)
if err != nil {
log.Fatal(err.Error())
}
users = append(users, &tmpUser)
}

result := db.Create(users)

if result.Error != nil {
log.Fatalf(result.Error.Error())
}

fmt.Println("RowsAffected: ", result.RowsAffected)

for _, m := range users {
fmt.Printf("%+v\n", m)
}
}

定义钩子函数

1
2
3
4
func (u *User) BeforeCreate(tx *gorm.DB) (err error){
u.ID = uuid.New()
return nil
}

备注:

  • 在插入记录之前,生成uuid(具体根据实际业务场景补充处理的业务逻辑,包括检验等)
  • 官方还提供提供很多额外的钩子函数(BeforeSave,AfterSave等)具体参考官方提供的文档

查询

简单查询

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
38
39
40
41
42
func printRecord(u *User, result *gorm.DB){
fmt.Printf("%v\n", u)
fmt.Println(result.Error, result.RowsAffected)
}

func printRecords(u []*User, result *gorm.DB){

for _, u := range u {
fmt.Println(u)
}
fmt.Println(result.Error, result.RowsAffected)

}

func simpleQueryRow(db *gorm.DB){

// 查询第一条记录(主键升序)
var firstUser *User
result := db.First(&firstUser)
printRecord(firstUser, result)

// 仅当有一个ID主键时,可直接定义User时把ID初始化
firstIDUser2 := &User{ID: "e8efff22-a497-4a88-be1e-5123eb23ff75"}
result = db.First(&firstIDUser2)
printRecord(firstIDUser2, result)

// 查询表中第一条记录(没有指定排序字段)
var firstUser2 *User
result = db.Take(&firstUser2)
printRecord(firstUser2, result)

// 查询表中最后一条记录(主键排序)
var lastUser *User
result = db.Last(&lastUser)
printRecord(lastUser, result)

// 查询当前所有记录
var users []*User
result = db.Find(&users)
printRecords(users, result)

}

备注:

  • 这几个都是按照官方给出的示例,整理了一下日常比较常用到的查询操作

条件查询

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
func condQueryRow(db *gorm.DB){

// 查询当前username为condQueryRow的第一条记录(Struct方式)
var tmpUser1 *User
result := db.Where(&User{UserName: "qNptxqb"}).First(&tmpUser1)
printRecord(tmpUser1, result)

// 查询当前username为condQueryRow的第一条记录(Map方式)
var tmpUser2 *User
result = db.Where(map[string]interface{}{"username": "qNptxqb"}).First(&tmpUser2)
printRecord(tmpUser2, result)

// 指定Century查询字段查询记录
var tmpUser3 []User
result = db.Where(&User{Century: "VII", UserName: "jaQlaFs"}, "Century").Find(&tmpUser3)
printRecords(tmpUser3, result)

// String 条件,直接写表达式
var tmpUser4 *User
result = db.Where("username = ?", "qNptxqb").First(&tmpUser4)
printRecord(tmpUser4, result)

var users []User
result = db.Where("date > ?", "2010-10-1").Find(&users)
printRecords(users, result)

// Order排序(默认升序)
var users2 []User
result = db.Where("date > ?", "2010-10-1").Order("date").Find(&users2)
printRecords(users2, result)

// 查询特定的字段,不返回所有字段
var tmpUser5 *User
result = db.Select("username", "date").Where("username = ?", "qNptxqb").First(&tmpUser5)
printRecord(tmpUser5, result)
}

备注:

  • Gorm提供了很多条件查询的场景,目前对我而言基本的查询业务逻辑都能支持。具体可能出现一些复杂的sql,后面会介绍怎么直接使用sql查询;

高级查询

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
type APIUser struct {
ID string `gorm:"primaryKey,column:id"`
UserName string `gorm:"column:username"`
FirstName string `gorm:"first_name"`
LastName string `gorm:"last_name"`
}

func advancedQueryRow(db *gorm.DB){

// 智能选择字段,如果经常只需要查询某些字段,可以重新定义小结构体
var apiUser []APIUser
result := db.Model(&User{}).Find(&apiUser)
for _, user := range apiUser{
fmt.Println(user)
}
fmt.Println(result.Error, result.RowsAffected)

// 扫描结果绑定值map[string]interface{} 或者 []map[string]interface{}
var users []map[string]interface{}
result = db.Model(&User{}).Find(&users)
for _, user := range users{
fmt.Println(user)
}
fmt.Println(result.Error, result.RowsAffected)

// Pluck查询单个列,并将结果扫描到切片
var emails []string
result = db.Model(&User{}).Pluck("email",&emails)
fmt.Println(emails)
fmt.Println(result.Error, result.RowsAffected)

// Count查询
var count int64
result = db.Model(&User{}).Where("date > ?", "2012-10-22").Count(&count)
fmt.Println(count)
fmt.Println(result.Error, result.RowsAffected)
}

备注:

  • 定义小结构体可以实现在调用API时自动选择特定字段
  • Pluck适合查询单个列,如果需要查询多个常用字段可以通过Select和Scan

更新

常用更新操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func updateRow(db *gorm.DB){
// Save会保存所有字段,即使字段是零值,如果保存的值没有主键,就会创建,否则则是更新指定记录
result := db.Save(&User{ID: "e8efff22-a497-4a88-be1e-5123eb23ff75", UserName: "zhangsan", Date: "2023-12-12"})
fmt.Println(result.Error, result.RowsAffected)

// 更新单个列
result = db.Model(&User{}).Where("username = ?", "jaQlaFs").Update("first_name", "zhangsan")
fmt.Println(result.Error, result.RowsAffected)

// 更新多个列
result = db.Model(&User{}).Where("username = ?", "zhangsan").Updates(User{FirstName: "zhangsan2", LastName: "zhangsan3"})
fmt.Println(result.Error, result.RowsAffected)

// 更新指定列(Select指定last_name)
result = db.Model(&User{}).Where("username = ?", "zhangsan").Select("last_name").Updates(User{FirstName: "zhangsan2", LastName: "zhangsan4"})
fmt.Println(result.Error, result.RowsAffected)
}

备注:

  • 具体一些更新的操作和查询类似,具体区分即可;

删除

常用删除操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func deleteRows(db *gorm.DB){

// 指定匹配字段删除数据
result := db.Delete(&User{}, map[string]interface{}{"username": "NJrauTj"})
fmt.Println(result.Error, result.RowsAffected)

result = db.Delete(&User{}, "username = ?", "NJrauTj")
fmt.Println(result.Error, result.RowsAffected)

// Where指定字段匹配删除数据
result = db.Where("username = ? and phone_number = ?", "jXQKmPv", "574-821-9631").Delete(&User{})
fmt.Println(result.Error, result.RowsAffected)

// 批量删除的两种方式
result = db.Where("email like ?", "%.com%").Delete(&User{})
fmt.Println(result.Error, result.RowsAffected)

result = db.Delete(&User{}, "email like ?", "%.com%")
fmt.Println(result.Error, result.RowsAffected)
}

备注:

  • 普通删除常用场景匹配指定单挑数据删除以及批量删除,语法和更新类似;
  • Gorm文档中有涉及禁用全局删除,即当执行不带任何条件的批量删除时就会返回错误;以及关于删除的钩子函数有实用
    场景的同学,可以看官方文档,这里不再赘述;

原生SQL和SQL生成器

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
// execSQL 执行原生SQL语句
func execSQL(db *gorm.DB){

// 将查询SQL的结果映射到指定的单个变量中
var oneUser User
result := db.Raw("SELECT * FROM user LIMIT 1").Scan(&oneUser)
fmt.Println(oneUser)
fmt.Println(result.Error, result.RowsAffected)

// 将查询SQL的批量结果映射到列表中
var users []User
result = db.Raw("SELECT * FROM user").Scan(&users)
for _, user := range users {
fmt.Println(user)
}
fmt.Println(result.Error, result.RowsAffected)

var updateUser User
result = db.Raw("UPDATE users SET username = ? where id = ?", "toms jobs", "ab6f089b-3272-49b5-858f-a93ed5a43b4f").Scan(&updateUser)
fmt.Println(updateUser)
fmt.Println(result.Error, result.RowsAffected)

// 直接通过Exec函数执行Update操作,不返回任何查询结果?
result = db.Exec("UPDATE user SET username = ? where id = ?", "toms jobs", "ab6f089b-3272-49b5-858f-a93ed5a43b4f")
fmt.Println(result.Error, result.RowsAffected)

// DryRun模式,在不执行的情况下生成SQL及其参数,可以用于准备或测试的SQL
var tmpUsers []APIUser
stmt := db.Session(&gorm.Session{DryRun: true}).Model(&User{}).Find(&tmpUsers).Statement
fmt.Println(stmt.SQL.String())
fmt.Println(stmt.Vars)
}

备注:

  • Scan函数是会将查询SQL的结果映射到定义的变量,如果不需要返回查询结果可以直接使用Exec函数执行原生SQL;
  • DryRun模式,可以直接生成SQL机器参数,但是不会直接执行;

总结

关于Gorm的CURD日常使用就介绍到这里,如果同学对Gorm感兴趣,可以接去Gorm官网,我这里只是简单介绍一下在日常业务环境经常使用的操作,具体Gorm中文文档地址。

至于Gorm后面可能还会出几篇文章介绍Gorm模型之间的关联关系,以及关联模式下的CRUD;还有关于Gorm的性能优化,具体能搭配一些常用的插件。

上述文章的源码:

https://github.com/libuliduobuqiuqiu/GoDemo/blob/master/GormDemo/gorm_demo.go