Go語言中ORM框架GORM使用介紹
安裝
通過如下命令安裝 GORM:
$ go get -u gorm.io/gorm
你也許見過使用 go get -u github.com/jinzhu/gorm 命令來安裝 GORM,這個是老版本 v1,現(xiàn)已過時,不建議使用。新版本 v2 已經(jīng)遷移至 github.com/go-gorm/gorm 倉庫下。
快速開始
如下示例代碼帶你快速上手 GORM 的使用:
package main
import (
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
// Product 定義結(jié)構(gòu)體用來映射數(shù)據(jù)庫表
type Product struct {
gorm.Model
Code string
Price uint
}
func main() {
// 建立數(shù)據(jù)庫連接
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
if err != nil {
panic("failed to connect database")
}
// 遷移表結(jié)構(gòu)
db.AutoMigrate(&Product{})
// 增加數(shù)據(jù)
db.Create(&Product{Code: "D42", Price: 100})
// 查找數(shù)據(jù)
var product Product
db.First(&product, 1) // find product with integer primary key
db.First(&product, "code = ?", "D42") // find product with code D42
// 更新數(shù)據(jù) - update product's price to 200
db.Model(&product).Update("Price", 200)
// 更新數(shù)據(jù) - 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"})
// 刪除數(shù)據(jù) - delete product
db.Delete(&product, 1)
}提示:這里使用了
SQLite數(shù)據(jù)庫驅(qū)動,需要通過go get -u gorm.io/driver/sqlite命令安裝。
將以上代碼保存在 main.go 中并執(zhí)行。
$ go run main.go
執(zhí)行完成后,我們將在當(dāng)前目錄下得到 test.db SQLite 數(shù)據(jù)庫文件。
① 進(jìn)入 SQLite 命令行。
② 查看已存在的數(shù)據(jù)庫表。
③ 設(shè)置稍后查詢表數(shù)據(jù)時的輸出模式為按列左對齊。
④ 查詢表中存在的數(shù)據(jù)。
有過使用 ORM 框架經(jīng)驗的同學(xué),以上代碼即使我不進(jìn)行講解也能看懂個大概。
這段示例代碼基本能夠概括 GORM 框架使用套路:
定義結(jié)構(gòu)體映射表結(jié)構(gòu):
Product結(jié)構(gòu)體在 GORM 中稱作「模型」,一個模型對應(yīng)一張數(shù)據(jù)庫表,一個結(jié)構(gòu)體實例對象對應(yīng)一條數(shù)據(jù)庫表記錄。連接數(shù)據(jù)庫:GORM 使用
gorm.Open方法與數(shù)據(jù)庫建立連接,連接建立好后,才能對數(shù)據(jù)庫進(jìn)行 CRUD 操作。自動遷移表結(jié)構(gòu):調(diào)用
db.AutoMigrate方法能夠自動完成在數(shù)據(jù)庫中創(chuàng)建Product結(jié)構(gòu)體所映射的數(shù)據(jù)庫表,并且,當(dāng)Product結(jié)構(gòu)體字段有變更,再次執(zhí)行遷移代碼,GORM 會自動對表結(jié)構(gòu)進(jìn)行調(diào)整,非常方便。不過,我不推薦在生產(chǎn)環(huán)境項目中使用此功能。因為數(shù)據(jù)庫表操作都是高風(fēng)險操作,一定要經(jīng)過多人 Review 并審核通過,才能執(zhí)行操作。GORM 自動遷移功能雖然理論上不會出現(xiàn)問題,但線上操作謹(jǐn)慎為妙,個人認(rèn)為只有在小項目或數(shù)據(jù)不那么重要的項目中使用比較合適。CRUD 操作:遷移好數(shù)據(jù)庫后,就有了數(shù)據(jù)庫表,可以進(jìn)行 CRUD 操作了。
有些同學(xué)可能有個疑問,以上示例代碼中并沒有類似 defer db.Close() 主動關(guān)閉連接的操作,那么何時關(guān)閉數(shù)據(jù)庫連接?
其實 GORM 維護(hù)了一個數(shù)據(jù)庫連接池,初始化 db 后所有的連接都由底層庫來管理,無需程序員手動干預(yù),GORM 會在合適的時機自動關(guān)閉連接。GORM 框架作者 jinzhu 也有在源碼倉庫 Issue 中回復(fù)過網(wǎng)友的提問,感興趣的同學(xué)可以點擊進(jìn)入查看。
接下來我將對 GORM 的使用進(jìn)行詳細(xì)講解。
聲明模型
GORM 使用模型(Model)來映射一張數(shù)據(jù)庫表,模型是標(biāo)準(zhǔn)的 Go struct,由 Go 的基本數(shù)據(jù)類型、實現(xiàn)了 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 字段標(biāo)簽來控制數(shù)據(jù)庫表字段的類型、列大小、默認(rèn)值等屬性,比如使用 column 字段標(biāo)簽來映射數(shù)據(jù)庫中字段名稱。
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 字段標(biāo)簽情況下,GORM 默認(rèn)使用字段名的 snake_case 作為列名。
GORM 默認(rèn)使用結(jié)構(gòu)體名的 snake_cases 作為表名,為結(jié)構(gòu)體實現(xiàn) TableName 方法可以自定義表名。
我更喜歡「顯式勝于隱式」的做法,所以數(shù)據(jù)庫名和表名都會顯示寫出來。
因為我們不使用自動遷移的功能,所以其他字段標(biāo)簽都用不到,就不在此一一介紹了,感興趣的同學(xué)可以查看官方文檔進(jìn)行學(xué)習(xí)。
User 結(jié)構(gòu)體中有一個嵌套的結(jié)構(gòu)體 gorm.Model,它是 GORM 默認(rèn)提供的一個模型 struct,用來簡化用戶模型定義。
GORM 傾向于約定優(yōu)于配置,默認(rèn)情況下,使用 ID 作為主鍵,使用 CreatedAt、UpdatedAt、DeletedAt 字段追蹤記錄的創(chuàng)建、更新、刪除時間。而這幾個字段就定義在 gorm.Model 中:
type Model struct {
ID uint `gorm:"primarykey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt DeletedAt `gorm:"index"`
}由于我們不使用自動遷移功能,所以需要手動編寫 SQL 語句來創(chuàng)建 user 數(shù)據(jù)庫表結(jié)構(gòu):
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='用戶表';
數(shù)據(jù)庫中字段類型要跟 Go 中模型的字段類型相對應(yīng),不兼容的類型可能導(dǎo)致錯誤。
連接數(shù)據(jù)庫
GORM 官方支持的數(shù)據(jù)庫類型有:MySQL、PostgreSQL、SQLite、SQL Server 和 TiDB。
這里使用最常見的 MySQL 作為示例,來講解 GORM 如何連接到數(shù)據(jù)庫。
在前文快速開始的示例代碼中,我們使用 SQLite 數(shù)據(jù)庫時,安裝了 sqlite 驅(qū)動程序。要連接 MySQL 則需要使用 mysql 驅(qū)動。
在 GORM 中定義了 gorm.Dialector 接口來規(guī)范數(shù)據(jù)庫連接操作,實現(xiàn)了此接口的程序我們將其稱為「驅(qū)動」。針對每種數(shù)據(jù)庫,都有對應(yīng)的驅(qū)動,驅(qū)動是獨立于 GORM 庫的,需要單獨引入。
連接 MySQL 數(shù)據(jù)庫的代碼如下:
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{})
}可以發(fā)現(xiàn),這段代碼與連接 SQLite 數(shù)據(jù)庫的代碼如出一轍,這就是面向接口編程的好處。
首先,mysql.Open 接收一個字符串 dsn,DSN 全稱 Data Source Name,翻譯過來叫數(shù)據(jù)庫源名稱。DSN 定義了一個數(shù)據(jù)庫的連接信息,包含用戶名、密碼、數(shù)據(jù)庫 IP、數(shù)據(jù)庫端口、數(shù)據(jù)庫字符集、數(shù)據(jù)庫時區(qū)等信息。DSN 遵循特定格式:
username:password@protocol(address)/dbname?param=value
通過 DSN 所包含的信息,mysql 驅(qū)動就能夠知道以什么方式連接到 MySQL 數(shù)據(jù)庫了。
mysql.Open 返回的正是一個 gorm.Dialector 對象,將其傳遞給 gorm.Open 方法后,我們將得到 *gorm.DB 對象,這個對象可以用來操作數(shù)據(jù)庫。
GORM 使用 database/sql 來維護(hù)數(shù)據(jù)庫連接池,對于連接池我們可以設(shè)置如下幾個參數(shù):
func SetConnect(db *gorm.DB) error {
sqlDB, err := db.DB()
if err != nil {
return err
}
sqlDB.SetMaxOpenConns(100) // 設(shè)置數(shù)據(jù)庫的最大打開連接數(shù)
sqlDB.SetMaxIdleConns(100) // 設(shè)置最大空閑連接數(shù)
sqlDB.SetConnMaxLifetime(10 * time.Second) // 設(shè)置空閑連接最大存活時間
return nil
}現(xiàn)在,數(shù)據(jù)庫連接已經(jīng)建立,我們可以對數(shù)據(jù)庫進(jìn)行操作了。
創(chuàng)建
可以使用 Create 方法創(chuàng)建一條數(shù)據(jù)庫記錄:
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) // 通過數(shù)據(jù)的指針來創(chuàng)建
fmt.Printf("user: %+v\n", user) // user.ID 自動填充
fmt.Printf("affected rows: %d\n", result.RowsAffected)
fmt.Printf("error: %v\n", result.Error)要創(chuàng)建記錄,我們需要先實例化 User 對象,然后將其指針傳遞給 db.Create 方法。
db.Create 方法執(zhí)行完成后,依然返回一個 *gorm.DB 對象。
user.ID 會被自動填充為創(chuàng)建數(shù)據(jù)庫記錄后返回的真實值。
result.RowsAffected 可以拿到此次操作影響行數(shù)。
result.Error 可以知道執(zhí)行 SQL 是否出錯。
在這里,我將 db.Create(&user) 這句 ORM 代碼所生成的原生 SQL 語句放在了注釋中,方便你對比學(xué)習(xí)。并且,之后的示例中我也會這樣做。
Create 方法不僅支持創(chuàng)建單條記錄,它同樣支持批量操作,一次創(chuàng)建多條記錄:
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 語句完成批量創(chuàng)建記錄。
查詢
查詢記錄是我們在日常開發(fā)中使用最多的場景了,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 方法則可以拿到數(shù)據(jù)庫表名,然后使用 SELECT * 語句從數(shù)據(jù)庫中查詢記錄。
根據(jù)生成的 SQL 可以發(fā)現(xiàn) First 方法查詢數(shù)據(jù)默認(rèn)根據(jù)主鍵 ID 升序排序,并且只會過濾刪除時間為 NULL 的數(shù)據(jù),使用 LIMIT 關(guān)鍵字來限制數(shù)據(jù)條數(shù)。
使用 Last 方法可以查詢最后一條數(shù)據(jù),排序規(guī)則為主鍵 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)這里不再查詢單條數(shù)據(jù),所以改用 Find 方法來查詢所有符合條件的記錄。
以上介紹的幾種查詢方法,都是通過 SELECT * 查詢數(shù)據(jù)庫表中的全部字段,我們可以使用 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 方法可以自定義排序規(guī)則:
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)
這段代碼會執(zhí)行兩條查詢語句,之所以能夠采用這種「鏈?zhǔn)秸{(diào)用」的方式執(zhí)行多條 SQL,是因為每個方法返回的都是 *gorm.DB 對象,這也是一種編程技巧。
使用 Count 方法可以統(tǒng)計記錄條數(shù):
var count int64
// SELECT count(*) FROM `user` WHERE `user`.`deleted_at` IS NULL
result = db.Model(&User{}).Count(&count)有時候遇到比較復(fù)雜的業(yè)務(wù),我們可能需要使用 SQL 子查詢,子查詢可以嵌套在另一個查詢中,GORM 允許將 *gorm.DB 對象作為參數(shù)時生成子查詢:
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) 第二個參數(shù)是一個范型 interface{},所以不僅可以接收字符串,GORM 在判斷其類型為 *gorm.DB 時,就會構(gòu)造一個子查詢。
更新
為了講解更新操作,我們需要先查詢一條記錄,之后的更新操作都是基于這條被查詢出來的 User 對象:
var user User // SELECT * FROM `user` WHERE `user`.`deleted_at` IS NULL ORDER BY `user`.`id` LIMIT 1 result := db.First(&user)
更新操作只要修改 User 對象的屬性,然后調(diào)用 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 屬性存在值,不然就變成了創(chuàng)建操作。
Save 方法會保存所有的字段,即使字段是對應(yīng)類型的零值。
除了使用 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 結(jié)構(gòu)體換成 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})此外,更新數(shù)據(jù)時,還可以使用 gorm.Expr 來實現(xiàn) SQL 表達(dá)式:
// 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) 方法調(diào)用會被轉(zhuǎn)換成 age=age + 1 SQL 表達(dá)式。
刪除
可以使用 Delete 方法刪除數(shù)記錄:
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 默認(rèn)使用邏輯刪除策略,不會對記錄進(jìn)行物理刪除。
所以 Delete 方法在對數(shù)據(jù)進(jìn)行刪除時,實際上執(zhí)行的是 SQL UPDATE 操作,而非 DELETE 操作。
將 deleted_at 字段更新為當(dāng)前時間,表示當(dāng)前數(shù)據(jù)已刪除。這也是為什么前文在講解查詢和更新的時候,生成的 SQL 語句都自動附加了 deleted_at IS NULL Where 條件的原因。
這樣就實現(xiàn)了邏輯層面的刪除,數(shù)據(jù)在數(shù)據(jù)庫中仍然存在,但查詢和更新的時候會將其過濾掉。
記錄被刪除后,我們無法通過如下代碼直接查詢到被邏輯刪除的記錄:
// 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 條件。
對于比較重要的數(shù)據(jù),建議使用邏輯刪除,這樣可以在需要的時候恢復(fù)數(shù)據(jù),也便于故障追蹤。
不過,如果明確想要物理刪除一條記錄,同理可以使用 Unscoped 方法:
// DELETE FROM `user` WHERE name = 'JiangHu' AND `user`.`id` = 1
result = db.Unscoped().Where("name = ?", "JiangHu").Delete(&user)關(guān)聯(lián)
日常開發(fā)中,多數(shù)情況下不只是對單表進(jìn)行操作,還要對存在關(guān)聯(lián)關(guān)系的多表進(jìn)行操作。
這里以一個博客系統(tǒng)最常見的三張表「文章表、評論表、標(biāo)簽表」為例,對 GORM 如何操作關(guān)聯(lián)表進(jìn)行講解。
這里涉及最常見的關(guān)聯(lián)關(guān)系:一對多和多對多。一篇文章可以有多條評論,所以文章和評論是一對多關(guān)系;一篇文章可以存在多個標(biāo)簽,每個標(biāo)簽也可以包含多篇文章,所以文章和標(biāo)簽是多對多關(guān)系。
模型定義如下:
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"
}在模型定義中,Post 文章模型使用 Comments 和 Tags 分別保存關(guān)聯(lián)的評論和標(biāo)簽,這兩個字段不會保存在數(shù)據(jù)庫表中。
Comments 字段標(biāo)簽使用 foreignKey 來指明 Comments 表中的外鍵,并使用 constraint 指明了約束條件,references 指明 Comments 表外鍵引用 Post 表的 ID 字段。
其實現(xiàn)在生產(chǎn)環(huán)境中都不再推薦使用外鍵,各個表之間不再有數(shù)據(jù)庫層面的外鍵約束,在做 CRUD 操作時全部通過代碼層面來進(jìn)行業(yè)務(wù)約束。這里為了演示 GORM 的外鍵和級聯(lián)操作功能,所以定義了這些結(jié)構(gòu)體標(biāo)簽。
Tags 字段標(biāo)簽使用 many2many 來指明多對多關(guān)聯(lián)表名。
對于 Comment 模型,PostID 字段就是外鍵,用來保存 Post.ID。Post 字段同樣不會保存在數(shù)據(jù)庫中,這種做法在 ORM 框架中非常常見。
接下來,我將同樣對關(guān)聯(lián)表的 CRUD 操作進(jìn)行一一講解。
創(chuàng)建
創(chuàng)建 Post 時會自動創(chuàng)建與之關(guān)聯(lián)的 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,并且包含兩條評論和兩個標(biāo)簽。
注意 Comment 的 Post 字段引用了 &post,并沒有指定 PostID 外鍵字段,GORM 能夠正確處理它。
以上代碼將生成并依次執(zhí)行如下 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;可以發(fā)現(xiàn),與文章形成一對多關(guān)系的評論以及與文章形成多對多關(guān)系的標(biāo)簽,都會被創(chuàng)建,并且 GORM 會維護(hù)其關(guān)聯(lián)關(guān)系,而且這些操作全部在一個事務(wù)下完成。
此外,前文介紹的 Save 方法不僅能夠更新記錄,實際上它還支持創(chuàng)建記錄,當(dāng) Post 對象不存在主鍵 ID 時,Save 方法將會創(chuàng)建一條新的記錄:
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;查詢
可以使用如下方式,根據(jù) Post 的 ID 查詢與之關(guān)聯(lián)的 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方法的參數(shù)是Comments,即在Post模型中定義的字段,而非評論的模型名Comment。這點一定不要搞錯了,不然執(zhí)行 SQL 時會報錯。
Post 是源模型,主鍵 ID 不能為空。Association 方法指定關(guān)聯(lián)字段名,在 Post 模型中關(guān)聯(lián)的評論使用 Comments 表示。最后使用 Find 方法來查詢關(guān)聯(lián)的評論。
在查詢 Post 時,我們可以預(yù)加載與之關(guān)聯(lián)的 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 方法來指定預(yù)加載的關(guān)聯(lián)字段名,這樣在查詢 Post 記錄時,會將關(guān)聯(lián)字段表的記錄全部查詢出來,并賦值給關(guān)聯(lián)字段。
以上代碼將執(zhí)行如下 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 語句查詢出所有關(guān)聯(lián)記錄,并且將關(guān)聯(lián) Comments 和 Tags 分別賦值給 Post 模型對應(yīng)字段。
當(dāng)遇到多表查詢時,我們通常還會使用 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 方法來實現(xiàn) JOIN 功能,最終使用 Scan 方法可以將查詢結(jié)果掃描到 postComment 對象中。
針對一對多關(guān)聯(lián)關(guān)系,Joins 方法同樣支持預(yù)加載:
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 功能的預(yù)加載無需顯式使用 Preload 來指明,只需要在 Joins 方法中指明一對多關(guān)系中一這一端模型 Post 即可,使用 Find 查詢 Comment 記錄。
根據(jù)生成的 SQL 可以發(fā)現(xiàn)查詢主表為 comment,副表為 post。并且副表的字段都被重命名為 模型名__字段名 的格式,如 Post__title(題外話:如果你使用過 Python 的 Django ORM 框架,那么對這個雙下劃線命名字段的做法應(yīng)該有種似曾相識的感覺)。
更新
同講解單表更新時一樣,我們需要先查詢出一條記錄,用來演示更新操作:
var post Post // SELECT * FROM `post` WHERE `post`.`deleted_at` IS NULL ORDER BY `post`.`id` LIMIT 1 result := db.First(&post)
可以使用如下方法替換 Post 關(guān)聯(lián)的 Comments:
comment := Comment{
Content: "comment3",
}
err := db.Model(&post).Association("Comments").Replace([]*Comment{&comment})仍然使用 Association 方法指定 Post 關(guān)聯(lián)的 Comments,Replace 方法用來完成替換操作。
這里要注意,Replace 方法返回結(jié)果不再是 *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 刪除文章表時,不會刪除關(guān)聯(lián)表的數(shù)據(jù):
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)對于存在關(guān)聯(lián)關(guān)系的記錄,刪除時默認(rèn)同樣采用 UPDATE 操作,且不影響關(guān)聯(lián)數(shù)據(jù)。
如果想要在刪除評論時,順便刪除與文章的關(guān)聯(lián)關(guān)系,可以使用 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)事務(wù)
GORM 提供了對事務(wù)的支持,這在復(fù)雜的業(yè)務(wù)邏輯中是必要的。
要在事務(wù)中執(zhí)行一系列操作,可以使用 Transaction 方法實現(xiàn):
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 方法內(nèi)部的代碼,都將在一個事務(wù)中被處理。Transaction 方法接收一個函數(shù),其參數(shù)為 tx *gorm.DB,事務(wù)中所有數(shù)據(jù)庫的操作,都應(yīng)該使用這個 tx 而非 db。
在執(zhí)行事務(wù)的函數(shù)中,返回任何錯誤,整個事務(wù)都將被回滾,返回 nil 則事務(wù)被提交。
除了使用 Transaction 自動管理事務(wù),我們還可以手動管理事務(wù):
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() 用于開啟事務(wù),并返回 tx,稍后的事務(wù)操作都應(yīng)使用這個 tx 對象。如果在處理事務(wù)的過程中遇到錯誤,可以使用 tx.Rollback() 回滾事務(wù),如果沒有問題,最終可以使用 tx.Commit() 提交事務(wù)。
注意:手動事務(wù),事務(wù)一旦開始,你就應(yīng)該使用
tx處理數(shù)據(jù)庫操作。
鉤子
GORM 還支持 Hook 功能,Hook 是在創(chuàng)建、查詢、更新、刪除等操作之前、之后調(diào)用的函數(shù),用來管理對象的生命周期。
鉤子方法的函數(shù)簽名為 func(*gorm.DB) error,比如以下鉤子函數(shù)在創(chuàng)建操作之前觸發(fā):
func (u *User) BeforeCreate(tx *gorm.DB) (err error) {
u.UUID = uuid.New()
if u.Name == "admin" {
return errors.New("invalid name")
}
return nil
}比如我們?yōu)?User 模型定義 BeforeCreate 鉤子,這樣在創(chuàng)建 User 對象前,GORM 會自動調(diào)用此函數(shù),完成為 User 對象創(chuàng)建 UUID 以及用戶名合法性驗證功能。
GORM 支持的鉤子函數(shù)以及執(zhí)行時機如下:
| 鉤子函數(shù) | 執(zhí)行時機 |
|---|---|
| BeforeSave | 調(diào)用 Save 前 |
| AfterSave | 調(diào)用 Save 后 |
| BeforeCreate | 插入記錄前 |
| AfterCreate | 插入記錄后 |
| BeforeUpdate | 更新記錄前 |
| AfterUpdate | 更新記錄后 |
| BeforeDelete | 刪除記錄前 |
| AfterDelete | 刪除記錄后 |
| AfterFind | 查詢記錄后 |
原生 SQL
雖然我們使用 ORM 框架往往是為了將原生 SQL 的編寫轉(zhuǎn)為面向?qū)ο缶幊蹋贿^對原生 SQL 的支持是一款 ORM 框架必備的功能。
可以使用 Raw 方法執(zhí)行原生查詢 SQL,并將結(jié)果 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 同樣支持使用表達(dá)式:
var sumage int
db.Raw(`SELECT SUM(age) as sumage FROM user WHERE member_number ?`, gorm.Expr("IS NULL")).Scan(&sumage)此外,我們還可以使用 Exec 執(zhí)行任意原生 SQL:
db.Exec("UPDATE user SET age = ? WHERE id IN ?", 18, []int64{1, 2})
// 使用表達(dá)式
db.Exec(`UPDATE user SET age = ? WHERE name = ?`, gorm.Expr("age * ? + ?", 1, 2), "Jianghu")
// 刪除表
db.Exec("DROP TABLE user")使用 Exec 無法拿到執(zhí)行結(jié)果,可以用來對表進(jìn)行操作,比如增加、刪除表等。
編寫 SQL 時支持使用 @name 語法命名參數(shù):
db.Exec("UPDATE user SET age = ? WHERE id IN ?", 18, []int64{1, 2})
// 使用表達(dá)式
db.Exec(`UPDATE user SET age = ? WHERE name = ?`, gorm.Expr("age * ? + ?", 1, 2), "Jianghu")
// 刪除表
db.Exec("DROP TABLE user")使用 DryRun 模式可以直接拿到由 GORM 生成的原生 SQL,而不執(zhí)行,方便后續(xù)使用:
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 模式可以翻譯為空跑,意思是不執(zhí)行真正的 SQL,這在調(diào)試時非常有用。
調(diào)試
GORM 常用功能我們已經(jīng)基本講解完成了,最后再來介紹下在日常開發(fā)中,遇到問題如何進(jìn)行調(diào)試。
GORM 調(diào)試方法我總結(jié)了如下 5 點:
- 全局開啟日志
還記得在連接數(shù)據(jù)庫時 gorm.Open 方法的第二個參數(shù)嗎,我們當(dāng)時傳遞了一個空配置 &gorm.Config{},這個可選的參數(shù)可以改變 GORM 的一些默認(rèn)功能配置,比如我們可以設(shè)置日志級別為 Info,這樣就能夠在控制臺打印所有執(zhí)行的 SQL 語句:
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
Logger:logger.Default.LogMode(logger.Info),
})- 打印慢查詢 SQL
有時候某段 ORM 代碼執(zhí)行很慢,我們可以通過開啟慢查詢?nèi)罩荆瑏頇z測 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{
// 設(shè)定慢查詢時間閾值為 3ms(默認(rèn)值:200 * time.Millisecond)
SlowThreshold: 3 * time.Millisecond,
// 設(shè)置日志級別
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 能夠打印當(dāng)前 ORM 語句執(zhí)行的 SQL:
db.Debug().First(&User{}) - 全局開啟 DryRun 模型
在連接數(shù)據(jù)庫時,我們可以全局開啟「空跑」模式:
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{ DryRun: true, }) 開啟 DryRun 模型后,任何 SQL 語句都不會真正執(zhí)行,方便測試。
- 局部開啟 DryRun 模型
在當(dāng)前 Session 中局部開啟「空跑」模型,可以在不執(zhí)行操作的情況下生成 SQL 及其參數(shù),用于準(zhǔn)備或測試生成的 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}總結(jié)
本文對 Go 語言中最流行的 ORM 框架 GORM 進(jìn)行了講解,介紹了如何編寫模型,如何連接數(shù)據(jù)庫,以及最常使用的 CRUD 操作。并且還對關(guān)聯(lián)表中的一對多、多對多兩種關(guān)聯(lián)關(guān)系操作進(jìn)行了講解。我們還介紹了必不可少的功能「事務(wù)」,GORM 還提供了鉤子函數(shù)方便我們在 CRUD 操作前后插入一些自定義邏輯。最后對如何使用原生 SQL 以及如何調(diào)試也進(jìn)行了介紹。
只要你原生 SQL 基礎(chǔ)扎實,ORM 框架學(xué)習(xí)起來并不會太費力,并且我們還有各種調(diào)試方式來打印 GORM 所生成的 SQL,方便排查問題。
以上就是Go語言中ORM框架GORM使用介紹的詳細(xì)內(nèi)容,更多關(guān)于Go ORM框架GORM的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Go語言基于viper的conf庫進(jìn)行配置文件解析
在現(xiàn)代軟件開發(fā)中,配置文件是不可或缺的一部分,如何高效地將這些格式解析到 Go 結(jié)構(gòu)體中,一直是開發(fā)者的痛點,下面我們來看看如何使用conf進(jìn)行配置文件解析吧2025-03-03
詳解如何在Go語言中循環(huán)數(shù)據(jù)結(jié)構(gòu)
這篇文章主要為大家詳細(xì)介紹了如何在Go語言中循環(huán)數(shù)據(jù)結(jié)構(gòu)(循環(huán)字符串、循環(huán)map結(jié)構(gòu)和循環(huán)Struct),文中的示例代碼代碼講解詳細(xì),需要的可以參考一下2022-10-10
golang在GRPC中設(shè)置client的超時時間
這篇文章主要介紹了golang在GRPC中設(shè)置client的超時時間,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2021-04-04

