[Gorm]Gorm快速入門


1.什么是Gorm

gorm是ORM(Object-Relationl Mapping)的一種,適用於golang的開發,它的作用是在關系型數據庫和對象之間作一個映射,這樣,我們在具體的操作數據庫的時候,就不需要再去和復雜的SQL語句打交道,只要像平時操作對象一樣操作它就可以了 。

換言之,gorm可以讓程序員用代碼來對數據庫進行操作,他對sql語句進行封裝,讓代碼定義的數據結構(結構體的結構)轉換成數據庫的表的結構,讓代碼定義的定位轉換為對數據庫的操作。

gorm是go語言開發數據庫的一個庫,具備很多優秀特性,官方文檔上列出了這樣一些特性。

  • 全功能 ORM
  • 關聯 (Has One,Has Many,Belongs To,Many To Many,多態,單表繼承)
  • Create,Save,Update,Delete,Find 中鈎子方法
  • 支持 PreloadJoins 的預加載
  • 事務,嵌套事務,Save Point,Rollback To Saved Point
  • Context、預編譯模式、DryRun 模式
  • 批量插入,FindInBatches,Find/Create with Map,使用 SQL 表達式、Context Valuer 進行 CRUD
  • SQL 構建器,Upsert,數據庫鎖,Optimizer/Index/Comment Hint,命名參數,子查詢
  • 復合主鍵,索引,約束
  • Auto Migration
  • 自定義 Logger
  • 靈活的可擴展插件 API:Database Resolver(多數據庫,讀寫分離)、Prometheus…
  • 每個特性都經過了測試的重重考驗
  • 開發者友好

2.Gorm的安裝和基本配置

2.1 環境配置

我們首先新建一個數據庫,名字是gorm_test,字符集為utf8mb4。

我們采用mod的方式對gorm進行使用,首先新建一個項目,配置我們的項目的goproxy

添加https://goproxy.cn作為本項目的GOPROXY

新建一個文件,作為連接數據庫的文件。

我們為其導入兩個庫,分別是gorm的本體和對mysql的驅動

