通过gorm操作mysql
GORM是Golang的ORM(Object Relational Mapping)操作库,使用上主要就是把struct类型和数据库表记录进行映射,操作数据库的时候不需要直接手写Sql代码,本例子以gorm版本1.23.8为例子
GORM库地址:https://github.com/go-gorm/gorm
官方文档地址:https://gorm.io/zh_CN/docs/
一、gorm安装:
操作MySQL需要安装两个包:
- MySQL驱动包
- GORM包 使用go get命令安装依赖包
//安装MySQL驱动
go get -u gorm.io/driver/mysql
//安装gorm包
go get -u gorm.io/gorm
导入包:
import (
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
二、gorm模型定义:
1、ORM简介:
ORM框架操作数据库都需要预先定义模型,模型可以理解成数据模型,作为操作数据库的媒介
- 从数据库读取的数据会先保存到预先定义的模型对象,然后我们就可以从模型对象得到我们想要的数据
- 插入数据到数据库也是先新建一个模型对象,然后把想要保存的数据先保存到模型对象,然后把模型对象保存到数据库
gorm模型定义是通过struct实现的,这样我们就可以通过gorm库实现struct类型和mysql表数据的映射
注:gorm负责将对模型的读写操作翻译成sql语句,然后gorm再把数据库执行sql语句后返回的结果转化为我们定义的模型对象
2、模型定义:
gorm模型定义主要就是在struct类型定义的基础上增加字段标签说明实现,首先定义一个订单表:
CREATE TABLE `productorder` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增ID,订单Id',
`name` varchar(50) NOT NULL COMMENT '订单名称',
`quantity` varchar(50) NOT NULL COMMENT '订单数量',
`createtime` int NOT NULL DEFAULT 0 COMMENT '订单创建时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT '商品订单';
模型定义如下(注:结构体字段名首字母一定要大写):
type productorder struct {
ID int //表字段名为id
Name string //表字段名为name
Quantity string //表字段名为quantity
Createtime int //表字段名为create_time
}
默认gorm对struct字段名使用Snake Case命名风格转换成mysql表字段名,因此转换后都是小写的名字,多个单词之间用下划线分隔,例如CreateTime 转换为就是create_time
注意:Snake Case命名风格,就是各个单词之间用下划线(_)分隔,例如: CreateTime的Snake Case风格命名为create_time
注意:如果结构体名为productOrder,那么对应的数据库表名就是product_order
3、gorm模型标签:
上述模型中CreateTime映射到的表字段为create_time,如果要变成正常的createtime,可通过模型标签实现,语法如下:
gorm:"标签定义" //多个标签定义可以使用分号(;)分隔
gorm常用标签如下:
| 标签 | 说明 | 例子 |
| column | 指定列名 | gorm:”column:createtime” |
| primaryKey | 指定主键 | gorm:”column:id;primary_key” |
| – | 忽略字段 | gorm:”-” 可以忽略struct字段,被忽略的字段不参与gorm的读写操作 |
上述模型修改后的内容如图:

注意:在gorm和column中不能出现空格,否则会失效
4、定义表名:
默认情况下,模型对应的表名为复数形式,比如模型名为productorder,那么对应的表名为productorders,如果要去掉复数形式,有两种方式:
第一种方式,通过调用Tabler接口的TableName()函数,返回一个新的表名,如下:
func (productorder) TableName() string {
return "productorder" //返回单数形式的表名
}
默认情况下都给模型定义表名,有时候定义模型只是单纯的用于接收手写sql查询的结果,这个时候是不需要定义表名;手动通过gorm函数Table()指定表名,也不需要给模型定义TableName函数
- 在gorm 1.19版本,TableName方法在gorm目录下的scope.go里
- 在gorm 1.23版本, TableName方法在gorm目录下的schema.go里
第二种方式:通过在连接数据库时定义SingularTable: true实现,可参考下面文章
三、gorm连接数据库:
gorm支持多种数据库,本文主要讲解连接mysql,主要分为两个步骤:
- 配置DSN (Data Source Name)
- 使用gorm.Open连接数据库
1、配置DSN,语法格式如下:
[username[:password]@][protocol[(address)]]/dbname[?param1=value1&...¶mN=valueN]
user := "root" //用户
password := "123456789" //密码
dbName := "test" //数据库名
host := "127.0.0.1" //主机名
port := "3306" //端口
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8&parseTime=True&loc=Local", user, password, host, port, dbName)
//charset=utf8 客户单字符集编码为utf8
//parseTime=true,支持把数据库datetime和date类型转换为golang的time.Time类型
//loc=Local 使用系统本地市区
//readTimeout 读超时时间,0代表不限制,本例子中没设置此参数,可根据需要添加
//writeTimeout - 写超时时间,0代表不限制,本例子中没设置此参数,可根据需要添加
2、使用gorm.Open连接数据库:
var err error
var db *grom.DB //定义指针变量
db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{
NamingStrategy: schema.NamingStrategy{
SingularTable: true, //设置表名为单数形式
TablePrefix: "sys_", //设置表前缀
},
})
if err != nil {
log.Fatal(err)
}
- gorm.Open()返回类型即为*gorm.DB和error,可通过go doc gorm Open查询文档
- schema需要导入包gorm.io/gorm/schema
- SingularTable: true 设置表名为单数形式,即为上文中设置表名的第二种方法
- TablePrefix:设置表前缀
注:gorm.Config{} 如果没有特别设置,可以留空
3、连接池:
在高并发实践中,为了提高数据库连接的使用率,避免重复建立数据库连接带来的性能消耗,会经常使用数据库连接池技术来维护数据库连接
配置如下:
sqlDB,_ := db.DB()
//SetMaxIdleConns 设置空闲连接池中连接的最大数量
sqlDB.SetMaxIdleConns(10)
//SetMaxOpenConns 设置打开数据库连接的最大数量
sqlDB.SetMaxOpenConns(100)
//SetConnMaxLifetime 设置了连接可复用的最大时间
sqlDB.SetConnMaxLifetime(time.Hour)
注:db.DB()将返回一个通用的数据库接口*sql.DB,直接通过go doc sql即可查看用法,使用连接池技术后,千万不要使用完db后调用db.Close关闭数据库连接,这样会导致整个数据库连接池关闭,导致连接池没有可用的连接。
完整的连接数据库并执行命令创建sql的内容如下:
1 package main
2
3 import (
4 "fmt"
5 "gorm.io/driver/mysql"
6 "gorm.io/gorm"
7 "gorm.io/gorm/schema"
8 "log"
9 "time"
10 )
11
12 //注意:如果此结构体需要导出,那么productorder的首字母需要大写
13 type productorder struct {
14 ID int `gorm:"primary_key"`
15 Name string //表字段名为name
16 Quantity string //表字段名为quantity
17 CreateTime int `gorm:"column:createtime"`
18 }
19
20 //func (productorder) TableName() string {
21 // return "productorder"
22 //}
23
24 var db *gorm.DB
25 //开发时,一般都是将初始化sql语句放在init()函数中,先执行
26 func init() {
27 user := "root" //用户
28 password := "5182086abcD#" //密码
29 dbName := "test" //数据库名
30 host := "127.0.0.1" //主机名
31 port := "3306" //端口
32 dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8&parseTime=True&loc=Local", user, password, host, port, dbName)
33 //charset=utf8 客户单字符集编码为utf8
34 //parseTime=true,支持把数据库datetime和date类型转换为golang的time.Time类型
35 //loc=Local 使用系统本地市区
36 //readTimeout 读超时时间,0代表不限制
37 //writeTimeout - 写超时时间,0代表不限制
38 var err error
39 db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{
40 NamingStrategy: schema.NamingStrategy{ //schema包的结构体NamingStrategy
41 SingularTable: true, //设置表名为单数形式
42 TablePrefix: "sys_", //设置表前缀
43 },
44 })
45 if err != nil {
46 log.Fatal(err)
47 }
48 sqlDB, err := db.DB()
49 //SetMaxIdleConns 设置空闲连接池中连接的最大数量
50 sqlDB.SetMaxIdleConns(10)
51 //SetMaxOpenConns 设置打开数据库连接的最大数量
52 sqlDB.SetMaxOpenConns(100)
53 //SetConnMaxLifetime 设置了连接可复用的最大时间
54 sqlDB.SetConnMaxLifetime(time.Hour)
55 }
56 func main() {
57 p := productorder{
58 ID: 1,
59 Name: "phone",
60 Quantity: "100部",
61 CreateTime: 20220728,
62 }
63 db.Create(&p)
64 }
注意:如果第20-22行不注释掉,那么第41和42行设置就不会生效,注释后,表前缀就要变为sys_productorder,表名即为模型名的单数形式,查询sql后如下:

