Gorm
1. 准备工作
首先进入终端下载我们需要的包(确保go和mysql安装完成,并设置了环境变量)
arduino
go get -u gorm.io/driver/mysql
go get -u gorm.io/gorm
有两份官方文档有对 GORM 更详细的讲解。
- 创建 | GORM - The fantastic ORM library for Golang, aims to be developer friendly.(中文)
- go-sql-driver/mysql: Go MySQL Driver is a MySQL driver for Go's (golang) database/sql package (github.com)
2. 连接数据库
这里以mysql为例,其他数据库请看官方文档。
go
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{})
}
user -> 用户名
pass -> 密码
127.0.0.1 -> mysql默认本地地址,也就是localhost
3306 -> mysql默认端口
dbname -> 数据库名称
charset -> 字符集编码
parseTime -> 把数据库datetime和date类型转换为golang的time.Time类型
loc -> 使用系统本地时区
gorm.config{}
中可以进行一些高级配置,详情请看官方文档。
3. 结构体与mysql table 的映射关系
go的orm框架可以让我们在用go语言实现数据库操作的时候不使用sql语言,简单易上手,提高开发效率。
下面是gorm中go与mysql的对应关系
GO | mysql |
---|---|
结构体 | 数据表 |
结构体实例 | 数据行 |
结构体字段 | 字段 |
例如,一个结构体:
go
type Student struct {
ID int
Name string
age int
ExternalCharacter string
}
gorm 会将其映射为
typescript
CREATE TABLE `students` (
`id` bigint AUTO_INCREMENT,
`name` longtext,
`external_character` longtext,
PRIMARY KEY (`id`)
)
这里可以发现,结构体字段中的大写都变成了小写 ,age完全消失 ,Student
也变成了students
,ExternalCharacter
变成了external_character
。这里涉及到了gorm的映射规定。
具体可看模型定义 | GORM - The fantastic ORM library for Golang, aims to be developer friendly.
简单来说,就是咱要首字母大写,会按驼峰命名法 给我们加 _
符号,默认ID
为主键,表名会变成复数。
再看数据类型,明显这是不知道有多大所以往大了去调。
当然,这些我们也是能够一一自定义的。
3.1. 表名:
实现Tabler接口 中的TableName函数。
例如,针对上述的结构体,我们可以这样写:
go
func (s *Student) TableName() string {
return "student"
}
这样表名就会被映射为student而不是students
3.2 字段名:
我们可以在字段后面打上gorm tag,tag有很多用途,比如标记主键,指定not null等等,具体请看官方文档。
这里介绍自定义字段名,还是以上述的结构体为例:
go
type Student struct {
ID int `gorm:"column:序号"`
Name string
age int
ExternalCharacter string `gorm:"column:externalCharacter"`
}
如果我们改成这样的话,映射的表就是这样的:
typescript
CREATE TABLE `student` (
`序号` bigint AUTO_INCREMENT,
`name` longtext,
`externalCharacter` longtext,
PRIMARY KEY (`序号`)
)
3.2 数据类型
将longtext
改成 varchar(255)
,也是通过tag实现的。通过size:255
来确定最大长度
把bigint
改成int
只需要将int
改成int32
就行了,因为go中,咱64位的电脑默认int
是int64
例如:
go
type Student struct {
ID int32
Name string
age int
ExternalCharacter string `gorm:"column:externalCharacter; size:255"`
}
映射出来的表就是这样的
go
CREATE TABLE `student` (
`id` int AUTO_INCREMENT,
`name` longtext,
`externalCharacter` varchar(255),
PRIMARY KEY (`id`)
)
tag中定义多种内容,中间打;
号就行了
4. 创建table
我们可以通过 db.AutoMigrate
和 db.Migrator().CreateTable()
方法来创建table。
AutoMigrate 方法:
使用 db.AutoMigrate
方法是一种自动创建和迁移数据库表的方式。它会根据你的 GORM 模型定义,自动检查数据库表是否存在,如果不存在则创建表,如果表结构有变化则进行迁移。这种方式适用于开发过程中的快速迭代和维护数据库结构的情况。
示例:
go
// 自动创建和迁移表
err := db.AutoMigrate(&Student{})
// 这里的db就是上述gorm.open的db哈
if err != nil {
panic("创建/迁移表格失败, error = " + err.Error())
}
Migrator().CreateTable 方法:
使用 db.Migrator().CreateTable
方法是一种手动创建数据库表的方式。你需要显式地指定要创建的表,它不会检查是否已经存在,而是直接创建表。这种方式适用于在特定情况下需要手动控制表创建过程的情况。
示例:
go
// 手动创建表
err := db.Migrator().CreateTable(&Student{})
if err != nil {
panic("创建表格失败, error = " + err.Error())
}
区别总结:
AutoMigrate
方法自动根据模型定义创建和迁移表,适用于自动化和快速迭代。Migrator().CreateTable
方法手动创建表,适用于需要手动控制创建过程的情况。
在实际应用中,你可以根据项目需求选择合适的方法。通常情况下,开发和调试阶段可能更适合使用 AutoMigrate
方法,而特定场景下的手动控制可能需要使用 Migrator().CreateTable
方法。
5. 增
以下列结构体为例:
go
type Student struct {
ID int
Name string
Age int
ExternalCharacter string `gorm:"column:externalCharacter; size:255"`
}
func (s *Student) TableName() string {
return "student"
}
5.1. db.Create(结构体对象)
go
lisi := Student{2, "lisi", 18, "modest"}
result := db.Create(&lisi)
if result.Error != nil {
panic("插入字段失败, error = " + result.Error.Error())
}
fmt.Println(result.RowsAffected) // 返回影响的行数 1
等于INSERT INTO
student(
name,
age,
externalCharacter,
id) VALUES ('lisi',18,'modest',2)
当然也可以同时增添多个字段
例如:
go
students := []Student{
{ID: 3, Name: "wangwu", Age: 20, ExternalCharacter: "generous"},
{ID: 4, Name: "xuliu", Age: 21, ExternalCharacter: "liberal"},
}
result := db.Create(&students)
if result.Error != nil {
panic("增加字段失败")
}
fmt.Println(result.RowsAffected) // 返回影响的行数 2
等于INSERT INTO
student(
name,
age,
externalCharacter,
id) VALUES ('wangwu',20,'generous',3),('xuliu',21,'liberal',4)
5.2. db.Select(指定字段).Create(结构体对象)
go
lisi := Student{2, "lisi", 18, "modest"}
result := db.Select("ID", "Name", "Age").Create(&lisi)
if result.Error != nil {
panic("插入字段失败, error = " + result.Error.Error())
}
fmt.Println(result.RowsAffected) // 返回影响的行数 1
等于INSERT INTO
student(
name,
age,
id) VALUES ('lisi',18,2)
5.3. db.Omit(忽略字段).Create(结构体对象)
go
lisi := Student{2, "lisi", 18, "modest"}
result := db.Omit("ExternalCharacter").Create(&lisi)
if result.Error != nil {
panic("插入字段失败, error = " + result.Error.Error())
}
fmt.Println(result.RowsAffected) // 返回影响的行数 1
也等于INSERT INTO
student(
name,
age,
id) VALUES ('lisi',18,2)
5.4. 原生sql语句
go
lisi := Student{2, "lisi", 18, "modest"}
result := db.Exec("INSERT INTO `student` (`name`,`age`,`externalCharacter`,`id`) VALUES ('lisi',18,'modest',2)")
if result.Error != nil {
panic("插入字段失败, error = " + result.Error.Error())
}
fmt.Println(result.RowsAffected) // 返回影响的行数 1
6. 查
之后所有的操作都是基于我的这份表实现的。
我的表
我现在的表如下:
6.1 where
我们这里使用where
函数 设置条件
func (db *DB) Where(query interface{}, args ...interface{}) (tx *DB)
参数说明:
参数名 | 说明 |
---|---|
query | sql语句的where子句, where子句中使用问号(?)代替参数值,则表示通过args参数绑定参数 |
args | where子句绑定的参数,可以绑定多个参数 |
例如db.Where("id in (?)", []int{1,2,3,4})
后面的 select、having 语句这些 query 的也同样适用
6.2 Take
取走第一条查询信息
scss
s1 := Student{}
db.Where("age = 18").Take(&s1)
fmt.Println(s1)
这里我们是先创建了一个结构体对象,查询的结果直接赋予了该对象
输出:
{1 zhangsan 18 humble}
等于:
sql
SELECT * FROM `student` WHERE age = 18 LIMIT 1
注意这个limit 1,我这里有两个age = 18的,但是take只取一个,一般是第一个
6.3 First
根据主键正序排序后,查询的第一条数据
scss
s1 := Student{}
db.Where("age = 18").First(&s1)
fmt.Println(s1)
输出:
{1 zhangsan 18 humble}
等于:
vbnet
SELECT * FROM `student` WHERE age = 18 ORDER BY `student`.`id` LIMIT 1
6.4 Last
根据主键倒序排序后,查询最后一条记录
scss
s1 := Student{}
db.Where("age = 18").Last(&s1)
fmt.Println(s1)
输出:
{5 feixin 18 frantic}
等于:
sql
SELECT * FROM `student` WHERE age = 18 ORDER BY `student`.`id` DESC LIMIT 1
6.5 Find
这里就不是查询一条记录了,可以查询多条记录
这里因为多条记录,一个结构体对象咱也装不完,所以需要改成使用结构体切片(咱也不知道会查出多少记录)
还有一点就是find
如果没有找到记录是不会报err 的,但是take、first、last会,因为他们都要取走一条数据。
scss
s1 := []Student{}
db.Where("age = 18").Find(&s1)
fmt.Println(s1)
输出:
css
[{1 zhangsan 18 humble} {5 feixin 18 frantic}]
等于:
sql
SELECT * FROM `student` WHERE age = 18
再举一个使用where args
的例子
scss
s1 := []Student{}
db.Where("id in (?)", []int{1, 2, 3, 4}).Find(&s1)
fmt.Println(s1)
输出:
css
[{1 zhangsan 18 humble} {2 lisi 19 modest} {3 wangwu 20 generous} {4 xuliu 21 liberal}]
等于:
sql
SELECT * FROM `student` WHERE id in (1,2,3,4)
6.6 Pluck
查询一列的值
这个函数返回切片类型,同时需要一个模型Model
,就是我们之前定义的结构体模型
go
col := []string{}
db.Model(&Student{}).Pluck("ExternalCharacter", &col)
for _, i := range col {
fmt.Println(i)
}
输出:
humble
modest
generous
liberal
frantic
等于:
go
SELECT `externalCharacter` FROM `student`
关于这个model其实之前的所有操作都可以去写成这个model形式,不过可以省略,在这里就不行了因为你这里没有任何有关student结构体的信息,它甚至都不知道你是哪一个表,那肯定就查找不了咯,所以这里要写这个model。
比如之前的db.Where("age = 18").Find(&s1)
也可以写成
db.Model(&Student{}).Where("age = 18").Find(&s1)
然后这里如果不使用字符串切片,使用结构体student切片的话,就可以不使用model,每一个student对象中没在查找范围的值会变成默认值,也就是int 变 0, string 变 空值。
例如:
go
col := []Student{}
db.Pluck("ExternalCharacter", &col)
fmt.Println(col)
输出:
css
[{0 0 humble} {0 0 modest} {0 0 generous} {0 0 liberal} {0 0 frantic}]
等于:
go
SELECT `ExternalCharacter` FROM `student`
所以,model的作用 ok 了吗 😝
6.7 Select
Pluck
是 一列,那如果我需要多列信息呢?select来帮忙
例如,我想要查询 name
和 age
两列
go
result := []Student{}
err = db.Select("Age", "Name").Find(&result).Error
if err != nil {
panic("查询失败, error = " + err.Error())
}
for _, i := range result {
fmt.Println(i.Name, i.Age)
}
之的代码没有加err,汗,懒得加了,反正err是这样加😉
输出:
zhangsan 18
lisi 19
wangwu 20
xuliu 21
feixin 18
等于:
go
SELECT `age`,`name` FROM `student`
select 中也可以使用mysql中的聚集函数
以 avg
为例:
go
var averAge float32
err = db.Model(&Student{}).Select("avg(age)").Pluck("age", &averAge).Error
if err != nil{
panic("查询失败, error = " + err.Error())
}
fmt.Println(averAge)
输出:
19.2
等于:
sql
SELECT avg(age) FROM `student`
使用方法和mysql中差不多,具体看官方文档哈
6.8 Order
看单词儿也大概知道了,是排序用的,可以将搜索结果进行排序
例如我按年龄排序:
go
result := []Student{}
err = db.Order("age asc").Select("Age", "Name").Find(&result).Error
if err != nil {
panic("查询失败, error = " + err.Error())
}
for _, i := range result {
fmt.Println(i.Name, i.Age)
}
这其实就是之前那个加了一个order
输出:
zhangsan 18
feixin 18
lisi 19
wangwu 20
xuliu 21
等于:
sql
SELECT `age`,`name` FROM `student` ORDER BY age asc
6.9 Limit
可以限制查找条数,比如上面这个,如果想要只找出前三条的话,可以这样写:
sql
result := []Student{}
err = db.Limit(3).Order("age asc").Select("Age", "Name").Find(&result).Error
if err != nil {
panic("查询失败, error = " + err.Error())
}
for _, i := range result {
fmt.Println(i.Name, i.Age)
}
输出:
zhangsan 18
feixin 18
lisi 19
等于:
sql
SELECT `age`,`name` FROM `student` ORDER BY age asc LIMIT 3
6.10 Offset
这个可以过滤掉前几个,还是以上面的为例,如果我想不要zhangsan 和 feixin,取后面三个,那可以这样写:
sql
result := []Student{}
err = db.Offset(2).Limit(3).Order("age asc").Select("Age", "Name").Find(&result).Error
if err != nil {
panic("查询失败, error = " + err.Error())
}
for _, i := range result {
fmt.Println(i.Name, i.Age)
}
输出:
lisi 19
wangwu 20
xuliu 21
等于:
sql
SELECT `age`,`name` FROM `student` ORDER BY age asc LIMIT 3 OFFSET 2
有没有一种反复套娃的赶脚,是这样的没错。🤔
6.11 Count
这个就是返回查询相对应了多少条行数,比如我的表里有两个18岁的小伙子,查查他们的
go
var count int64
err = db.Model(&Student{}).Where("age = 18").Count(&count).Error
// 这里记得where要写在count前面
if err != nil {
panic("查询失败, error = " + err.Error())
}
fmt.Println(count)
输出:
2
等于:
sql
SELECT count(*) FROM `student` WHERE age = 18
6.12 Group 与 Having
Group
和 Having
一般联合使用,和sql中也是一样的,因为where
没法对聚合函数进行操作
在 GORM 中,使用 Group
函数时,通常需要搭配 Select
函数一起使用,以指定你希望在分组操作中选择的字段。这是因为在 SQL 查询中,使用 GROUP BY
进行分组时,通常需要明确指定分组后所需的字段。
所以Group
, Having
, Select
三者一般一起使用。
简而言之,就是Select
用了聚集函数,然后Group
分组,再用Having
进行筛选
比如我现在要对我的表中的各个 age
的人数进行统计,首先我的表中18的有两人,19,20,21的各一人
go
type Result struct {
Age int
Numbers int
}
var results []Result
err = db.Model(&Student{}).Select("age, count(*) as numbers").
Group("age").Having("numbers > 0").Find(&results).Error
if err != nil {
panic("查询失败, error = " + err.Error())
}
fmt.Println(results)
这里我是定义了一个Result
结构体存储结果, count(*) as numbers
是计算这一列的数目,并取一个别名为numbers
, 这里所有这些都要用引号括起来,因为实际上他就是要转化为sql语句(可以仔细对照一下 下文中的等于啥sql语句)。最后老方法用Find
取结果并赋值给了results
, 注意这里赋值也是映射赋值,你结构体里面的字段名一定要大写。
输出:
css
[{18 2} {19 1} {20 1} {21 1}]
等于:
sql
SELECT age, count(*) as numbers FROM `student` GROUP BY `age` HAVING numbers > 0
6.13 原生sql语句
就是上面的例子,如果使用sql语句的话,也可以这样写
go
type Result struct {
Age int
Numbers int
}
var results []Result
sql := " SELECT age, count(*) as numbers FROM `student` GROUP BY `age` HAVING numbers > 0"
err = db.Raw(sql).Find(&results).Error // 这个Find 应该要换成 Scan
if err != nil {
panic("查询失败, error = " + err.Error())
}
fmt.Println(results)
之前在增 中 使用的 Exec
方法是只返回语句执行情况,不会返回结果集,这里需要查询结果,我们改用Raw
结果是一样的,就不表了,这里说一下 Find
和Scan
Find
方法:Find
方法用于从数据库中查询数据并将结果映射到指定的结构体切片中。它适用于正常的 GORM 查询操作,根据条件从数据库中检索数据并进行映射。Scan
方法:Scan
方法用于将结果扫描到指定的结构体切片中,而不需要进行查询操作。它通常用于执行原生 SQL 查询,并将查询结果映射到结构体中。
意思就是Find
和Scan
差不多,但是这里我在使用原生sql语句,所以应该使用Scan
7. 删
删就很简单了,我们可以先查出来再删,也可以根据主键删除
还是这张表
现在我要把这个frantic的家伙给删除
可以先找到他
go
s := Student{}
err = db.Where("externalCharacter = 'frantic'").Find(&s).Error
if err != nil {
panic("查询失败, error = " + err.Error())
}
err = db.Delete(s).Error
if err != nil {
panic("删除失败, error = " + err.Error())
}
如果知道主键的话也可以直接
go
err = db.Delete(&Student{}, 5).Error
if err != nil {
panic("删除失败, error = " + err.Error())
}
等于:
go
DELETE FROM `student` WHERE `student`.`id` = 5
8. 改
还是这张表
8.1 Save
我们可以直接对结构体的字段进行修改然后进行save
操作映射到mysql中同步更改
比如我这里把 feixin 的 age 改为 19
go
s := Student{}
err = db.Where("id = ?", 5).Find(&s).Error
if err != nil {
panic("查询失败, error = " + err.Error())
}
s.Age = 19
err = db.Save(&s).Error
if err != nil {
panic("更新失败, error = " + err.Error())
}
等于:
go
UPDATE `student` SET `name`='feixin',`age`=19,`externalCharacter`='frantic' WHERE `id` = 5
8.2 Update
上面的也可以这样写
go
s := Student{}
err = db.Where("id = ?", 5).Find(&s).Error
if err != nil {
panic("查询失败, error = " + err.Error())
}
err = db.Model(&s).Update("age", 19).Error
if err != nil {
panic("更新失败, error = " + err.Error())
}
等于:
go
UPDATE `student` SET `age`=19 WHERE `id` = 5
8.3 Updates
可以更新多列值
go
s := Student{}
err = db.Where("id = ?", 5).Find(&s).Error
if err != nil {
panic("查询失败, error = " + err.Error())
}
err = db.Model(&s).Updates(Student{
Name: "feixin",
Age: 18,
}).Error
if err != nil {
panic("更新失败, error = " + err.Error())
}
等于:
go
UPDATE `student` SET `name`='feixin',`age`=18 WHERE `id` = 5
8.4 表达式
比如我把所有人的年龄都加1
less
err = db.Model(&Student{}).Where("age > ?", 0).Update("age", gorm.Expr("age + ?", 1)).Error
if err != nil {
panic("更新失败, error = " + err.Error())
}
等于:
sql
UPDATE `student` SET `age`=age + 1 WHERE age > 0
在 GORM 中,使用 Update
方法时,需要提供一个条件来指定要更新哪些记录。所以这里需要写where
语句,不然会报err,之前的没有没有使用是因为Model
里填的是结构体对象,实际上就算已经指定了条件。
当然,如果不想使用where的话也是有办法的,我们可以启用 AllowGlobalUpdate
模式
vbscript
err = db.Session(&gorm.Session{AllowGlobalUpdate: true}).Model(&Student{}).Update("age", gorm.Expr("age + ?", 1)).Error
if err != nil {
panic("更新失败, error = " + err.Error())
}
等于:
go
UPDATE `student` SET `age`=age + 1
9.总结
GORM(Go Object Relational Mapping) 是 Go 编程语言中的一个 ORM(对象关系映射)库,它用于简化在 Go 应用程序中与关系型数据库进行交互的过程。ORM 是一种编程技术,用于将数据库表和数据映射到程序中的对象,以便以面向对象的方式操作数据库。
GORM 的作用:
- 简化数据库操作: GORM 提供了一种更直观和简单的方式来执行数据库操作,使开发人员可以使用面向对象的语法,而不需要编写大量的 SQL 查询语句。
- 提高开发效率: 使用 GORM 可以减少手动编写 SQL 查询和处理数据库连接的工作量,从而提高开发效率。
- 避免 SQL 注入: GORM 会自动将参数化查询应用到 SQL 查询中,从而减少 SQL 注入的风险。
- 数据库无关性: GORM 支持多种数据库,包括 MySQL、PostgreSQL、SQLite 等,使得应用程序在不同数据库之间切换更加容易。
- 模型定义: GORM 允许你通过结构体定义数据模型,从而将数据库表映射到 Go 的结构体上。
GORM 的优点:
- 简化查询操作: GORM 提供了丰富的查询方法,包括条件查询、排序、分页等,使得查询操作变得简单易用。
- 自动映射: GORM 可以自动将数据库表的记录映射到 Go 结构体,大大减少了手动的映射工作。
- 事务支持: GORM 提供了事务管理功能,使你能够以一种安全的方式执行数据库操作。
- 钩子(Hooks)支持: GORM 支持在模型对象的生命周期事件中执行钩子函数,例如在创建、更新、删除等操作前后执行特定的逻辑。
GORM 的缺点:
- 性能: 相对于手动编写 SQL 查询,ORM 框架可能会引入一定的性能开销。尤其是在大规模数据操作时,一些复杂查询可能会影响性能。
- 学习曲线: 尽管 GORM 试图简化数据库操作,但仍然需要学习其 API 和用法。对于新手来说,掌握 ORM 可能需要一些时间。
- 灵活性: 有时候复杂的查询需求可能无法直接使用 GORM 提供的方法,需要编写原始 SQL 查询来满足特定需求。
综合考虑,GORM 在大多数情况下可以显著简化数据库操作,提高开发效率。然而,根据项目的特定需求,开发人员需要权衡使用 GORM 的优势和劣势,以确保选择了适合的工具。
好了,到这里简单的连接数据库和增删改查操作都基本完成了,可以发现gorm操作数据库是十分便利的,各种函数套娃就可以了,实在不行我们还可以使用原生sql语句来进行操作。
希望我的文章能起到作用哦~😝