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ù)庫文件。
① 進入 SQLite 命令行。
② 查看已存在的數(shù)據(jù)庫表。
③ 設(shè)置稍后查詢表數(shù)據(jù)時的輸出模式為按列左對齊。
④ 查詢表中存在的數(shù)據(jù)。
有過使用 ORM 框架經(jīng)驗的同學(xué),以上代碼即使我不進行講解也能看懂個大概。
這段示例代碼基本能夠概括 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ù)庫進行 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)進行調(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ù)庫表,可以進行 CRUD 操作了。
有些同學(xué)可能有個疑問,以上示例代碼中并沒有類似 defer db.Close()
主動關(guān)閉連接的操作,那么何時關(guān)閉數(shù)據(jù)庫連接?
其實 GORM 維護了一個數(shù)據(jù)庫連接池,初始化 db
后所有的連接都由底層庫來管理,無需程序員手動干預(yù),GORM 會在合適的時機自動關(guān)閉連接。GORM 框架作者 jinzhu
也有在源碼倉庫 Issue 中回復(fù)過網(wǎng)友的提問,感興趣的同學(xué)可以點擊進入查看。
接下來我將對 GORM 的使用進行詳細(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é)可以查看官方文檔進行學(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
來維護數(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ù)庫進行操作了。
創(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 表達式:
// 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 表達式。
刪除
可以使用 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)使用邏輯刪除策略,不會對記錄進行物理刪除。
所以 Delete
方法在對數(shù)據(jù)進行刪除時,實際上執(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ù)情況下不只是對單表進行操作,還要對存在關(guān)聯(lián)關(guān)系的多表進行操作。
這里以一個博客系統(tǒng)最常見的三張表「文章表、評論表、標(biāo)簽表」為例,對 GORM 如何操作關(guān)聯(liá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 操作時全部通過代碼層面來進行業(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 操作進行一一講解。
創(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 會維護其關(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 同樣支持使用表達式:
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}) // 使用表達式 db.Exec(`UPDATE user SET age = ? WHERE name = ?`, gorm.Expr("age * ? + ?", 1, 2), "Jianghu") // 刪除表 db.Exec("DROP TABLE user")
使用 Exec
無法拿到執(zhí)行結(jié)果,可以用來對表進行操作,比如增加、刪除表等。
編寫 SQL 時支持使用 @name
語法命名參數(shù):
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")
使用 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ā)中,遇到問題如何進行調(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 進行了講解,介紹了如何編寫模型,如何連接數(shù)據(jù)庫,以及最常使用的 CRUD 操作。并且還對關(guān)聯(lián)表中的一對多、多對多兩種關(guān)聯(lián)關(guān)系操作進行了講解。我們還介紹了必不可少的功能「事務(wù)」,GORM 還提供了鉤子函數(shù)方便我們在 CRUD 操作前后插入一些自定義邏輯。最后對如何使用原生 SQL 以及如何調(diào)試也進行了介紹。
只要你原生 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語言中循環(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-10golang在GRPC中設(shè)置client的超時時間
這篇文章主要介紹了golang在GRPC中設(shè)置client的超時時間,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2021-04-04