import (
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

2.2 最簡方式連接數據庫

只需要在main函數中,設置dsn和open函數即可。

func main() {
	dsn := "root:123456@tcp(127.0.0.1:3306)/gorm_test?charset=utf8mb4&parseTime=True&loc=Local"
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	fmt.Println(db,err)
}

2.3 Config方式連接數據庫

實際在工程中使用更多的是這種方式,通過這種方式可以設置的配置更多

func main() {
	db, err := gorm.Open(mysql.New(mysql.Config{
		DSN: "root:123456@tcp(127.0.0.1:3306)/gorm_test?charset=utf8mb4&parseTime=True&loc=Local", // DSN data source name
		DefaultStringSize: 171, // string 類型字段的默認長度
		DisableDatetimePrecision: true, // 禁用 datetime 精度,MySQL 5.6 之前的數據庫不支持
		DontSupportRenameIndex: true, // 重命名索引時采用刪除並新建的方式,MySQL 5.7 之前的數據庫和 MariaDB 不支持重命名索引
		DontSupportRenameColumn: true, // 用 `change` 重命名列,MySQL 8 之前的數據庫和 MariaDB 不支持重命名列
		SkipInitializeWithVersion: false, // 根據當前 MySQL 版本自動配置
	}), &gorm.Config{})
	fmt.Println(db,err)
}
mysql.Config還有很多可選配置項,如
 
而后面的gorm.Config也有很多可選配置,並且非常常用,給出官方文檔的API供參考。https://gorm.io/zh_CN/docs/gorm_config.html
我們選幾個常用的配置項說明,
&gorm.Config{
		SkipDefaultTransaction: false, //跳過默認事務
		NamingStrategy: schema.NamingStrategy{  //更改默認的命名約定
			TablePrefix: "t_",   // 設定一個新建的表名前綴,比如新建一個`User`表名字會自動改為為`t_users`
			SingularTable: true, // 使用單數表名,啟用該選項后,`User` 表將是`user`
			NameReplacer: strings.NewReplacer("CID", "Cid"), // 在轉為數據庫名稱之前,使用NameReplacer更改結構/字段名稱。
		},
		DisableForeignKeyConstraintWhenMigrating: false, //邏輯外鍵 代碼里自動建立外鍵關系
	}

2.4 連接池

什么是連接池?考慮現有的數據庫連接方式,用戶每一次對數據庫的請求都需要open一次數據庫,創建一個數據庫連接單例db,這樣會給服務器造成極大的負載。數據庫連接是一種關鍵的有限的昂貴的資源,對數據庫連接的管理能顯著影響到整個應用程序的伸縮性和健壯性,影響到程序的性能指標。數據庫連接池負責分配,管理和釋放數據庫連接,它允許應用程序重復使用一個現有的數據庫連接,而不是重新建立一個。

數據庫連接池在初始化時將創建一定數量的數據庫連接放到連接池中, 這些數據庫連接的數量是由最小數據庫連接數來設定的。無論這些數據庫連接是否被使用,連接池都將一直保證至少擁有這么多的連接數量。

連接池的最大數據庫連接數量限定了這個連接池能占有的最大連接數,當應用程序向連接池請求的連接數超過最大連接數量時,這些請求將被加入到等待隊列中。
在gorm中,我們對連接池的配置方式如下:

sqlDB, err := db.DB()

// SetMaxIdleConns 設置空閑連接池中連接的最大數量
sqlDB.SetMaxIdleConns(10)

// SetMaxOpenConns 設置打開數據庫連接的最大數量。
sqlDB.SetMaxOpenConns(100)

// SetConnMaxLifetime 設置了連接可復用的最大時間。
sqlDB.SetConnMaxLifetime(time.Hour)

3.Gorm的基本使用

3.1 遷移

Gorm中的AutoMigrate方法用於自動遷移數據庫中的組織和結構(schema)

比如我們聲明一個struct,命名其為User

type User struct {
  Name string
}

 使用Automigrate的方法將User這個結構體的遷移到數據庫中,作為一個表。

err := db.AutoMigrate(&User{})
if err!=nil{
  fmt.Println(err)
}

 可以看到,生成了一個t_user的表,表中有一個字段name。

我們也可以使用 Migrator 接口對數據庫構建獨立遷移,以下是一些方法。

// 為 `User` 創建表
db.Migrator().CreateTable(&User{})

// 將 "ENGINE=InnoDB" 添加到創建 `User` 的 SQL 里去
db.Set("gorm:table_options", "ENGINE=InnoDB").Migrator().CreateTable(&User{})

// 檢查 `User` 對應的表是否存在
db.Migrator().HasTable(&User{})
db.Migrator().HasTable("users")

// 如果存在表則刪除(刪除時會忽略、刪除外鍵約束)
db.Migrator().DropTable(&User{})
db.Migrator().DropTable("users")

// 重命名表
db.Migrator().RenameTable(&User{}, &UserInfo{})
db.Migrator().RenameTable("users", "user_infos")

還可以對列、約束、索引等結構進行操作,具體實現可以參考官方文檔https://gorm.io/zh_CN/docs/migration.html

3.2 模型聲明

什么是模型?模型就是一個go的結構體,攜帶了gorm規定的標簽或實現了gorm的一些接口。模型是對數據庫操作的基礎,一個好的模型定義會幫助提高數據庫的使用效率。

我們首先聲明一個全局的結構體,以便將其他的文件定義的結構傳入。(在實際使用中一般不直接這么做)

var GLOBAL_DB *gorm.DB

接下來我們新建一個文件,專門用於定義各種struct。定義一個TestUser的結構體,這里使用官方文檔的例子。

type TestUser 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有一個默認的gorm.Model,其包括字段 IDCreatedAtUpdatedAtDeletedAt,已經定義好了數據庫常用的字段,可以用來嵌入各類結構體中。

type Model struct {
  ID        uint           `gorm:"primaryKey"`
  CreatedAt time.Time
  UpdatedAt time.Time
  DeletedAt gorm.DeletedAt `gorm:"index"`
}
 在文件中寫一個函數用來遷移結構體,方便在主函數中直接被調用。
 
func TestUserCreate(){
	GLOBAL_DB.AutoMigrate(&TestUser{})
}

我們在主函數中將數據庫單例復制給全局的數據庫對象,調用這個遷移函數。

GLOBAL_DB = db
TestUserCreate()

會發現數據庫中自動生成了我們定義好的數據庫對象。並且他的命名符合蛇形規范,會將大寫開頭的字母(除首個外)轉為小寫前加一個下划線_

我們會發現id字段自動被設定成了主鍵,這是怎么回事呢,因為使用了標簽tag,gorm支持標簽tag的方法,tag 是可選的,tag 名大小寫不敏感,但建議使用 camelCase 風格,下面列舉了一些常用的tag:

type Model struct {
  UUID uint  `gorm:"primaryKey"`
  Time time.Time `gorm:"column:my_time"`
}
type TestUser struct {
  Model        Model `gorm:"embedded;embeddedPrefix:embed_"`
  Name         string `gorm:"default:xxx"`
  Email        *string `gorm:"not null"`
  Age          uint8 `gorm:"comment:年齡"`
  Birthday     *time.Time
  MemberNumber sql.NullString
  ActivatedAt  sql.NullTime
}

primaryKey標簽設定某一項為主項,column設定行名,emdedded可以實現非匿名的嵌入,輸入not null可以設定為鍵為非空,comment可以設定注釋。下面是官方文檔給出的標簽的使用說明。

4.CRUD

4.1 創建

gorm的創建操作通過以下方式實現,

db_res:= GLOBAL_DB.Create(&[]TestUser{
   {Name:"xxx",Age: 18},
   {Name:"yyy",Age: 18},
   {Name:"zzz",Age: 18},
})

通過為聲明的結構體賦值,再將其取地址傳遞給數據庫單例的Create方法即可,這里可以傳入一個結構體的實例,也可以傳入一個存有結構體實例的切片,會返回一個對象*DB,

包括錯誤信息,創建影響的行數,狀態等,我們接收這個對象,可以用來判斷創建是否成功。

if db_res.Error!=nil{
   fmt.Println("創建失敗!")
}else {
   fmt.Println("創建成功")
}

創建也可以在創建之前通過一些方法對特定字段操作,比如Omit()方法,跳過某個字段,Select創建指定字段等等。

db_res:= GLOBAL_DB.Omit("Name").Create(&TestUser{Name:"該名字將不會被傳入",Age: 18})

創建操作官方文檔:https://gorm.io/zh_CN/docs/create.html

4.2 查詢

查詢需要一個容器來接收查詢返回的結果,通常有兩者方式來實現。

第一種方法可以使用一個map[string]interface{}類型的數據結構來實現它,First()方法表示將首條記錄寫入傳入的參數中。

func TestFind()  {
	var result map[string]interface{}
	GLOBAL_DB.Model(&TestUser{}).First(&result)
	fmt.Println(result)
}

 結果為

第二種我們可以使用一個和聲明的結構體同樣的結構體來接收它。Last()方法表示將最后一條記錄寫入傳入的參數中。

func TestFind()  {
	var User TestUser
	GLOBAL_DB.Model(&TestUser{}).Last(&User)
	fmt.Println(User)
}

 結果為

4.2.1 主鍵查詢

通過主鍵(如果沒有指定主鍵,默認為第一個鍵)來查詢。我們將數據結構設置為如下情況,並將數據庫修改。

type TestUser struct {
	Id			 int `gorm:"primaryKey"`
	Name         string `gorm:"default:xxx"`
	Age          uint8 `gorm:"comment:年齡"`
}

 

通過主鍵查詢的寫法

func TestFind()  {
	var User TestUser
	GLOBAL_DB.First(&User,2)
	fmt.Println(User)
}

我們查找主鍵為2的那條數據。

4.2.2 通過string查詢

第一種方式使用Where函數,類似sql語句的方式進行查詢。 查找name = xxx 且age=12的首個數據項。

func TestFind()  {
	var User TestUser
	GLOBAL_DB.Where("name = ? AND age = 12","xxx").First(&User)
	fmt.Println(User)
}

 第二種方式使用結構體或Map查詢

func TestFind()  {
	var User TestUser
	var User1 TestUser
	GLOBAL_DB.Where(TestUser{Name:"xxx"}).First(&User)
	GLOBAL_DB.Where(map[string]interface{}{
		"name":"xxx",
	}).First(&User1)
	fmt.Println(User)
	fmt.Println(User1)
}

 gorm也提供了一些篩選查詢方式,比如

db.Not("name = ?", "jinzhu").First(&user) //排除這個條件查詢
db.Where("role = ?", "admin").Or("role = ?", "super_admin").Find(&users) //符合or的條件也可以被檢索到
db.Select("name", "age").Find(&users) //Select 允許指定從數據庫中檢索哪些字段, 默認情況下,GORM 會檢索所有字段。
db.Order("age desc, name").Find(&users)//指定從數據庫檢索記錄時的排序方式

db.Offset(3).Find(&users)//Limit 指定獲取記錄的最大數量 Offset 指定在開始返回記錄之前要跳過的記錄數量
rows, err := db.Table("orders").Select("date(created_at) as date, sum(amount) as total").Group("date(created_at)").Having("sum(amount) > ?", 100).Rows()//分組,聚合

也支持模糊查詢

func TestFind()  {
	var User TestUser
	GLOBAL_DB.Where("name LIKE ? ","%x%").Find(&User)
	fmt.Println(User)
}

4.2.3 內聯條件

我們可以使用內聯條件來代替Where函數,Where函數給出的參數均可以放在First的參數中作為內聯條件使用,比如

func TestFind()  {
	var User TestUser
	GLOBAL_DB.First(&User,TestUser{Name:"xxx"})
	fmt.Println(User)
}

 又或者

func TestFind()  {
	var User TestUser
	GLOBAL_DB.Not("age = 12 ").Find(&User,map[string]interface{}{
		"Name":"xxx",
	})
	fmt.Println(User)
}

4.2.4 為查詢指定對象

當我們想把查詢的對象保存在另一個結構體中時,我們會發現我們沒有為查詢指定查詢的對象,可以使用Model為查詢指定查詢的對象,再返回。

type UserInfo struct{
	Name string
	Age uint8
}

func TestFind()  {
  	var u []UserInfo
	GLOBAL_DB.Model(&TestUser{}).Where("name LIKE ? ","%x%").Find(&u)
	fmt.Println(u)
}

4.3 更新

更新和上面的查詢一樣,先查詢到目標函數后更新,只是把Find()函數或者First()函數換成Update()、Updatas()和Save(),第一種只更新選擇的字段,第二種更新所有字段,可以使用Map結構或結構體形式傳入參數,結構體零值不參與更新,第三種無論如何都更新所有的內容,包括零值。

新建文件,我們進行簡單的測試。

func TestUpdate()  {
	GLOBAL_DB.Model(&TestUser{}).Where("name LIKE ? ","%x%").Update("name","帥")
}

 先使用第一種Update(),模糊查找,將所有以x開頭結尾的name都變成帥,結果如下。

使用Save()函數,Save函數會根據主鍵更新,如果沒有主鍵,可以先Find()到需要更新的數據項,對需要更新的項進行更新,再使用Save進行保存。

func TestUpdate()  {
	var users []TestUser
	dbres :=GLOBAL_DB.Model(&TestUser{}).Where("name LIKE ? ","帥").Find(&users)
	for k:= range users{
		users[k].Name = "不帥"
	}
	dbres.Save(&users)
}

使用Updates()函數,可以使用兩種形式進行傳參。

func TestUpdate()  {
	var user TestUser
	GLOBAL_DB.First(&user).Updates(TestUser{Name:"第一條",Age: 0}) // 結構體的0值不會被寫入更新
	GLOBAL_DB.First(&user).Updates(map[string]interface{}{"Name":"第一條","Age": 0}) // map類型的0值會被寫入更新
}

批量更新,使用Find函數先查詢再更新。注意這里的結構體要寫成切片,以便返回所有的結果。所有的數據項都會被修改。

func TestUpdate()  {
	var user []TestUser
	GLOBAL_DB.Find(&user).Updates(map[string]interface{}{"Name":"第一條","Age": 0})
}

4.4 刪除

刪除的用法和更新一樣,也是使用find或first先查找再刪除,但是刪除涉及到一個軟刪除的概念,數據仍然保留在數據庫內,但是標識為已刪除的數據。

我們先來看基礎用法,單條刪除,刪除首個元素。

func TestDelete(){
	var user TestUser
	GLOBAL_DB.First(&user).Delete(&user)
}

 為了演示軟刪除,我們把數據類型嵌入一個gorm.Model,初始化創建數據庫為

使用上面的刪除操作后,可以看到為首個數據項添加了一個delete_at數據,這就是軟刪除。

我們可以使用unscoped()函數來實現直接刪除記錄。

func TestDelete(){
	var user TestUser
	GLOBAL_DB.Unscoped().First(&user).Delete(&user)
}

4.5 原生SQL

我們可以使用Raw()函數實現原生sql語句的直接操作。

func TestRaw(){
	var user TestUser
	GLOBAL_DB.Raw("SELECT * FROM t_test_user WHERE NAME = ?","yyy").Scan(&user)
	fmt.Println(user)
}

查詢name =yyy的數據項

5.一對一關系

5.1 Belong To

belongs to 會與另一個模型建立了一對一的連接。 這種模型的每一個實例都“屬於”另一個模型的一個實例。

我們建立兩個結構體分別是Dog和GirlGod,其中Dog是屬於GirlGod的,每個dog只能屬於一個girlgod。

// `Dog` 屬於 `GirlGod`,`GirlGodID` 是外鍵
type Dog struct{
	gorm.Model
	Name string
	GirlGodID uint
  	GirlGod GirlGod
}

type GirlGod struct{
	gorm.Model
	Name string
}

注意,這里的Dog中有一個屬性為GirlGodID,默認的標志着這個Dog屬於哪一個GirlGod,這個約定默認了他是Dog的外鍵,GirlGod的主鍵為ID。可以通過tag重寫外鍵。

我們只需要創建Dog這個表,gorm會自動為我們創建GirlGod這個表,因為Dog是屬於GirlGod的,不能單獨存在。

func CreateB2(){
	GLOBAL_DB.AutoMigrate(&Dog{})
}

 

我們初始化一個女神和一個舔狗,對舔狗進行創建。

g:=GirlGod{
		Model:gorm.Model{
			ID: 1,
		},
		Name:"女神一號",

	}
d:=Dog{
		Model:gorm.Model{
			ID: 1,
		},
		Name: "舔狗一號",
		GirlGod: g,

}
GLOBAL_DB.Create(&d)
GLOBAL_DB.AutoMigrate(&Dog{})

 可以發現女神被自動創建了,同時舔狗擁有一個女神的id。

我們還可以通過預加載,查找Belong to的那個結構。

我們聲明一個Dog結構體的實例,用於接收返回的結果,對id為1的舔狗進行查詢。

var dog Dog
GLOBAL_DB.First(&dog,1)
fmt.Println(dog)

如果我們想找到該Dog屬於的女神的信息,可以使用預加載,在查找函數前加Preload函數,參數是要查找的那個關聯屬性。

var dog Dog
GLOBAL_DB.Preload("GirlGod").First(&dog,1)
fmt.Println(dog)

 可以看到女神一號被找到了。

5.2 Has One

has one 與另一個模型建立一對一的關聯,但它和一對一關系有些許不同。 這種關聯表明一個模型的每個實例都包含或擁有另一個模型的一個實例。

 我們建立一個Dog和GirlGod結構,每個GirlGod只能擁有一個Dog,

// GirlGod 有一個舔狗Dog,GirlGodID 是外鍵
type GirlGod struct{
	gorm.Model
	Name string
	Dog Dog  //has one 女神擁有一個舔狗
}

type Dog struct{
	gorm.Model
	Name string
	GirlGodID uint  // 被擁有 舔狗指向女神的外鍵
}

注意,每個GirlGod都有一個Dog嵌入結構體,每個Dog都有一個GirlGodID 作為外鍵連接GirlGod。可以通過tag重寫外鍵。

我們實例化一個女神和一個舔狗結構體,只創建女神,但是這時不會自動生成舔狗,因為在has one情況下女神不強制擁有dog

d:=Dog{
		Model:gorm.Model{
			ID: 1,
		},
		Name: "舔狗一號",
	}

g:=GirlGod{
		Model:gorm.Model{
			ID: 1,
		},
		Name:"女神一號",
		Dog: d,
	}
GLOBAL_DB.Create(&g)
GLOBAL_DB.AutoMigrate(&GirlGod{})

 

我們還可以通過預加載,查找Has one的那個結構。

我們創建一個女神和一個舔狗,演示預加載操作。

	var girl GirlGod
	GLOBAL_DB.Preload("Dog").First(&girl,2)
	fmt.Println(girl)
	GLOBAL_DB.AutoMigrate(&GirlGod{})

5.3 使用Gorm建立關系

建立關系

我們清空舔狗和女神的關系,將外鍵都清空,使用belong to的形式,為舔狗添加女神外鍵。

通過grom的Association函數進行建立關系。

d := Dog{
	Model: gorm.Model{
		ID: 1,
	},
}
g := GirlGod{
	Model: gorm.Model{
		ID: 1,
	},
}
GLOBAL_DB.Model(&d).Association("GirlGod").Append(&g)

注意要先創建d的模型,再連接要建立外鍵的屬性,最后指定建立外鍵的對象。

刪除關系

同上,GLOBAL_DB.Model(&d).Association("GirlGod").Delete(&g)

或清除所有關系GLOBAL_DB.Model(&d).Association("GirlGod").Clear()

更新關系

同上,GLOBAL_DB.Model(&d).Association("GirlGod").Replace(&g,&g2),從指定g到指定g2。

同樣的我們對於Has one也可以使用上述的代碼,注意模型的建立和外鍵與對象的更改。

6.一對多關系

為了說明一對多關系的效果,我們在之前結構的基礎上,給Dog增加一個錢包信息的內嵌結構體,用來描述Dog擁有的錢,Dog擁有這個結構體。

type Info struct {  
	gorm.Model
	Money int
	DogID uint
}

type Dog struct {  //擁有Info
	gorm.Model
	Name      string
	GirlGodID uint //舔狗指向女神的外鍵
	Info Info  
}

type GirlGod struct { //擁有dog
	gorm.Model
	Name string
	Dogs []Dog
}

使用AutoMigrate()函數為數據庫引入這個表,手動給Info賦值如下。

可以看到,id為1的dog擁有了100大小的money,id為2的dog擁有了20000大小的money。

創建2個dog表和1個女神表

	d1 :=Dog{
		Model:gorm.Model{
			ID: 1,
		},
		Name: "汪汪一號",
	}
	d2 :=Dog{
		Model:gorm.Model{
			ID: 2,
		},
		Name: "汪汪二號",
	}
	g:=GirlGod{
		Model:gorm.Model{
			ID: 1,
		},
		Name: "女神一號",
		Dogs: []Dog{d1,d2},
	}

id為1的女神擁有id為1和2的dog。

我們對其進行查詢操作。

使用preload查詢女神的dog

var girl GirlGod
GLOBAL_DB.Preload("Dogs").First(&girl)
fmt.Println(girl)
汪汪一號 1 {{0 0001-01-01 00:00:00 +0000 UTC 0001-01-01 00:00:00 +0000 UTC {0001-01-01 00:00:00 +0000 UTC false}} 0 0}} {{2 2021-11-21 16:32:21.965 +0800 CST 2021-11-21 16:32:21.965 +0800 CST {0001-01-01 00:00:00 +0000 UTC false}} 汪汪二號 1 {{0 0001-01-01 00:00:00 +0000 UTC 0001-01-01 00:00:00 +0000 UTC {0001-01-01 00:00:00 +0000 UTC false}} 0 0}}]}