4、gorm调试模式:
为了方便调试,了解gorm操作到底执行了怎么样的sql语句,开发的时候需要打开调试日志,这样gorm会打印出执行的每一条sql语句
使用Debug函数执行即可,如下:
db.Debug().Create(&p) //调用Debug()后在调用执行命令语句,执行即可看到sql语句
注:调试时候可以加Debug(),实际生产不用加
四、自动更新时间:
1、通过gorm包的Model内置好的字段实现:
GORM 约定使用 CreatedAt、UpdatedAt 追踪创建/更新时间。如果定义了这种字段,GORM 在创建、更新时会自动填充当前时间,将上面例子中的模型修改如下:

- 创建时间字段为CreatedAt,类型为time.Time,映射到表中字段为createdat
- 更新时间字段为UpdatedAt,类型为time.Time,映射到表中字段为updatedat
注意:字段不可以乱写,必须写为CreatedAt,UpdatedAt,这两个字段是gorm自带的结构体Model中的字段,如果不重新写,也可以直接通过继承的方式,只需要在自定义结构体中gorm.Model即可
表结构中添加对应的字段,如图:

在实例化结构体模型的时候,不需要指定CreatedAt,UpdatedAt字段,只需要指定原有字段即可,如图:

执行命令go run productorder.go,查看表中数据,创建时间和更新时间都自动填充为当前时间,如图:

将命令修改为更新命令,将phone5改为phone1,命令如下:
db.Debug().Model(&productorder{}).Where("name = ?","phone5").Update("name","phone1")
再次查看表中数据,发现name改了后,对应的更新时间也变化了,如图:

注:通过结构体创建数据可以自动创建时间(createdat),不需要指定字段,但是通过map创建数据则需要指定createdat,否则无法显示创建时间,通过map更新数据不需要指定updatedat
2、通过自定义字段实现:
要通过自定义字段实现,需要配置 autoCreateTime、autoUpdateTime 标签
修改结构体模型,添加自定义创建时间字段和自定义修改时间字段,并配置autoCreateTime、autoUpdateTime 标签,如图:

在数据库表中创建对应的映射字段customupdatetime、customcreatetime,如图:

通过如下命令执行创建sql,不需要添加自定义时间字段,如图:

查看数据库表,看到时间字段已经自动填充,如图:

再次执行如下命令更新数据库表:
db.Debug().Model(&productorder{}).Where("name = ?","phone5").Update("name","phone1")
查看数据库表更新时间字段,已经改变,如图:

五、常用的gorm命令如下:
创建数据
1、根据结构体指针创建单条记录,如图:

注:productorder为结构体
2、批量插入数据,需要将一个切片传递给Create方法,首先看下没有批量插入数据之前的表内容:

定义结构体类型切片,将结构体添加到切片中,然后插入到数据库,如图:

插入后的内容如图:

3、通过map来创建单条数据,如图:

注:需要通过Model方法执行要运行的数据库操作模型,可以理解为指定数据表,Create()中尽量使用指针,本例子中忘记写了指针符号&
查看数据库内容如下:

4、通过map创建多条数据,如图:

查看sql数据如下:

注:通过map方式创建数据的时候,需要在map中定义createdat字段,并设置值为time.Now(),如上图,这样才可以在数据库中显示创建时间,通过map更新数据则不需要,执行后更新时间会自动变化
查询数据
1、查询表中第一条数据,注意要使用结构体指针,如图:

执行结果如下:

注:查询后的数据会保存到模型中,也就是结构体中,因此可直接获取值
2、查询表中最后一条数据,如图:

注意:First()和Last()会根据主键排序,并且只有在目标struct是指针或者通过db.Model()指定model时候,此方法才有效
First(),Take(),Last()方法如果查询不到记录,会返回ErrRecordNotFound错误,如图:


注:看语句结尾要用.Error,前面通过err接收,如果要避免ErrRecordNotFound错误,可以使用下面的Find()方法
3、查询全部数据,用Find()方法,如图:

运行后结果如下:

注:Find()方法可以接受struct和slice数据
5、查询全部数据中,name=phone5的数据,通过切片遍历出来,如图:

执行结果如下:

6、通过Error来判断语法是否错误,下面估计将=写成==,如图:

执行结果如下:

7、通过Select根据Where条件查询id字段数据,如下:

运行结果如下:

注意:上图中根据Select来查询id字段,那么实际查询到数据后,会将id字段映射到结构体模型的ID字段,其余的字段并没有查询,因此其余字段的值还是默认的零值,如果此时打印SystemType的值将为空的字符串(如果是全部查询,那么每个字段都可以打印出来值)
更新数据
本例子以数据库test中的表abc为例子,如图:

1、更新多个列值:
下面例子表示更新id为1列名为system_type和ip_address列的值,如图:

运行结果如下,已经更新成功:

注意:更新多个列用的是Updates方法,Updates 方法支持 struct 和 map[string]interface{} 参数,当使用struct更新时,GORM只会更新非零值的字段
2、更新单个列值
还是以上图中为例,更新id为1的列hostname的值为window100,如图:

运行后结果如下,已经更新成功:

注:更新单个列用的是update,并且需要指定字段名hostname
更多用法如下:
//查询全部数据中,name=phone5的数据的第一条数据
db.Where("name = ?","phone5").First(&p)
//查询全部数据中,name不等于phone5的所有记录
db.Where("name <> ?","phone5").Find(&p)
执行原生SQL
原生SQL就是直接操作数据库的SQL语句,通过gorm执行此语句,指定原生SQL有两种方式,一种是Raw(),一种是Exec()
通过Raw()来执行SQL:
首先查看数据库temp_database的表role内容如下:

1、定义结构体,查询数据库获取role_name的值,如图:

定义结构体切片,用于存储从数据库中查询的数据,如图:

- 39行:通过db.Raw()来执行原生SQL,通过Scan映射到对应的切片中
最后打印的结果就是结构体切片,如图:

2、根据条件,获取id为10000的用户角色,如图:


3、通过两个条件进行判断,并通过query来分行写语句,如图:

注:10000和”init”分别对应id和role_flag后面的值
4、通过db.Raw()来指定update语句,将id为10000的role_flag修改为abcd,如图:


通过Exec()指定SQL:
1、指定update命令,修改id为10000的role_flag的值为gong,如图:


注:db.Exec()后是没有Scan()的,此处和Raw()不一样的
2、gorm也支持表达式,可进行运算,下面修改id为表达式的值,如图:


Raw()和Exec()的区别:
- Raw() :可执行原生SQL或可执行命令,并返回结果或影响的行数,然后调用Scan()将结果映射到结构体中,一般用于查询数据SELECT
- Exec() :可执行原生SQL或可执行命令,无需映射到结构体,不返回结果,一般用于INSERT、update、delete语句
例句:通过RAW()执行原生SQL来判断主从状态,如下:
package main
import (
"fmt"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"log"
"time"
)
// 此处的结构体名可自定义
type slavestatus struct {
SlaveIORunning string `gorm:"column:Slave_IO_Running"`
SlaveSQLRunning string `gorm:"column:Slave_SQL_Running"`
SecondsBehindMaster int `gorm:"column:Seconds_Behind_Master"`
}
var db *gorm.DB
func init() {
user := "root"
password := "Abcd1234"
dbName := "mysql" //此时这个库名也可以随便写,只要存在就行
host := "127.0.0.1"
port := "3306"
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8&parseTime=True&loc=Local", user, password, host, port, dbName)
var err error
db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
log.Fatal(err)
}
sqlDB, err := db.DB()
//SetMaxIdleConns 设置空闲连接池中连接的最大数量
sqlDB.SetMaxIdleConns(10)
//SetMaxOpenConns 设置打开数据库连接的最大数量
sqlDB.SetMaxOpenConns(100)
//SetConnMaxLifetime 设置了连接可复用的最大时间
sqlDB.SetConnMaxLifetime(time.Hour)
}
func main() {
var ss slavestatus
db.Raw("show slave status").Scan(&ss)
fmt.Println("从库IO线程:", ss.SlaveIORunning)
fmt.Println("从库SQL线程:", ss.SlaveSQLRunning)
fmt.Println("从库落后主库时间:", ss.SecondsBehindMaster)
}


