GORM 是 Go 言语中最受欢迎的 ORM 库之一,它供给了强壮的功用和简练的 API,让数据库操作变得更加简略和易保护。本文将具体介绍 GORM 的常见用法,包括数据库衔接、模型界说、CRUD、业务办理等方面,协助咱们快速上手运用 GORM 进行 Web 后端开发。
装置
经过如下指令装置 GORM:
$ go get -u gorm.io/gorm
你也许见过运用 go get -u github.com/jinzhu/gorm
指令来装置 GORM,这个是老版本 v1,现已过时,不主张运用。新版本 v2 已经搬迁至 github.com/go-gorm/gorm
库房下。
快速开端
如下示例代码带你快速上手 GORM 的运用:
package main
import (
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
// Product 界说结构体用来映射数据库表
type Product struct {
gorm.Model
Code string
Price uint
}
func main() {
// 树立数据库衔接
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
if err != nil {
panic("failed to connect database")
}
// 搬迁表结构
db.AutoMigrate(&Product{})
// 添加数据
db.Create(&Product{Code: "D42", Price: 100})
// 查找数据
var product Product
db.First(&product, 1) // find product with integer primary key
db.First(&product, "code = ?", "D42") // find product with code D42
// 更新数据 - update product's price to 200
db.Model(&product).Update("Price", 200)
// 更新数据 - update multiple fields
db.Model(&product).Updates(Product{Price: 200, Code: "F42"}) // non-zero fields
db.Model(&product).Updates(map[string]interface{}{"Price": 200, "Code": "F42"})
// 删去数据 - delete product
db.Delete(&product, 1)
}
提示:这儿运用了
SQLite
数据库驱动,需求经过go get -u gorm.io/driver/sqlite
指令装置。
将以上代码保存在 main.go
中并履行。
$ go run main.go
履行完结后,咱们将在当前目录下得到 test.db
SQLite 数据库文件。
① 进入 SQLite 指令行。
② 检查已存在的数据库表。
③ 设置稍后查询表数据时的输出形式为按列左对齐。
④ 查询表中存在的数据。
有过运用 ORM 结构经验的同学,以上代码即便我不进行解说也能看懂个大概。
这段示例代码根本能够概括 GORM 结构运用套路:
-
界说结构体映射表结构:
Product
结构体在 GORM 中称作「模型」,一个模型对应一张数据库表,一个结构体实例目标对应一条数据库表记载。 -
衔接数据库:GORM 运用
gorm.Open
办法与数据库树立衔接,衔接树立好后,才能对数据库进行 CRUD 操作。 -
主动搬迁表结构:调用
db.AutoMigrate
办法能够主动完结在数据库中创立Product
结构体所映射的数据库表,而且,当Product
结构体字段有改动,再次履行搬迁代码,GORM 会主动对表结构进行调整,非常便利。不过,我不推荐在出产环境项目中运用此功用。因为数据库表操作都是高风险操作,一定要经过多人 Review 并审阅经过,才能履行操作。GORM 主动搬迁功用尽管理论上不会出现问题,但线上操作慎重为妙,个人认为只有在小项目或数据不那么重要的项目中运用比较适宜。 -
CRUD 操作:搬迁好数据库后,就有了数据库表,能够进行 CRUD 操作了。
有些同学或许有个疑问,以上示例代码中并没有相似 defer db.Close()
主动关闭衔接的操作,那么何时关闭数据库衔接?
其实 GORM 保护了一个数据库衔接池,初始化 db
后一切的衔接都由底层库来办理,无需程序员手动干预,GORM 会在适宜的机遇主动关闭衔接。GORM 结构作者 jinzhu
也有在源码库房 Issue 中回复过网友的提问,感兴趣的同学能够点击进入检查。
接下来我将对 GORM 的运用进行具体解说。
声明模型
GORM 运用模型(Model)来映射一张数据库表,模型是标准的 Go struct
,由 Go 的根本数据类型、完结了 Scanner
和 Valuer
接口的自界说类型及其指针或别号组成。
例如:
type User struct {
ID uint
Name string
Email *string
Age uint8
Birthday *time.Time
MemberNumber sql.NullString
ActivatedAt sql.NullTime
CreatedAt time.Time
UpdatedAt time.Time
}
咱们能够运用 gorm
字段标签来操控数据库表字段的类型、列大小、默许值等特点,比方运用 column
字段标签来映射数据库中字段名称。
type User struct {
gorm.Model
Name string `gorm:"column:name"`
Email *string `gorm:"column:email"`
Age uint8 `gorm:"column:age"`
Birthday *time.Time `gorm:"column:birthday"`
MemberNumber sql.NullString `gorm:"column:member_number"`
ActivatedAt sql.NullTime `gorm:"column:activated_at"`
}
func (u *User) TableName() string {
return "user"
}
在不指定 column
字段标签情况下,GORM 默许运用字段名的 snake_case
作为列名。
GORM 默许运用结构体名的 snake_cases
作为表名,为结构体完结 TableName
办法能够自界说表名。
我更喜爱「显式胜于隐式」的做法,所以数据库名和表名都会显现写出来。
因为咱们不运用主动搬迁的功用,所以其他字段标签都用不到,就不在此逐个介绍了,感兴趣的同学能够检查官方文档进行学习。
User
结构体中有一个嵌套的结构体 gorm.Model
,它是 GORM 默许供给的一个模型 struct
,用来简化用户模型界说。
GORM 倾向于约定优于配置,默许情况下,运用 ID
作为主键,运用 CreatedAt
、UpdatedAt
、DeletedAt
字段追寻记载的创立、更新、删去时刻。而这几个字段就界说在 gorm.Model
中:
type Model struct {
ID uint `gorm:"primarykey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt DeletedAt `gorm:"index"`
}
由于咱们不运用主动搬迁功用,所以需求手动编写 SQL 句子来创立 user
数据库表结构:
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(50) DEFAULT '' COMMENT '用户名',
`email` varchar(255) NOT NULL DEFAULT '' COMMENT '邮箱',
`age` tinyint(4) NOT NULL DEFAULT '0' COMMENT '年龄',
`birthday` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '生日',
`member_number` varchar(50) COMMENT '成员编号',
`activated_at` datetime COMMENT '激活时刻',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`deleted_at` datetime,
PRIMARY KEY (`id`),
UNIQUE KEY `u_email` (`email`),
INDEX `idx_deleted_at`(`deleted_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户表';
数据库中字段类型要跟 Go 中模型的字段类型相对应,不兼容的类型或许导致过错。
衔接数据库
GORM 官方支撑的数据库类型有:MySQL、PostgreSQL、SQLite、SQL Server 和 TiDB。
这儿运用最常见的 MySQL 作为示例,来解说 GORM 怎么衔接到数据库。
在前文快速开端的示例代码中,咱们运用 SQLite 数据库时,装置了 sqlite
驱动程序。要衔接 MySQL 则需求运用 mysql
驱动。
在 GORM 中界说了 gorm.Dialector
接口来标准数据库衔接操作,完结了此接口的程序咱们将其称为「驱动」。针对每种数据库,都有对应的驱动,驱动是独立于 GORM 库的,需求单独引入。
衔接 MySQL 数据库的代码如下:
package main
import (
"fmt"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
func ConnectMySQL(host, port, user, pass, dbname string) (*gorm.DB, error) {
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
user, pass, host, port, dbname)
return gorm.Open(mysql.Open(dsn), &gorm.Config{})
}
能够发现,这段代码与衔接 SQLite 数据库的代码千篇一律,这便是面向接口编程的优点。
首先,mysql.Open
接纳一个字符串 dsn
,DSN 全称 Data Source Name
,翻译过来叫数据库源名称。DSN 界说了一个数据库的衔接信息,包括用户名、密码、数据库 IP、数据库端口、数据库字符集、数据库时区等信息。DSN 遵循特定格局:
username:password@protocol(address)/dbname?param=value
经过 DSN 所包括的信息,mysql
驱动就能够知道以什么办法衔接到 MySQL 数据库了。
mysql.Open
回来的正是一个 gorm.Dialector
目标,将其传递给 gorm.Open
办法后,咱们将得到 *gorm.DB
目标,这个目标能够用来操作数据库。
GORM 运用 database/sql
来保护数据库衔接池,关于衔接池咱们能够设置如下几个参数:
func SetConnect(db *gorm.DB) error {
sqlDB, err := db.DB()
if err != nil {
return err
}
sqlDB.SetMaxOpenConns(100) // 设置数据库的最大翻开衔接数
sqlDB.SetMaxIdleConns(100) // 设置最大空闲衔接数
sqlDB.SetConnMaxLifetime(10 * time.Second) // 设置空闲衔接最大存活时刻
return nil
}
现在,数据库衔接已经树立,咱们能够对数据库进行操作了。
创立
能够运用 Create
办法创立一条数据库记载:
now := time.Now()
email := "u1@jianghushinian.com"
user := User{Name: "user1", Email: &email, Age: 18, Birthday: &now}
// INSERT INTO `user` (`created_at`,`updated_at`,`deleted_at`,`name`,`email`,`age`,`birthday`,`member_number`,`activated_at`) VALUES ('2023-05-22 22:14:47.814','2023-05-22 22:14:47.814',NULL,'user1','u1@jianghushinian.com',18,'2023-05-22 22:14:47.812',NULL,NULL)
result := db.Create(&user) // 经过数据的指针来创立
fmt.Printf("user: %+v\n", user) // user.ID 主动填充
fmt.Printf("affected rows: %d\n", result.RowsAffected)
fmt.Printf("error: %v\n", result.Error)
要创立记载,咱们需求先实例化 User
目标,然后将其指针传递给 db.Create
办法。
db.Create
办法履行完结后,依然回来一个 *gorm.DB
目标。
user.ID
会被主动填充为创立数据库记载后回来的实在值。
result.RowsAffected
能够拿到此次操作影响行数。
result.Error
能够知道履行 SQL 是否出错。
在这儿,我将 db.Create(&user)
这句 ORM
代码所生成的原生 SQL 句子放在了注释中,便利你比照学习。而且,之后的示例中我也会这样做。
Create
办法不只支撑创立单条记载,它相同支撑批量操作,一次创立多条记载:
now = time.Now()
email2 := "u2@jianghushinian.com"
email3 := "u3@jianghushinian.com"
users := []User{
{Name: "user2", Email: &email2, Age: 19, Birthday: &now},
{Name: "user3", Email: &email3, Age: 20, Birthday: &now},
}
// INSERT INTO `user` (`created_at`,`updated_at`,`deleted_at`,`name`,`email`,`age`,`birthday`,`member_number`,`activated_at`) VALUES ('2023-05-22 22:14:47.834','2023-05-22 22:14:47.834',NULL,'user2','u2@jianghushinian.com',19,'2023-05-22 22:14:47.833',NULL,NULL),('2023-05-22 22:14:47.834','2023-05-22 22:14:47.834',NULL,'user3','u3@jianghushinian.com',20,'2023-05-22 22:14:47.833',NULL,NULL)
result = db.Create(&users)
代码首要逻辑不变,只需求将单个的 User
实例换成 User
切片即可。GORM 会运用一条 SQL 句子完结批量创立记载。
查询
查询记载是咱们在日常开发中运用最多的场景了,GORM 供给了多种办法来支撑 SQL 查询操作。
运用 First
办法能够查询第一条记载:
var user User
// SELECT * FROM `user` WHERE `user`.`deleted_at` IS NULL ORDER BY `user`.`id` LIMIT 1
result := db.First(&user)
First
办法接纳一个模型指针,经过模型的 TableName
办法则能够拿到数据库表名,然后运用 SELECT *
句子从数据库中查询记载。
依据生成的 SQL 能够发现 First
办法查询数据默许依据主键 ID
升序排序,而且只会过滤删去时刻为 NULL
的数据,运用 LIMIT
关键字来约束数据条数。
运用 Last
办法能够查询最终一条数据,排序规矩为主键 ID
降序:
var lastUser User
// SELECT * FROM `user` WHERE `user`.`deleted_at` IS NULL ORDER BY `user`.`id` DESC LIMIT 1
result = db.Last(&lastUser)
运用 Where
办法能够添加查询条件:
var users []User
// SELECT * FROM `user` WHERE name != 'unknown' AND `user`.`deleted_at` IS NULL
result = db.Where("name != ?", "unknown").Find(&users)
这儿不再查询单条数据,所以改用 Find
办法来查询一切符合条件的记载。
以上介绍的几种查询办法,都是经过 SELECT *
查询数据库表中的悉数字段,咱们能够运用 Select
办法指定需求查询的字段:
var user2 User
// SELECT `name`,`age` FROM `user` WHERE `user`.`deleted_at` IS NULL ORDER BY `user`.`id` LIMIT 1
result = db.Select("name", "age").First(&user2)
运用 Order
办法能够自界说排序规矩:
var users2 []User
// SELECT * FROM `user` WHERE `user`.`deleted_at` IS NULL ORDER BY id desc
result = db.Order("id desc").Find(&users2)
GORM 也供给了对 Limit & Offset
的支撑:
var users3 []User
// SELECT * FROM `user` WHERE `user`.`deleted_at` IS NULL LIMIT 2 OFFSET 1
result = db.Limit(2).Offset(1).Find(&users3)
运用 -1
能够撤销 Limit & Offset
的约束条件:
var users4 []User
var users5 []User
// SELECT * FROM `user` WHERE `user`.`deleted_at` IS NULL LIMIT 2 OFFSET 1; (users4)
// SELECT * FROM `user` WHERE `user`.`deleted_at` IS NULL; (users5)
result = db.Limit(2).Offset(1).Find(&users4).Limit(-1).Offset(-1).Find(&users5)
这段代码会履行两条查询句子,之所以能够采用这种「链式调用」的办法履行多条 SQL,是因为每个办法回来的都是 *gorm.DB
目标,这也是一种编程技巧。
运用 Count
办法能够统计记载条数:
var count int64
// SELECT count(*) FROM `user` WHERE `user`.`deleted_at` IS NULL
result = db.Model(&User{}).Count(&count)
有时分遇到比较杂乱的业务,咱们或许需求运用 SQL 子查询,子查询能够嵌套在另一个查询中,GORM 允许将 *gorm.DB
目标作为参数时生成子查询:
var avgages []float64
// SELECT AVG(age) as avgage FROM `user` WHERE `user`.`deleted_at` IS NULL GROUP BY `name` HAVING AVG(age) > (SELECT AVG(age) FROM `user` WHERE name LIKE 'user%')
subQuery := db.Select("AVG(age)").Where("name LIKE ?", "user%").Table("user")
result = db.Model(&User{}).Select("AVG(age) as avgage").Group("name").Having("AVG(age) > (?)", subQuery).Find(&avgages)
Having
办法签名如下:
func (db *DB) Having(query interface{}, args ...interface{}) (tx *DB)
第二个参数是一个范型 interface{}
,所以不只能够接纳字符串,GORM 在判断其类型为 *gorm.DB
时,就会构造一个子查询。
更新
为了解说更新操作,咱们需求先查询一条记载,之后的更新操作都是依据这条被查询出来的 User
目标:
var user User
// SELECT * FROM `user` WHERE `user`.`deleted_at` IS NULL ORDER BY `user`.`id` LIMIT 1
result := db.First(&user)
更新操作只要修正 User
目标的特点,然后调用 db.Save(&user)
办法即可完结:
user.Name = "John"
user.Age = 20
// UPDATE `user` SET `created_at`='2023-05-22 22:14:47.814',`updated_at`='2023-05-22 22:24:34.201',`deleted_at`=NULL,`name`='John',`email`='u1@jianghushinian.com',`age`=20,`birthday`='2023-05-22 22:14:47.813',`member_number`=NULL,`activated_at`=NULL WHERE `user`.`deleted_at` IS NULL AND `id` = 1
result = db.Save(&user)
在更新操作时,User
目标要确保 ID
特点存在值,否则就变成了创立操作。
Save
办法会保存一切的字段,即便字段是对应类型的零值。
除了运用 Save
办法更新一切字段,咱们还能够运用 Update
办法更新指定字段:
// UPDATE `user` SET `name`='Jianghushinian',`updated_at`='2023-05-22 22:24:34.215' WHERE `user`.`deleted_at` IS NULL AND `id` = 1
result = db.Model(&user).Update("name", "Jianghushinian")
Update
只能支撑更新单个字段,要想更新多个字段,能够运用 Updates
办法:
// UPDATE `user` SET `updated_at`='2023-05-22 22:29:35.19',`name`='JiangHu' WHERE `user`.`deleted_at` IS NULL AND `id` = 1
result = db.Model(&user).Updates(User{Name: "JiangHu", Age: 0})
留意,Updates
办法与 Save
办法有一个很大的不同之处,它只会更新非零值字段。Age
字段为零值,所以不会被更新。
假如一定要更新零值字段,除了能够运用上面的 Save
办法,还能够将 User
结构体换成 map[string]interface{}
类型的 map
目标:
// UPDATE `user` SET `age`=0,`name`='JiangHu',`updated_at`='2023-05-22 22:29:35.623' WHERE `user`.`deleted_at` IS NULL AND `id` = 1
result = db.Model(&user).Updates(map[string]interface{}{"name": "JiangHu", "age": 0})
此外,更新数据时,还能够运用 gorm.Expr
来完结 SQL 表达式:
// UPDATE `user` SET `age`=age + 1,`updated_at`='2023-05-22 22:24:34.219' WHERE `user`.`deleted_at` IS NULL AND `id` = 1
result = db.Model(&user).Update("age", gorm.Expr("age + ?", 1))
gorm.Expr("age + ?", 1)
办法调用会被转换成 age=age + 1
SQL 表达式。
删去
能够运用 Delete
办法删去数记载:
var user User
// UPDATE `user` SET `deleted_at`='2023-05-22 22:46:45.086' WHERE name = 'JiangHu' AND `user`.`deleted_at` IS NULL
result := db.Where("name = ?", "JiangHu").Delete(&user)
关于删去操作,GORM 默许运用逻辑删去策略,不会对记载进行物理删去。
所以 Delete
办法在对数据进行删去时,实际上履行的是 SQL UPDATE
操作,而非 DELETE
操作。
将 deleted_at
字段更新为当前时刻,表示当前数据已删去。这也是为什么前文在解说查询和更新的时分,生成的 SQL 句子都主动附加了 deleted_at IS NULL
Where 条件的原因。
这样就完结了逻辑层面的删去,数据在数据库中依然存在,但查询和更新的时分会将其过滤掉。
记载被删去后,咱们无法经过如下代码直接查询到被逻辑删去的记载:
// SELECT * FROM `user` WHERE name = 'JiangHu' AND `user`.`deleted_at` IS NULL ORDER BY `user`.`id` LIMIT 1
result = db.Where("name = ?", "JiangHu").First(&user)
if err := result.Error; err != nil {
fmt.Println(err) // record not found
}
这将得到一个过错 record not found
。
不过,GORM 供给了 Unscoped
办法,能够绕过逻辑删去:
// SELECT * FROM `user` WHERE name = 'JiangHu' ORDER BY `user`.`id` LIMIT 1
result = db.Unscoped().Where("name = ?", "JiangHu").First(&user)
以上代码能够查询出被逻辑删去的记载,生成的 SQL 句子中没有包括 deleted_at IS NULL
Where 条件。
关于比较重要的数据,主张运用逻辑删去,这样能够在需求的时分康复数据,也便于毛病追寻。
不过,假如清晰想要物理删去一条记载,同理能够运用 Unscoped
办法:
// DELETE FROM `user` WHERE name = 'JiangHu' AND `user`.`id` = 1
result = db.Unscoped().Where("name = ?", "JiangHu").Delete(&user)
相关
日常开发中,大都情况下不只是对单表进行操作,还要对存在相相联络的多表进行操作。
这儿以一个博客体系最常见的三张表「文章表、谈论表、标签表」为例,对 GORM 怎么操作相关表进行解说。
这儿触及最常见的相相联络:一对多和多对多。一篇文章能够有多条谈论,所以文章和谈论是一对多联络;一篇文章能够存在多个标签,每个标签也能够包括多篇文章,所以文章和标签是多对多联络。
模型界说如下:
type Post struct {
gorm.Model
Title string `gorm:"column:title"`
Content string `gorm:"column:content"`
Comments []*Comment `gorm:"foreignKey:PostID;constraint:OnUpdate:CASCADE,OnDelete:SET NULL;references:ID"`
Tags []*Tag `gorm:"many2many:post_tags"`
}
func (p *Post) TableName() string {
return "post"
}
type Comment struct {
gorm.Model
Content string `gorm:"column:content"`
PostID uint `gorm:"column:post_id"`
Post *Post
}
func (c *Comment) TableName() string {
return "comment"
}
type Tag struct {
gorm.Model
Name string `gorm:"column:name"`
Post []*Post `gorm:"many2many:post_tags"`
}
func (t *Tag) TableName() string {
return "tag"
}
我准备了对应的建表 SQL,能够点击链接进行检查:GitHub 地址。
在模型界说中,Post
文章模型运用 Comments
和 Tags
别离保存相关的谈论和标签,这两个字段不会保存在数据库表中。
Comments
字段标签运用 foreignKey
来指明 Comments
表中的外键,并运用 constraint
指明了约束条件,references
指明 Comments
表外键引证 Post
表的 ID
字段。
其完结在出产环境中都不再推荐运用外键,各个表之间不再有数据库层面的外键约束,在做 CRUD 操作时悉数经过代码层面来进行业务约束。这儿为了演示 GORM 的外键和级联操作功用,所以界说了这些结构体标签。
Tags
字段标签运用 many2many
来指明多对多相关表名。
关于 Comment
模型,PostID
字段便是外键,用来保存 Post.ID
。Post
字段相同不会保存在数据库中,这种做法在 ORM 结构中非常常见。
接下来,我将相同对相关表的 CRUD 操作进行逐个解说。
创立
创立 Post
时会主动创立与之相关的 Comments
和 Tags
:
var post Post
post = Post{
Title: "post1",
Content: "content1",
Comments: []*Comment{
{Content: "comment1", Post: &post},
{Content: "comment2", Post: &post},
},
Tags: []*Tag{
{Name: "tag1"},
{Name: "tag2"},
},
}
result := db.Create(&post)
这儿界说了一个文章目标 post
,而且包括两条谈论和两个标签。
留意 Comment
的 Post
字段引证了 &post
,并没有指定 PostID
外键字段,GORM 能够正确处理它。
以上代码将生成并顺次履行如下 SQL 句子:
BEGIN TRANSACTION;
INSERT INTO `tag` (`created_at`,`updated_at`,`deleted_at`,`name`) VALUES ('2023-05-22 22:56:52.923','2023-05-22 22:56:52.923',NULL,'tag1'),('2023-05-22 22:56:52.923','2023-05-22 22:56:52.923',NULL,'tag2') ON DUPLICATE KEY UPDATE `id`=`id`
INSERT INTO `post` (`created_at`,`updated_at`,`deleted_at`,`title`,`content`) VALUES ('2023-05-22 22:56:52.898','2023-05-22 22:56:52.898',NULL,'post1','content1') ON DUPLICATE KEY UPDATE `id`=`id`
INSERT INTO `comment` (`created_at`,`updated_at`,`deleted_at`,`content`,`post_id`) VALUES ('2023-05-22 22:56:52.942','2023-05-22 22:56:52.942',NULL,'comment1',1),('2023-05-22 22:56:52.942','2023-05-22 22:56:52.942',NULL,'comment2',1) ON DUPLICATE KEY UPDATE `post_id`=VALUES(`post_id`)
INSERT INTO `post_tags` (`post_id`,`tag_id`) VALUES (1,1),(1,2) ON DUPLICATE KEY UPDATE `post_id`=`post_id`
COMMIT;
能够发现,与文章构成一对多联络的谈论以及与文章构成多对多联络的标签,都会被创立,而且 GORM 会保护其相相联络,而且这些操作悉数在一个业务下完结。
此外,前文介绍的 Save
办法不只能够更新记载,实际上它还支撑创立记载,当 Post
目标不存在主键 ID
时,Save
办法将会创立一条新的记载:
var post3 Post
post3 = Post{
Title: "post3",
Content: "content3",
Comments: []*Comment{
{Content: "comment33", Post: &post3},
},
Tags: []*Tag{
{Name: "tag3"},
},
}
result = db.Save(&post3)
以上代码生成的 SQL 如下:
BEGIN TRANSACTION;
INSERT INTO `tag` (`created_at`,`updated_at`,`deleted_at`,`name`) VALUES ('2023-05-22 23:17:53.189','2023-05-22 23:17:53.189',NULL,'tag3') ON DUPLICATE KEY UPDATE `id`=`id`
INSERT INTO `post` (`created_at`,`updated_at`,`deleted_at`,`title`,`content`) VALUES ('2023-05-22 23:17:53.189','2023-05-22 23:17:53.189',NULL,'post3','content3') ON DUPLICATE KEY UPDATE `id`=`id`
INSERT INTO `comment` (`created_at`,`updated_at`,`deleted_at`,`content`,`post_id`) VALUES ('2023-05-22 23:17:53.19','2023-05-22 23:17:53.19',NULL,'comment33',0) ON DUPLICATE KEY UPDATE `post_id`=VALUES(`post_id`)
INSERT INTO `post_tags` (`post_id`,`tag_id`) VALUES (0,0) ON DUPLICATE KEY UPDATE `post_id`=`post_id`
COMMIT;
查询
能够运用如下办法,依据 Post
的 ID
查询与之相关的 Comments
:
var (
post Post
comments []*Comment
)
post.ID = 1
// SELECT * FROM `comment` WHERE `comment`.`post_id` = 1 AND `comment`.`deleted_at` IS NULL
err := db.Model(&post).Association("Comments").Find(&comments)
留意⚠️:传递给
Association
办法的参数是Comments
,即在Post
模型中界说的字段,而非谈论的模型名Comment
。这点一定不要搞错了,否则履行 SQL 时会报错。
Post
是源模型,主键 ID
不能为空。Association
办法指定相关字段名,在 Post
模型中相关的谈论运用 Comments
表示。最终运用 Find
办法来查询相关的谈论。
在查询 Post
时,咱们能够预加载与之相关的 Comments
:
post2 := Post{}
result := db.Preload("Comments").Preload("Tags").First(&post2)
fmt.Println(post2)
for i, comment := range post2.Comments {
fmt.Println(i, comment)
}
for i, tag := range post2.Tags {
fmt.Println(i, tag)
}
咱们能够像往常一样运用 First
办法查询一条 Post
记载,一起搭配运用 Preload
办法来指定预加载的相关字段名,这样在查询 Post
记载时,会将相关字段表的记载悉数查询出来,并赋值给相关字段。
以上代码将履行如下 SQL:
BEGIN TRANSACTION;
SELECT * FROM `post` WHERE `post`.`deleted_at` IS NULL ORDER BY `post`.`id` LIMIT 1
SELECT * FROM `comment` WHERE `comment`.`post_id` = 1 AND `comment`.`deleted_at` IS NULL
SELECT * FROM `post_tags` WHERE `post_tags`.`post_id` = 1
SELECT * FROM `tag` WHERE `tag`.`id` IN (1,2) AND `tag`.`deleted_at` IS NULL
COMMIT;
GORM 经过多条 SQL 句子查询出一切相关记载,而且将相关 Comments
和 Tags
别离赋值给 Post
模型对应字段。
当遇到多表查询时,咱们一般还会运用 JOIN
来衔接多张表:
type PostComment struct {
Title string
Comment string
}
postComment := PostComment{}
post3 := Post{}
post3.ID = 3
// SELECT post.title, comment.Content AS comment FROM `post` LEFT JOIN comment ON comment.post_id = post.id WHERE `post`.`deleted_at` IS NULL AND `post`.`id` = 3
result := db.Model(&post3).Select("post.title, comment.Content AS comment").Joins("LEFT JOIN comment ON comment.post_id = post.id").Scan(&postComment)
运用 Select
办法来指定需求查询的字段,运用 Joins
办法来完结 JOIN
功用,最终运用 Scan
办法能够将查询成果扫描到 postComment
目标中。
针对一对多相相联络,Joins
办法相同支撑预加载:
var comments2 []*Comment
// SELECT `comment`.`id`,`comment`.`created_at`,`comment`.`updated_at`,`comment`.`deleted_at`,`comment`.`content`,`comment`.`post_id`,`Post`.`id` AS `Post__id`,`Post`.`created_at` AS `Post__created_at`,`Post`.`updated_at` AS `Post__updated_at`,`Post`.`deleted_at` AS `Post__deleted_at`,`Post`.`title` AS `Post__title`,`Post`.`content` AS `Post__content` FROM `comment` LEFT JOIN `post` `Post` ON `comment`.`post_id` = `Post`.`id` AND `Post`.`deleted_at` IS NULL WHERE `comment`.`deleted_at` IS NULL
result = db.Joins("Post").Find(&comments2)
for i, comment := range comments2 {
fmt.Println(i, comment)
fmt.Println(i, comment.Post)
}
JOIN
功用的预加载无需显式运用 Preload
来指明,只需求在 Joins
办法中指明一对多联络中一这一端模型 Post
即可,运用 Find
查询 Comment
记载。
依据生成的 SQL 能够发现查询主表为 comment
,副表为 post
。而且副表的字段都被重命名为 模型名__字段名
的格局,如 Post__title
(题外话:假如你运用过 Python 的 Django ORM 结构,那么对这个双下划线命名字段的做法应该有种似曾相识的感觉)。
更新
同解说单表更新时一样,咱们需求先查询出一条记载,用来演示更新操作:
var post Post
// SELECT * FROM `post` WHERE `post`.`deleted_at` IS NULL ORDER BY `post`.`id` LIMIT 1
result := db.First(&post)
能够运用如下办法替换 Post
相关的 Comments
:
comment := Comment{
Content: "comment3",
}
err := db.Model(&post).Association("Comments").Replace([]*Comment{&comment})
依然运用 Association
办法指定 Post
相关的 Comments
,Replace
办法用来完结替换操作。
这儿要留意,Replace
办法回来成果不再是 *gorm.DB
目标,而是直接回来 error
。
生成 SQL 如下:
BEGIN TRANSACTION;
INSERT INTO `comment` (`created_at`,`updated_at`,`deleted_at`,`content`,`post_id`) VALUES ('2023-05-23 09:07:42.852','2023-05-23 09:07:42.852',NULL,'comment3',1) ON DUPLICATE KEY UPDATE `post_id`=VALUES(`post_id`)
UPDATE `post` SET `updated_at`='2023-05-23 09:07:42.846' WHERE `post`.`deleted_at` IS NULL AND `id` = 1
UPDATE `comment` SET `post_id`=NULL WHERE `comment`.`id` <> 8 AND `comment`.`post_id` = 1 AND `comment`.`deleted_at` IS NULL
COMMIT;
删去
运用 Delete
删去文章表时,不会删去相关表的数据:
var post Post
// UPDATE `post` SET `deleted_at`='2023-05-23 09:09:58.534' WHERE id = 1 AND `post`.`deleted_at` IS NULL
result := db.Where("id = ?", 1).Delete(&post)
关于存在相相联络的记载,删去时默许相同采用 UPDATE
操作,且不影响相关数据。
假如想要在删去谈论时,趁便删去与文章的相相联络,能够运用 Association
办法:
// UPDATE `comment` SET `post_id`=NULL WHERE `comment`.`post_id` = 6 AND `comment`.`id` IN (NULL) AND `comment`.`deleted_at` IS NULL
err := db.Model(&post2).Association("Comments").Delete(post2.Comments)
业务
GORM 供给了对业务的支撑,这在杂乱的业务逻辑中是必要的。
要在业务中履行一系列操作,能够运用 Transaction
办法完结:
func TransactionPost(db *gorm.DB) error {
return db.Transaction(func(tx *gorm.DB) error {
post := Post{
Title: "Hello World",
}
if err := tx.Create(&post).Error; err != nil {
return err
}
comment := Comment{
Content: "Hello World",
PostID: post.ID,
}
if err := tx.Create(&comment).Error; err != nil {
return err
}
return nil
})
}
在 Transaction
办法内部的代码,都将在一个业务中被处理。Transaction
办法接纳一个函数,其参数为 tx *gorm.DB
,业务中一切数据库的操作,都应该运用这个 tx
而非 db
。
在履行业务的函数中,回来任何过错,整个业务都将被回滚,回来 nil
则业务被提交。
除了运用 Transaction
主动办理业务,咱们还能够手动办理业务:
func TransactionPostWithManually(db *gorm.DB) error {
tx := db.Begin()
post := Post{
Title: "Hello World Manually",
}
if err := tx.Create(&post).Error; err != nil {
tx.Rollback()
return err
}
comment := Comment{
Content: "Hello World Manually",
PostID: post.ID,
}
if err := tx.Create(&comment).Error; err != nil {
tx.Rollback()
return err
}
return tx.Commit().Error
}
db.Begin()
用于敞开业务,并回来 tx
,稍后的业务操作都应运用这个 tx
目标。假如在处理业务的过程中遇到过错,能够运用 tx.Rollback()
回滚业务,假如没有问题,最终能够运用 tx.Commit()
提交业务。
留意:手动业务,业务一旦开端,你就应该运用
tx
处理数据库操作。
钩子
GORM 还支撑 Hook 功用,Hook 是在创立、查询、更新、删去等操作之前、之后调用的函数,用来办理目标的生命周期。
钩子办法的函数签名为 func(*gorm.DB) error
,比方以下钩子函数在创立操作之前触发:
func (u *User) BeforeCreate(tx *gorm.DB) (err error) {
u.UUID = uuid.New()
if u.Name == "admin" {
return errors.New("invalid name")
}
return nil
}
比方咱们为 User
模型界说 BeforeCreate
钩子,这样在创立 User
目标前,GORM 会主动调用此函数,完结为 User
目标创立 UUID
以及用户名合法性验证功用。
GORM 支撑的钩子函数以及履行机遇如下:
钩子函数 | 履行机遇 |
---|---|
BeforeSave | 调用 Save 前 |
AfterSave | 调用 Save 后 |
BeforeCreate | 刺进记载前 |
AfterCreate | 刺进记载后 |
BeforeUpdate | 更新记载前 |
AfterUpdate | 更新记载后 |
BeforeDelete | 删去记载前 |
AfterDelete | 删去记载后 |
AfterFind | 查询记载后 |
原生 SQL
尽管咱们运用 ORM 结构往往是为了将原生 SQL 的编写转为面向目标编程,不过对原生 SQL 的支撑是一款 ORM 结构必备的功用。
能够运用 Raw
办法履行原生查询 SQL,并将成果 Scan
到模型中:
var userRes UserResult
db.Raw(`SELECT id, name, age FROM user WHERE id = ?`, 3).Scan(&userRes)
fmt.Printf("affected rows: %d\n", db.RowsAffected)
fmt.Println(db.Error)
fmt.Println(userRes)
原生 SQL 相同支撑运用表达式:
var sumage int
db.Raw(`SELECT SUM(age) as sumage FROM user WHERE member_number ?`, gorm.Expr("IS NULL")).Scan(&sumage)
此外,咱们还能够运用 Exec
履行任意原生 SQL:
db.Exec("UPDATE user SET age = ? WHERE id IN ?", 18, []int64{1, 2})
// 运用表达式
db.Exec(`UPDATE user SET age = ? WHERE name = ?`, gorm.Expr("age * ? + ?", 1, 2), "Jianghu")
// 删去表
db.Exec("DROP TABLE user")
运用 Exec
无法拿到履行成果,能够用来对表进行操作,比方添加、删去表等。
编写 SQL 时支撑运用 @name
语法命名参数:
var post Post
db.Where("title LIKE @name OR content LiKE @name", sql.Named("name", "%Hello%")).Find(&post)
var user User
// SELECT * FROM user WHERE name1 = "Jianghu" OR name2 = "shinian" OR name3 = "Jianghu"
db.Raw("SELECT * FROM user WHERE name1 = @name OR name2 = @name2 OR name3 = @name",
sql.Named("name", "Jianghu"), sql.Named("name2", "shinian")).Find(&user)
运用 DryRun
形式能够直接拿到由 GORM 生成的原生 SQL,而不履行,便利后续运用:
var user User
stmt := db.Session(&gorm.Session{DryRun: true}).First(&user, 1).Statement
fmt.Println(stmt.SQL.String()) // SQL: SELECT * FROM `user` WHERE `user`.`id` = ? AND `user`.`deleted_at` IS NULL ORDER BY `user`.`id` LIMIT 1
fmt.Println(stmt.Vars) // 参数: [1]
DryRun
形式能够翻译为空跑,意思是不履行真正的 SQL,这在调试时非常有用。
调试
GORM 常用功用咱们已经根本解说完结了,最终再来介绍下在日常开发中,遇到问题怎么进行调试。
GORM 调试办法我总结了如下 5 点:
- 大局敞开日志
还记得在衔接数据库时 gorm.Open
办法的第二个参数吗,咱们当时传递了一个空配置 &gorm.Config{}
,这个可选的参数能够改动 GORM 的一些默许功用配置,比方咱们能够设置日志等级为 Info
,这样就能够在操控台打印一切履行的 SQL 句子:
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
Logger:logger.Default.LogMode(logger.Info),
})
- 打印慢查询 SQL
有时分某段 ORM 代码履行很慢,咱们能够经过敞开慢查询日志,来检测 SQL 中的慢查询句子:
func ConnectMySQL(host, port, user, pass, dbname string) (*gorm.DB, error) {
slowLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags),
logger.Config{
// 设定慢查询时刻阈值为 3ms(默许值:200 * time.Millisecond)
SlowThreshold: 3 * time.Millisecond,
// 设置日志等级
LogLevel: logger.Warn,
},
)
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
user, pass, host, port, dbname)
return gorm.Open(mysql.Open(dsn), &gorm.Config{
Logger: slowLogger,
})
}
- 打印指定 SQL
运用 Debug
能够打印当前 ORM 句子履行的 SQL:
db.Debug().First(&User{})
- 大局敞开 DryRun 模型
在衔接数据库时,咱们能够大局敞开「空跑」形式:
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
DryRun: true,
})
敞开 DryRun 模型后,任何 SQL 句子都不会真正履行,便利测验。
- 局部敞开 DryRun 模型
在当前 Session
中局部敞开「空跑」模型,能够在不履行操作的情况下生成 SQL 及其参数,用于准备或测验生成的 SQL:
var user User
stmt := db.Session(&gorm.Session{DryRun: true}).First(&user, 1).Statement
fmt.Println(stmt.SQL.String()) // => SELECT * FROM `users` WHERE `id` = $1 ORDER BY `id`
fmt.Println(stmt.Vars) // => []interface{}{1}
总结
本文对 Go 言语中最盛行的 ORM 结构 GORM 进行了解说,介绍了怎么编写模型,怎么衔接数据库,以及最常运用的 CRUD 操作。而且还对相关表中的一对多、多对多两种相相联络操作进行了解说。咱们还介绍了必不可少的功用「业务」,GORM 还供给了钩子函数便利咱们在 CRUD 操作前后刺进一些自界说逻辑。最终对怎么运用原生 SQL 以及怎么调试也进行了介绍。
只要你原生 SQL 根底扎实,ORM 结构学习起来并不会太费力,而且咱们还有各种调试办法来打印 GORM 所生成的 SQL,便利排查问题。
由于文章篇幅所限,这儿只介绍了 GORM 常用功用,不过也根本能够掩盖日常开发中大都场景。更多高档功用如自界说 Logger、读写别离、从数据库表反向生成模型等操作,能够参阅官方文档进行学习。
本文完整代码示例我放在了 GitHub 上,欢迎点击检查。
希望此文能对你有所协助。
联络我
- 微信:jianghushinian
- 邮箱:jianghushinian007@outlook.com
- 博客地址:jianghushinian.cn/
参阅
- GORM 源码:github.com/go-gorm/gor…
- GORM 文档:gorm.io/zh_CN/
- 本文示例代码:github.com/jianghushin…