其他的查詢方法

GLOBAL_DB.Preload("Dogs","name = ?","汪汪一號").First(&girl)
GLOBAL_DB.Preload("Dogs",func(db *gorm.DB)*gorm.DB{
	return db.Where("name = ? ","汪汪一號")
}).First(&girl)

多級查詢

我們如果想在查詢女神的時候同時查詢到dog的info信息應該如何操作呢,也可以使用preload()函數。

GLOBAL_DB.Preload("Dogs.Info").First(&girl)

 假如我們想對查詢的結果進行一些限制應該如何操作呢,比如我想限制只輸出money大於300的dog。

可以通過兩級的preload函數實現

GLOBAL_DB.Preload("Dogs.Info","Where money > ?","300").Preload("Dogs").First(&girl)

注意這里的查詢條件語句只能限制當前層的信息,如果在Info層添加Dog的name信息的話,將不會生效。

Join方式預加載查詢

GLOBAL_DB.Preload("Dogs", func(db *gorm.DB)(*gorm.DB) {
	return db.Joins("Info").Where("money > 200")
}).First(&girl) //查詢包括所有Dog的信息

通過這種方式可以多級查詢到只有money項大於200的dog的信息。

7.多對多關系

 我們創建這樣的一個結構體來演示多對多關系。

type Info struct {
   gorm.Model
   Money int
   DogID uint
}

