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