欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Go語(yǔ)言中ORM框架GORM使用介紹

 更新時(shí)間:2023年06月07日 11:42:44   作者:江湖十年  
GORM是Go語(yǔ)言中最受歡迎的ORM庫(kù)之一,它提供了強(qiáng)大的功能和簡(jiǎn)潔的?API,讓數(shù)據(jù)庫(kù)操作變得更加簡(jiǎn)單和易維護(hù),本文將詳細(xì)介紹GORM的常見用法,包括數(shù)據(jù)庫(kù)連接、模型定義、CRUD、事務(wù)管理等方面,幫助大家快速上手使用GORM進(jìn)行Web后端開發(fā)

安裝

通過如下命令安裝 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)了 ScannerValuer 接口的自定義類型及其指針或別名組成。

例如:

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 文章模型使用 CommentsTags 分別保存關(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.IDPost 字段同樣不會(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)的 CommentsTags

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)行配置文件解析

    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與C語(yǔ)言的互操作實(shí)現(xiàn)

    Go與C語(yǔ)言的互操作實(shí)現(xiàn)

    在Go與C語(yǔ)言互操作方面,Go更是提供了強(qiáng)大的支持。尤其是在Go中使用C,你甚至可以直接在Go源文件中編寫C代碼,本文就詳細(xì)的介紹一下如何使用,感興趣的可以了解一下
    2021-12-12
  • 深入了解Go語(yǔ)言中context的用法

    深入了解Go語(yǔ)言中context的用法

    這篇文章主要為大家詳細(xì)介紹了Go語(yǔ)言中context用法的相關(guān)知識(shí),文中的示例代碼講解詳細(xì),具有一定的學(xué)習(xí)價(jià)值,感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下
    2023-07-07
  • 詳解如何在Go語(yǔ)言中循環(huán)數(shù)據(jù)結(jié)構(gòu)

    詳解如何在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-10
  • Go Java算法最大單詞長(zhǎng)度乘積示例詳解

    Go Java算法最大單詞長(zhǎng)度乘積示例詳解

    這篇文章主要為大家介紹了Go Java算法最大單詞長(zhǎng)度乘積示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-08-08
  • golang規(guī)則引擎gengine用法案例

    golang規(guī)則引擎gengine用法案例

    這篇文章主要為大家介紹了golang?規(guī)則引擎gengine用法案例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2024-01-01
  • go語(yǔ)言中的協(xié)程詳解

    go語(yǔ)言中的協(xié)程詳解

    本文詳細(xì)講解了go語(yǔ)言中的協(xié)程,文中通過示例代碼介紹的非常詳細(xì)。對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2022-07-07
  • Go語(yǔ)言操作redis用法實(shí)例

    Go語(yǔ)言操作redis用法實(shí)例

    這篇文章主要介紹了Go語(yǔ)言操作redis用法,實(shí)例分析了Go語(yǔ)言操作redis的技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下
    2015-02-02
  • golang在GRPC中設(shè)置client的超時(shí)時(shí)間

    golang在GRPC中設(shè)置client的超時(shí)時(shí)間

    這篇文章主要介紹了golang在GRPC中設(shè)置client的超時(shí)時(shí)間,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來(lái)看看吧
    2021-04-04
  • Go語(yǔ)言對(duì)JSON進(jìn)行編碼和解碼的方法

    Go語(yǔ)言對(duì)JSON進(jìn)行編碼和解碼的方法

    這篇文章主要介紹了Go語(yǔ)言對(duì)JSON進(jìn)行編碼和解碼的方法,涉及Go語(yǔ)言操作json的技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下
    2015-02-02

最新評(píng)論