type Dog struct { //擁有Info
   gorm.Model
   Name     string
   Info Info
   GirlGods []GirlGod `gorm:"many2many:dog_girl_god"`
}

type GirlGod struct { //擁有dog
   gorm.Model
   Name string
   Dogs []Dog`gorm:"many2many:dog_girl_god"`
}

一個dog擁有多個女神,一個女神擁有多個dog,是多對多的關系。通過一個中間表連接兩個主鍵dog_girl_god,專門用於存儲兩個關系的變更。

初始化這樣的關系。

i:=Info{
   Money: 200,
}
g1:=GirlGod{
   Model:gorm.Model{
      ID:1,
   },
   Name: "女神一號",
}
g2:=GirlGod{
   Model:gorm.Model{
      ID:2,
   },
   Name: "女神二號",
}
d:=Dog{
   Name: "汪汪二號",
   GirlGods: []GirlGod{g1,g2},
   Info: i,
}

 通過預加載獲取包含的信息。

d:=Dog{
   Model:gorm.Model{
      ID: 1,
   },
}

//預加載獲取女神
GLOBAL_DB.Preload("GirlGods").Find(&d)

 

 

 如果想通過dog查找其擁有的女神,而不想顯示dog的信息,可以通過association實現。

var girls []GirlGod
GLOBAL_DB.Model(&d).Association("GirlGods").Find(&girls)
fmt.Println(girls)

 

 想同時顯示dog和其擁有女神的信息。

var girls []GirlGod
GLOBAL_DB.Model(&d).Preload("Dogs").Association("GirlGods").Find(&girls)
fmt.Println(girls)

使用joins實現條件查詢,查詢money少於10000的dog。

var girls []GirlGod
GLOBAL_DB.Model(&d).Preload("Dogs", func(db *gorm.DB)*gorm.DB {
   return db.Joins("Info").Where("money < ?","10000")
}).Association("GirlGods").Find(&girls)
fmt.Println(girls)

 

 多對多關系的維護

首先要創建一個dog3號。

創建一個結構體用於建立關聯模型Model。

d:=Dog{
   Model:gorm.Model{
      ID: 3,
   },
}

 可以通過如下操作實現關系維護,會修改dog_girl_god表的信息,表示關系的變更。

g1:=GirlGod{
   Model:gorm.Model{
      ID:1,
   },

}
g2:=GirlGod{
   Model:gorm.Model{
      ID:2,
   },
}
GLOBAL_DB.Model(&d).Association("GirlGods").Append(&g1,&g2)//添加id=3的dog對id為1,2的女神的關系
GLOBAL_DB.Model(&d).Association("GirlGods").Delete(&g1)//刪除id=3的dog對id=1的女神的關系
GLOBAL_DB.Model(&d).Association("GirlGods").Replace(&g2) //會刪除當前的所有關系再添加指定的參數
GLOBAL_DB.Model(&d).Association("GirlGods").Clear()//清空id=3的dog的所有關系

8.多態關聯和引用

8.1 多態關聯

為了演示多態關聯的使用,我們創建如下結構。

 

type Jiazi struct {
   ID uint
   Name string
   Gift Gift "polymorphic:Owner"
}
type Yujie struct {
   ID uint
   Name string
   Gift Gift "polymorphic:Owner"
}

type Gift struct {
   ID uint
   Name string
   OwnerType string
   OwnerID   uint
}

 

Jiazi和Yujie都有Gift,gift是被二者復用的,由tag"polymorphic:Owner"所指定,在gift定義一個OwnerType表示被什么類型的結構體使用,Ownerid表示被使用者的id。將結構體遷移到數據庫,建立三個表,分別是

我們為其創建實例:

 

func Polymorphic(){
   //GLOBAL_DB.AutoMigrate(&Jiazi{},&Yujie{},&Gift{})
   GLOBAL_DB.Create(&Jiazi{Name: "夾子一號",Gift:Gift{
      Name: "小風車",
   }})
   GLOBAL_DB.Create(&Yujie{Name: "御姐一號",Gift: Gift{
      Name: "大風車",
   }})
}

 

 

 

可以看到 在的gift中標識了屬於的對象類型和對象id。

我們還可以通過增加標簽tag polymorphicValue:xxx 修改ownertype的默認值。

同時,多態關聯也支持一對多。我們修改兩個結構如下:

 

type Jiazi struct {
   ID uint
   Name string
   Gift []Gift `gorm:"polymorphic:Owner;polymorphicValue:huhu"` //默認類型為類型名,設置value后type可以為指定值
}
type Yujie struct {
   ID uint
   Name string
   Gift Gift `gorm:"polymorphic:Owner;polymorphicValue:dudu"`
}

將jiazi擁有的禮物變成切片,同時更改他們多態類型的值分別為huhu和dudu。

 

func Polymorphic(){
   //GLOBAL_DB.AutoMigrate(&Jiazi{},&Yujie{},&Gift{})
   GLOBAL_DB.Create(&Jiazi{Name: "夾子一號",Gift:[]Gift{
      {Name: "小風車1",},
      {Name: "小風車2",},
   }})
   GLOBAL_DB.Create(&Yujie{Name: "御姐一號",Gift: Gift{
      Name: "大風車",
   }})
}

效果

8.2 更改關聯標簽:外鍵和引用

在一對多的情況下,我們不使用id作為默認的外鍵與外部鏈接,而是使用Name,但是如果單純的更改外鍵,擁有者仍會使用id作為外部引用的標志,我們可以通過更改引用讓擁有者使用name作為外部引用的標志。

 

type Jiazi struct {
	ID uint
	Name string
	Gift []Gift  `gorm:"foreignKey:JiaziName;references:name"`//外鍵指向外部 引用指向自己
}
type Gift struct {
	ID uint
	Name string
	JiaziName string
}

注意:在多對多的情況下,需要將其改為中間表的外鍵和引用。

9.事務操作

什么是事務?事務就是一個不可分割的一系列操作集合。為了確保數據一致性,GORM 會在事務里執行寫入操作(創建、更新、刪除)。如果沒有這方面的要求,可以在初始化時禁用它,這將獲得大約 30%+ 性能提升。

9.1 禁用自帶的事務

可以通過如下方式禁用自帶的事務:

// 全局禁用
db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{
  SkipDefaultTransaction: true,
})

// 持續會話模式
tx := db.Session(&Session{SkipDefaultTransaction: true})
tx.First(&user, 1)
tx.Find(&users)
tx.Model(&user).Update("Age", 18)

9.2 事務特性

為了演示事務的特性,我們作如下測試。

type TMG struct {
   ID uint
   Name string
}

func TestTransaction()  {
   flag := false
   GLOBAL_DB.AutoMigrate(&TMG{})
   GLOBAL_DB.Transaction(func(tx *gorm.DB) error {
      tx.Create(&TMG{Name: "漢字"})
      tx.Create(&TMG{Name: "英語"})
      tx.Create(&TMG{Name: "法語"})
      if flag{
         return nil
      }else {
         return errors.New("出錯")
      }
      return nil
   })
}

執行上述事務,我們會發現定義的三個數據項並不會被創建,因為在事務return之前發生了error,事務具有原子性,會將創建的數據回滾。

作為對比,我們做一個嵌套。

func TestTransaction()  {
   GLOBAL_DB.AutoMigrate(&TMG{})
   GLOBAL_DB.Transaction(func(tx *gorm.DB) error {
      tx.Create(&TMG{Name: "漢字"})
      tx.Create(&TMG{Name: "英語"})
      tx.Transaction(func(tx *gorm.DB) error {
         tx.Create(&TMG{Name: "法語"})
         return errors.New("出錯")
      })
      return nil
   })
}

 這時候嵌套的事務出錯,但是外層漢字和英語兩個項會被創建。

9.3 手動自建事務

func TestTransaction()  {
   GLOBAL_DB.AutoMigrate(&TMG{})
   tx:=GLOBAL_DB.Begin()
   tx.Create(&TMG{
      Name: "漢字",
   })
   tx.Create(&TMG{
      Name: "英語",
   })
   tx.Create(&TMG{
      Name: "法語",
   })
   tx.Commit()
}

通過Begin()開啟一個事務,create等方法進行操作,commit()提交事務。

還可以使用回滾等方法。回滾到存儲的某個狀態。

func TestTransaction()  {
   GLOBAL_DB.AutoMigrate(&TMG{})
   tx:=GLOBAL_DB.Begin()
   tx.Create(&TMG{
      Name: "漢字",
   })
   tx.Create(&TMG{
      Name: "英語",
   })
   tx.SavePoint("存檔點")
   tx.Create(&TMG{
      Name: "法語",
   })
   tx.RollbackTo("存檔點")
   tx.Commit()
}

 

10.自定義數據類型

自定義的數據類型必須實現ScannerValuer接口,以便讓 GORM 知道如何將該類型接收、保存到數據庫。官方文檔:https://gorm.io/zh_CN/docs/data_types.html

10.1 存入數據庫

如果我們想把一個json格式的數據存入數據庫,應該怎么辦呢?

我們可以創建如下格式。

type CInfo struct {
   Name string
   Age int
}

func (c CInfo)Value()(driver.Value,error){  //傳入要自定義數據類型的那個結構
   str,err:=json.Marshal(c)  //將數據編碼為json字符串
   if err!=nil{
      return nil,err
   }
   return string(str),nil  //返回string類型的json字符串
}
func (c CInfo)Scan(value interface{})(error){
   return nil
}
type CUser struct {
   ID uint
   Info CInfo
}

func Customize()  {
   GLOBAL_DB.AutoMigrate(&CUser{})
   GLOBAL_DB.Create(&CUser{Info: CInfo{
      "小明",
      18,
   }})
}

 通過Value函數將目標格式的數據轉為json格式的string放入數據庫,其中返回的driver.Value為最終存入數據庫的數據。

 如果我們此時使用First來查找id=1的數據,會發生什么呢?

var u CUser
   GLOBAL_DB.First(&u)
   fmt.Println(u)
}

 可以看到,查找到的數據項是0。

10.2 在數據庫中查找自定義數據類型

我們如何在數據庫中找到Name等於小明的相關信息呢?

這就需要用到下面的Scan()函數了,我們可以通過實現Scan函數,來完成反解析json。

func (c *CInfo)Scan(value interface{})(error){
   str,ok := value.([]byte)
   if !ok{
      return errors.New("不匹配的數據類型")
   }
   json.Unmarshal(str,c)
   return nil
}

這里用到了go的類型斷言,這里的value是一個interface{}類型的變量,為查詢從數據庫獲得的數據,這里我們用的json格式的字符串。這句的字面含義是“我認為value這個interface{}類型變量的underlying type是[]byte,如果是,請將其值賦給變量str,並且ok =true,如果不是ok = false。

這樣我們就獲取到了json的字符串其形式是[]byte,存放在str中,我們對其進行反序列化,將str的數據反序列化成原始格式,傳到c中,c是最終渲染解析到目標數據結構類型的數據,這里的c是一個指針,會對其進行直接修改。也就是我們在下面給出的var u CUser中的CInfo。

var u CUser
GLOBAL_DB.First(&u)
fmt.Println(u)

 

 再來寫一個案例,我們增加一個字符串切片類型,定義一個字符串切片為Args。

type Args []string
type CInfo struct {
   Name string
   Age  int
}

type CUser struct {
   ID   uint
   Info CInfo
   Args Args
}

 寫一個Value用來處理Args數據類型。

func (a Args) Value() (driver.Value, error) {
   if len(a) > 0 {
      var str string = a[0]
      for _, v := range a[1:] {
         str += "," + v
      }
      return str, nil
   }else{
      return "",nil
   }
}

 這里我們接受這個Args類型,將輸入的字符切片,間隔插入一個逗號拼接成一個字符串。

GLOBAL_DB.Create(&CUser{Info: CInfo{
   Name: "小王",
   Age:  18,
}, Args: Args{"1", "2", "3"}})

 

 對於查詢需求,我們再為Args寫一個Scan函數。

func (a *Args) Scan(value interface{}) error {
   str,ok:=value.([]byte)
   if !ok{
      return errors.New("數據類型無法解析")
   }
   *a = strings.Split(string(str),",")  //按照,為分割將string轉換為一個數組切片
   return nil
}

 先斷言判斷查詢到的數據的類型是否為字符切片,我們將其按照逗號為分割使用split函數將其轉換為一個數組切片,存放到查詢建立的數據載體a中。

var u CUser
GLOBAL_DB.First(&u)
fmt.Println(u)

 

10.3 通過type定義數據庫中的字段名

我們自定義的數據類型放在數據庫中一般會默認為一個類型,這里我們是

 我們可以通過tag來指定類型名,

type CUser struct {
   ID   uint
   Info CInfo `gorm:"type:text"`
   Args Args
}

 我們把Info指定為text類型,重新Automigrate()后查看:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM