Gorm框架

BigSun丶發表於2024-03-15

目錄
  • Go的orm框架
    • 1. gorm
    • 2. facebook-ent
    • 3. xorm
    • 4. upper/db:
    • 5. gorose
  • 一、Gorm介紹
  • 二、gorm連線資料庫
    • 2.1 快速連結mysql
    • 2.2 其他配置
    • 2.3 加入日誌列印sql
      • (1)日誌級別
      • (2)Debug
      • (3)具體程式碼
  • 三、automigrate功能
  • 四、快速增刪改查
    • 4.1 快速增刪改查
    • 4.2 邏輯刪除
  • 五、不能更新零值的問題
    • 5.1 使用sql.NullString更新
    • 5.2 使用指標解決
  • 六、表結構定義細節
    • 6.1 模型定義
    • 6.2 約定
    • 6.3 gorm.Model
    • 6.4 高階選項
      • (1)欄位級許可權控制
      • (2)建立/更新時間追蹤(納秒、毫秒、秒、Time)
      • (3)嵌入結構體
      • (4)欄位標籤
    • 6.5 案例
  • 七、Gorm增加操作
    • 7.1 建立記錄
    • 7.2 用指定的欄位建立記錄
    • 7.3 批次插入
    • 7.4 建立鉤子
    • 7.5 根據 Map 建立
    • 7.6 使用 SQL 表示式、Context Valuer 建立記錄(瞭解)
    • 7.7 高階選項
      • (1)關聯建立(詳見後面關聯關係)
      • (2)預設值
      • (3)Upsert 及衝突(瞭解)
  • 八、Gorm查詢
    • 8.1 檢索單個物件
      • (1)用主鍵檢索
    • 8.2 檢索全部物件
    • 8.3 條件
      • (1)String 條件
      • (2)Struct & Map 條件
      • (3)指定結構體查詢欄位
      • (4)內聯條件
      • (5)Not 條件
      • (6)Or 條件
    • 8.4 選擇特定欄位
    • 8.5 Order
    • 8.6 Limit & Offset
    • 8.7 Group By & Having
    • 8.8 Distinct
    • 8.9 Joins
      • (1)Joins 預載入
      • (2)Joins a Derived Table
    • 8.10 Scan
  • 九、高階查詢(瞭解)
    • 9.1 智慧選擇欄位
    • 9.2 Locking (FOR UPDATE)
    • 9.3 子查詢
      • (1)From 子查詢
    • 9.4 Group 條件
    • 9.5 帶多個列的 In
    • 9.6 命名引數
    • 9.7 Find 至 map
    • 9.8 FirstOrInit
    • 9.9 FirstOrCreate
    • 9.10 最佳化器、索引提示
    • 9.11 迭代
    • 9.12 FindInBatches
    • 9.13 查詢鉤子
    • 9.14 Pluck
    • 9.15 Scope
    • 9.16 Count
  • 十、更新
    • 10.1 儲存所有欄位
    • 10.2 更新單個列
    • 10.3 更新多列
    • 10.4 更新選定欄位
    • 10.5 更新 Hook
    • 10.6 批次更新
      • (1)阻止全域性更新
      • (2)更新的記錄數
    • 10.7 高階選項(瞭解)
      • (1)使用 SQL 表示式更新
      • (2)根據子查詢進行更新
      • (3)不使用 Hook 和時間追蹤
      • (4)返回修改行的資料(瞭解->mysql不支援)
      • (5)檢查欄位是否有變更?
      • (6)在 Update 時修改值
  • 十一、刪除
    • 11.1 刪除一條記錄
    • 11.2 根據主鍵刪除
    • 11.3 Delete Hook
    • 11.4 批次刪除
      • (1)阻止全域性刪除
      • (2)返回刪除行的資料(瞭解)
    • 11.5 軟刪除
      • (1)查詢被軟刪除的記錄
      • (2)永久刪除
      • (3)Delete Flag
  • 十二、SQL 構建器
    • 12.1 原生 SQL
    • 12.2 命名引數
    • 12.3 DryRun 模式
    • 12.4 ToSQL
    • 12.5 Row & Rows
    • 12.6 將 sql.Rows 掃描至 model
    • 12.7 Connection
    • 12.8 Advanced(瞭解)
      • (1)子句(Clause)
      • (2)子句構造器
      • (3)子句選項
      • (4)StatementModifier
  • 十三、關聯之Belongs To
    • 13.1 Belongs To
    • 13.2 重寫外來鍵
    • 13.3 重寫引用(一般不用)
    • 13.4 Belongs to 的 CRUD
    • 13.5 預載入
    • 13.6 外來鍵約束
    • 13.7 案例
  • 十四、透過preload和joins查詢多表
  • 十五、has many關係
  • 十六、多對多的關係
  • 十七、gorm其它操作
    • 17.1 自定義表名
      • (1)完全自定義表名
      • (2)給所有表名增加字首
    • 17.2 自定義事件欄位

Go的orm框架

1. gorm

  • 老牌國產Golang orm框架。支援主流關係型資料庫。中文文件適合新人入手,國內使用較多。最新版本2.x,比1.x有較大改動

  • 注意:Gorm最新地址為https://github.com/go-gorm/gorm,之前https://github.com/jinzhu/gorm地址為v1舊版本

  • Gorm最新原始碼地址:https://github.com/go-gorm/gorm

  • V1版本地址:https://github.com/jinzhu/gorm

  • 中文文件地址:https://gorm.io/zh_CN/

  • star數量:28k

2. facebook-ent

  • facebook開源的,ent是一個簡單而又功能強大的Go語言實體框架,ent易於構建和維護應用程式與大資料模型

  • 地址:https://github.com/ent/ent

  • 文件地址:https://entgo.io/

  • star數量:10.7k

3. xorm

  • 老牌的Go語言 orm框架。支援主流關係型資料庫 支援如下驅動 - Mysql - MyMysql - Postgres - Tidb - SQLite - MsSql - Oracle

  • 舊地址:https://github.com/go-xorm/xorm

  • 新地址:https://gitea.com/xorm/xorm

  • 中文文件地址:https://gitea.com/xorm/xorm/src/branch/master/README_CN.md

  • star數量:6.5k

4. upper/db:

  • 支援PostgreSQL、CockroachDB、MySQL、SQLite和MongoDB的資料訪問層,具有類似ORM的功能

  • 亮點是支援

    • nosql

    • PostgreSQL

    • MySQL

    • MSSQL

    • CockroachDB

    • MongoDB

    • QL

    • SQLite

  • 地址:https://github.com/upper/db

  • 文件地址:https://upper.io/v4/getting-started/

  • star數量:3k

5. gorose

  • 國產輕量級 orm框架,支援如下驅動 - mysql - sqlite3 - postgres - oracle - mssql - clickhouse

  • 地址:https://github.com/gohouse/gorose

  • 文件地址:https://www.kancloud.cn/fizz/gorose-2/1135835

  • star數量:1.1k

一、Gorm介紹

  • Gorm 是 Golang 的一個 orm 框架。ORM 是透過例項物件的語法,完成關係型 資料庫的操作,是"物件-關係對映"(Object/Relational Mapping) 的縮寫。使用 ORM 框架可以讓我們更方便的運算元據庫。

  • Gorm官方支援的資料庫型別有: MySQL, PostgreSQL, SQlite, SQL Server

  • 作者是中國人,中文文件齊全,對開發者友好,支援主流資料庫。

  • 全功能 ORM

  • 關聯 (擁有一個,擁有多個,屬於,多對多,多型,單表繼承)

  • Create,Save,Update,Delete,Find 中鉤子方法

  • 支援 Preload、Joins 的預載入

  • 事務,巢狀事務,Save Point,Rollback To to Saved Point

  • Context、預編譯模式、DryRun 模式

  • 批次插入,FindInBatches,Find/Create with Map,使用 SQL 表示式、Context Valuer 進行 CRUD

  • SQL 構建器,Upsert,鎖,Optimizer/Index/Comment Hint,命名引數,子查詢

  • 複合主鍵,索引,約束

  • 自動遷移

  • 自定義 Logger

  • 靈活的可擴充套件外掛 API:Database Resolver(多資料庫,讀寫分離)、Prometheus…

  • 每個特性都經過了測試的重重考驗

  • 開發者友好

二、gorm連線資料庫

2.1 快速連結mysql

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

func main() {
	// 參考 https://github.com/go-sql-driver/mysql#dsn-data-source-name 獲取詳情
	dsn := "user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
	_, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	//db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{Logger:logger.Default.LogMode(logger.Info)})  列印sql日誌
	if err != nil {
		panic(err) // 如果資料庫不存在會報錯

	}
}

2.2 其他配置

注意:想要正確的處理 time.Time ,您需要帶上 parseTime 引數, (更多引數) 要支援完整的 UTF-8 編碼,您需要將 charset=utf8 更改為 charset=utf8mb4 檢視 此文章 獲取詳情

  • MySQl 驅動程式提供了 一些高階配置 可以在初始化過程中使用,例如:
db, err := gorm.Open(mysql.New(mysql.Config{
  DSN: "gorm:gorm@tcp(127.0.0.1:3306)/gorm?charset=utf8&parseTime=True&loc=Local", // DSN data source name
  DefaultStringSize: 256, // string 型別欄位的預設長度
  DisableDatetimePrecision: true, // 禁用 datetime 精度,MySQL 5.6 之前的資料庫不支援
  DontSupportRenameIndex: true, // 重新命名索引時採用刪除並新建的方式,MySQL 5.7 之前的資料庫和 MariaDB 不支援重新命名索引
  DontSupportRenameColumn: true, // 用 `change` 重新命名列,MySQL 8 之前的資料庫和 MariaDB 不支援重新命名列
  SkipInitializeWithVersion: false, // 根據當前 MySQL 版本自動配置
}), &gorm.Config{})

2.3 加入日誌列印sql

  • Gorm 有一個 預設 logger 實現,預設情況下,它會列印慢 SQL 和錯誤

  • Logger 接受的選項不多,您可以在初始化時自定義它,例如:

newLogger := logger.New(
  log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer(日誌輸出的目標,字首和日誌包含的內容——譯者注)
  logger.Config{
    SlowThreshold: time.Second,   // 慢 SQL 閾值
    LogLevel:      logger.Silent, // 日誌級別
    IgnoreRecordNotFoundError: true,   // 忽略ErrRecordNotFound(記錄未找到)錯誤
    Colorful:      false,         // 禁用彩色列印
  },
)

// 全域性模式
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{
  Logger: newLogger,
})

// 新建會話模式
tx := db.Session(&Session{Logger: newLogger})
tx.First(&user)
tx.Model(&user).Update("Age", 18)

(1)日誌級別

  • GORM 定義了這些日誌級別:SilentErrorWarnInfo
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{
  Logger: logger.Default.LogMode(logger.Silent),
})

(2)Debug

  • Debug 單個操作,將當前操作的 log 級別調整為 logger.Info
db.Debug().Where("name = ?", "jinzhu").First(&User{})

(3)具體程式碼

package main

import (
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
	"log"
	"os"
	"time"
)

type  User struct {
	ID int
}
func main() {
	// 日誌配置
	newLogger := logger.New(
		log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer(日誌輸出的目標,字首和日誌包含的內容——譯者注)
		logger.Config{
			SlowThreshold:             time.Second, // 慢 SQL 閾值
			LogLevel:                  logger.Info, // 日誌級別為info
			IgnoreRecordNotFoundError: true,        // 忽略ErrRecordNotFound(記錄未找到)錯誤
			Colorful:                  true,        // 彩色列印
		},
	)

	dsn := "root:123@tcp(127.0.0.1:3306)/gorm_test?charset=utf8mb4&parseTime=True&loc=Local"
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
		Logger: newLogger,
	})
	if err != nil {
		panic(err) // 如果資料庫不存在會報錯
	}
	db.AutoMigrate(&User{})
}

三、automigrate功能

AutoMigrate自動建立表,可以開啟Navicat檢視錶結構

gorm.Model結構體巢狀

package main

import (
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
	"log"
	"os"
	"time"
)

type Product struct {
	gorm.Model
	Code  string
	Price uint
}
func main() {
	// 日誌配置
	newLogger := logger.New(
		log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer(日誌輸出的目標,字首和日誌包含的內容——譯者注)
		logger.Config{
			SlowThreshold:             time.Second, // 慢 SQL 閾值
			LogLevel:                  logger.Info, // 日誌級別為info
			IgnoreRecordNotFoundError: true,        // 忽略ErrRecordNotFound(記錄未找到)錯誤
			Colorful:                  true,        // 彩色列印
		},
	)

	dsn := "root:123@tcp(127.0.0.1:3306)/gorm_test?charset=utf8mb4&parseTime=True&loc=Local"
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
		Logger: newLogger,
	})
	if err != nil {
		panic(err) // 如果資料庫不存在會報錯
	}
	db.AutoMigrate(&Product{}) // 可以加多個
}

四、快速增刪改查

4.1 快速增刪改查

package main

import (
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
	"log"
	"os"
	"time"
)

type Product struct {
	gorm.Model
	Code  string
	Price uint
}
func main() {
	// 日誌配置
	newLogger := logger.New(
		log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer(日誌輸出的目標,字首和日誌包含的內容——譯者注)
		logger.Config{
			SlowThreshold:             time.Second, // 慢 SQL 閾值
			LogLevel:                  logger.Info, // 日誌級別為info
			IgnoreRecordNotFoundError: true,        // 忽略ErrRecordNotFound(記錄未找到)錯誤
			Colorful:                  true,        // 彩色列印
		},
	)

	dsn := "root:123@tcp(127.0.0.1:3306)/gorm_test?charset=utf8mb4&parseTime=True&loc=Local"
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
		Logger: newLogger,
	})
	if err != nil {
		panic(err) // 如果資料庫不存在會報錯
	}
	//1 遷移 表,可以寫多個
	//db.AutoMigrate(&Product{})

	//2 插入資料
	//db.Create(&Product{Code: "D42", Price: 100})
	//db.Create(&Product{Code: "D43", Price: 150})

	//3 查詢資料
	var product Product  // 定義空Product結構體物件
	////db.First(&product, 1) // 根據整型主鍵查詢
	db.First(&product, "code = ?", "F43") // 查詢 code 欄位值為 D43 的記錄
	//fmt.Println(product)

	//4  更新 - 將 product 的 price 更新為 200
	//db.Model(&product).Update("Price", 200)
	// Update - 更新多個欄位
	//db.Model(&product).Updates(Product{Price: 200, Code: "F43"}) // 僅更新非零值欄位
	//db.Model(&product).Updates(map[string]interface{}{"Price": 300, "Code": "F42"})

	//5  Delete - 刪除 product
	//db.Delete(&product, 1)
}

4.2 邏輯刪除

// 只要使用了gorm.Model結構體繼承,DeletedAt DeletedAt `gorm:"index"`  欄位
// 執行刪除是其實是update語句,並沒有真正的刪除

五、不能更新零值的問題

var product Product
db.First(&product)
db.Model(&product).Updates(Product{Price: 200, Code: ""}) // 僅更新非零值欄位
// UPDATE `products` SET `updated_at`='2022-05-14 23:48:51.96',`price`=200 WHERE `products`.`deleted_at` IS NULL AND `id` = 2

// 可以看到Code欄位不會更新,這是合理的,因為如果零值欄位也更新,Product表中好多資料都會被更新為空

5.1 使用sql.NullString更新

//表模型修改為
type Product struct {
	gorm.Model
	Code  sql.NullString
	Price uint
}
// 修改語句為
db.Model(&product).Updates(Product{Price: 200, Code: sql.NullString{"",true}}) 
	

5.2 使用指標解決

//表模型修改為
type Product struct {
	gorm.Model
	Code  *string
	Price uint
}
// 修改語句為
var product Product
db.First(&product)
var empty = ""
db.Model(&product).Updates(Product{Price: 200, Code: &empty}) // 僅更新非零值欄位

六、表結構定義細節

6.1 模型定義

  • 模型是標準的 struct,由 Go 的基本資料型別、實現了 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
}

6.2 約定

  • GORM 傾向於約定,而不是配置。預設情況下,GORM 使用 ID 作為主鍵,使用結構體名的 蛇形複數 作為表名,欄位名的 蛇形 作為列名,並使用 CreatedAtUpdatedAt 欄位追蹤建立、更新時間

  • 遵循 GORM 已有的約定,可以減少您的配置和程式碼量。如果約定不符合您的需求,GORM 允許您自定義配置它們

6.3 gorm.Model

  • GORM 定義一個 gorm.Model 結構體,其包括欄位 IDCreatedAtUpdatedAtDeletedAt
// gorm.Model 的定義
type Model struct {
  ID        uint           `gorm:"primaryKey"`
  CreatedAt time.Time
  UpdatedAt time.Time
  DeletedAt gorm.DeletedAt `gorm:"index"`
}
  • 您可以將它嵌入到您的結構體中,以包含這幾個欄位,詳情請參考 嵌入結構體

6.4 高階選項

(1)欄位級許可權控制

  • 可匯出的欄位在使用 GORM 進行 CRUD 時擁有全部的許可權,此外,GORM 允許您用標籤控制欄位級別的許可權。這樣您就可以讓一個欄位的許可權是隻讀、只寫、只建立、只更新或者被忽略

注意: 使用 GORM Migrator 建立表時,不會建立被忽略的欄位

type User struct {
  Name string `gorm:"<-:create"` // allow read and create
  Name string `gorm:"<-:update"` // allow read and update
  Name string `gorm:"<-"`        // allow read and write (create and update)
  Name string `gorm:"<-:false"`  // allow read, disable write permission
  Name string `gorm:"->"`        // readonly (disable write permission unless it configured)
  Name string `gorm:"->;<-:create"` // allow read and create
  Name string `gorm:"->:false;<-:create"` // createonly (disabled read from db)
  Name string `gorm:"-"`            // ignore this field when write and read with struct
  Name string `gorm:"-:all"`        // ignore this field when write, read and migrate with struct
  Name string `gorm:"-:migration"`  // ignore this field when migrate with struct
}

(2)建立/更新時間追蹤(納秒、毫秒、秒、Time)

  • GORM 約定使用 CreatedAtUpdatedAt 追蹤建立/更新時間。如果您定義了這種欄位,GORM 在建立、更新時會自動填充 當前時間

  • 要使用不同名稱的欄位,您可以配置 autoCreateTimeautoUpdateTime 標籤

  • 如果您想要儲存 UNIX(毫/納)秒時間戳,而不是 time,您只需簡單地將 time.Time 修改為 int 即可

type User struct {
  CreatedAt time.Time // 在建立時,如果該欄位值為零值,則使用當前時間填充
  UpdatedAt int       // 在建立時該欄位值為零值或者在更新時,使用當前時間戳秒數填充
  Updated   int64 `gorm:"autoUpdateTime:nano"` // 使用時間戳填納秒數充更新時間
  Updated   int64 `gorm:"autoUpdateTime:milli"` // 使用時間戳毫秒數填充更新時間
  Created   int64 `gorm:"autoCreateTime"`      // 使用時間戳秒數填充建立時間
}

(3)嵌入結構體

  • 對於匿名欄位,GORM 會將其欄位包含在父結構體中,例如:
type User struct {
  gorm.Model
  Name string
}
// 等效於
type User struct {
  ID        uint           `gorm:"primaryKey"`
  CreatedAt time.Time
  UpdatedAt time.Time
  DeletedAt gorm.DeletedAt `gorm:"index"`
  Name string
}
  • 對於正常的結構體欄位,你也可以透過標籤 embedded 將其嵌入,例如:
type Author struct {
    Name  string
    Email string
}

type Blog struct {
  ID      int
  Author  Author `gorm:"embedded"`
  Upvotes int32
}
// 等效於
type Blog struct {
  ID    int64
  Name  string
  Email string
  Upvotes  int32
}
  • 並且,您可以使用標籤 embeddedPrefix 來為 db 中的欄位名新增字首,例如:
type Blog struct {
  ID      int
  Author  Author `gorm:"embedded;embeddedPrefix:author_"`
  Upvotes int32
}
// 等效於
type Blog struct {
  ID          int64
  AuthorName  string
  AuthorEmail string
  Upvotes     int32
}

(4)欄位標籤

  • 宣告 model 時,tag 是可選的,GORM 支援以下 tag: tag 名大小寫不敏感,但建議使用 camelCase 風格
標籤名 說明
column 指定 db 列名
type 列資料型別,推薦使用相容性好的通用型別,例如:所有資料庫都支援 bool、int、uint、float、string、time、bytes 並且可以和其他標籤一起使用,例如:not nullsize, autoIncrement… 像 varbinary(8) 這樣指定資料庫資料型別也是支援的。在使用指定資料庫資料型別時,它需要是完整的資料庫資料型別,如:MEDIUMINT UNSIGNED not NULL AUTO_INCREMENT
serializer 指定如何將資料序列化和反序列化到資料庫中的序列化程式,如: serializer:json/gob/unixtime
size 指定列資料大小/長度, 如: size:256
primaryKey 指定列作為主鍵
unique 指定列作為unique
default 指定列的預設值
precision 指定列的精度
scale 指定列的比例
not null 指定列不為空
autoIncrement 指定列自增
autoIncrementIncrement 自動遞增步長,控制連續列值之間的間隔
embedded 嵌入欄位
embeddedPrefix 嵌入嵌入欄位的欄位列名字首
autoCreateTime 跟蹤當前時間建立時,對於'int'欄位,它將跟蹤unix秒,使用值'nano/'milli跟蹤unix nano/milli秒,如: autoCreateTime:nano
autoUpdateTime 在建立/更新時跟蹤當前時間,對於'int'欄位,它將跟蹤unix秒,使用值'nano/'milli跟蹤unix nano/milli秒, 如: autoUpdateTime:milli
index 使用選項建立索引,對多個欄位使用相同的名稱建立複合索引, 詳情參照 Indexes
uniqueIndex 與'index'相同,但建立唯一索引
check 建立檢查約束, 如: check:age > 13, 參照 Constraints
<- 設定欄位的寫入許可權, <-:create 僅建立欄位, <-:update 僅更新欄位, <-:false 沒有寫許可權, <- 建立和更新許可權
-> 設定欄位讀許可權, ->:false 沒有讀許可權
- 忽略該欄位, - 沒有讀寫許可權, -:migration 沒有遷移許可權, -:all 沒有 read/write/migrate 許可權
comment 遷移時為欄位新增註釋

6.5 案例

package main

import (
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
	"log"
	"os"
	"time"
)

type User struct {
	UserId uint `gorm:"primaryKey"`
	Name string `gorm:"column:user_name;type:varchar(60)"`
	Gender uint `gorm:"index"`
}

func main() {
	// 日誌配置
	newLogger := logger.New(
		log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer(日誌輸出的目標,字首和日誌包含的內容——譯者注)
		logger.Config{
			SlowThreshold:             time.Second, // 慢 SQL 閾值
			LogLevel:                  logger.Info, // 日誌級別為info
			IgnoreRecordNotFoundError: true,        // 忽略ErrRecordNotFound(記錄未找到)錯誤
			Colorful:                  true,        // 彩色列印
		},
	)

	dsn := "root:123@tcp(127.0.0.1:3306)/gorm_test?charset=utf8mb4&parseTime=True&loc=Local"
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
		Logger: newLogger,
	})
	if err != nil {
		panic(err) // 如果資料庫不存在會報錯
	}
	db.AutoMigrate(&User{}) 
  // 如果表之前存在會修改,但是隻會修改之前存在的欄位,有問題
  // 改了欄位屬性,再執行AutoMigrate,欄位屬性會變,設定default測試看
}

// 遷移完後在Navicat中檢視

七、Gorm增加操作

  • 我們儘量按照官方文件來看,網上教程都是老版本居多

[image-20220515000442200](http://photo.liuqingzheng.top/20220515-image-20220515000442200 .png)

package main

import (
	"database/sql"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"time"
)

type User struct {
	gorm.Model
	Name         string
	Age          uint8
	Birthday     time.Time
}

func main() {
	dsn := "root:123@tcp(127.0.0.1:3306)/gorm_test?charset=utf8mb4&parseTime=True&loc=Local"
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	if err != nil {
		panic("failed to connect database")
	}
	db.AutoMigrate(&User{})

}

7.1 建立記錄

user := User{Name: "liuqingzheng", Age: 18, Birthday: time.Now()}
result := db.Create(&user) // 透過資料的指標來建立
fmt.Println(user.ID) // 返回插入資料的主鍵
fmt.Println(result.Error)        // 返回 error
fmt.Println(result.RowsAffected)  // 返回插入記錄的條數

7.2 用指定的欄位建立記錄

  • 建立記錄並更新給出的欄位。
user := User{Name: "lqz", Age: 19, Birthday: time.Now()}
db.Select("Name", "Age", "CreatedAt").Create(&user)
// INSERT INTO `users` (`name`,`age`,`created_at`) VALUES ("lqz", 19, "2022-07-04 11:05:21.775")
  • 建立一個記錄且一同忽略傳遞給略去的欄位值。
user := User{Name: "dlrb", Age: 20, Birthday: time.Now()}
db.Omit("Name", "Age", "CreatedAt").Create(&user)
// INSERT INTO `users` (`birthday`,`updated_at`) VALUES ("2022-01-01 00:00:00.000", "2022-07-04 11:05:21.775")

7.3 批次插入

  • 要有效地插入大量記錄,請將一個 slice 傳遞給 Create 方法。 GORM 將生成單獨一條SQL語句來插入所有資料,並回填主鍵的值,鉤子方法也會被呼叫。
t,_:=time.Parse("2006-01-02","1998-07-12")
var users = []User{{Name: "彭于晏",Birthday: t}, {Name: "劉清政",Birthday: t}, {Name: "古天樂",Birthday: t}}
db.Create(&users)
for _, user := range users {
	fmt.Println(user.ID)
}
因為Mysql5.7版本及以上版本的datetime值不能為'0000-00-00 00:00:00',
//處理方法:
修改mysql.ini
在[mysqld]新增一項:
sql_mode=NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO
  • 使用 CreateInBatches 分批建立時,你可以指定每批的數量,例如:
	var users [20]User
	t, _ := time.Parse("2006-01-02", "1999-07-12")
	for i, _ := range users {
		users[i].Name = fmt.Sprintf("劉清政%d號", i)
		users[i].Birthday = t
	}
	fmt.Println(users)

	db.CreateInBatches(&users,10)
  • UpsertCreate With Associations 也支援批次插入

注意 使用CreateBatchSize 選項初始化 GORM 時,所有的建立& 關聯 INSERT 都將遵循該選項

// 關聯插入
db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{
  CreateBatchSize: 1000,
})

db := db.Session(&gorm.Session{CreateBatchSize: 1000})

users = [5000]User{{Name: "jinzhu", Pets: []Pet{pet1, pet2, pet3}}...}

db.Create(&users)
// INSERT INTO users xxx (5 batches)
// INSERT INTO pets xxx (15 batches)

7.4 建立鉤子

  • GORM 允許使用者定義的鉤子有 BeforeSave, BeforeCreate, AfterSave, AfterCreate 建立記錄時將呼叫這些鉤子方法,請參考 Hooks 中關於生命週期的詳細資訊
// main中
db.Create(&User{
		Name: "lqz_nb",
		Birthday: time.Now(),
})
// 插入之前的鉤子函式
func (u *User) BeforeCreate(tx *gorm.DB) (err error) {
	fmt.Println("User被插入了")
	return
}
  • 如果您想跳過 鉤子 方法,您可以使用 SkipHooks 會話模式,例如:
DB.Session(&gorm.Session{SkipHooks: true}).Create(&user)

DB.Session(&gorm.Session{SkipHooks: true}).Create(&users)

DB.Session(&gorm.Session{SkipHooks: true}).CreateInBatches(users, 100)

7.5 根據 Map 建立

  • GORM 支援根據 map[string]interface{}[]map[string]interface{}{} 建立記錄,例如:
// 此處可以不傳Birthday因為它是null的
	db.Model(&User{}).Create(map[string]interface{}{
		"Name": "lqz_001", "Age": 18,
	})
	// batch insert from `[]map[string]interface{}{}`
	db.Model(&User{}).Create([]map[string]interface{}{
		{"Name": "lqz_002", "Age": 18},
		{"Name": "lqz_003", "Age": 20},
	})

注意: 根據 map 建立記錄時,association 不會被呼叫,且主鍵也不會自動填充

7.6 使用 SQL 表示式、Context Valuer 建立記錄(瞭解)

  • GORM 允許使用 SQL 表示式插入資料,有兩種方法實現這個目標。根據 map[string]interface{}自定義資料型別 建立,例如:
// 透過 map 建立記錄
db.Model(User{}).Create(map[string]interface{}{
  "Name": "jinzhu",
  "Location": clause.Expr{SQL: "ST_PointFromText(?)", Vars: []interface{}{"POINT(100 100)"}},
})
// INSERT INTO `users` (`name`,`location`) VALUES ("jinzhu",ST_PointFromText("POINT(100 100)"));

// 透過自定義型別建立記錄
type Location struct {
    X, Y int
}

// Scan 方法實現了 sql.Scanner 介面
func (loc *Location) Scan(v interface{}) error {
  // Scan a value into struct from database driver
}

func (loc Location) GormDataType() string {
  return "geometry"
}

func (loc Location) GormValue(ctx context.Context, db *gorm.DB) clause.Expr {
  return clause.Expr{
    SQL:  "ST_PointFromText(?)",
    Vars: []interface{}{fmt.Sprintf("POINT(%d %d)", loc.X, loc.Y)},
  }
}

type User struct {
  Name     string
  Location Location
}

db.Create(&User{
  Name:     "jinzhu",
  Location: Location{X: 100, Y: 100},
})
// INSERT INTO `users` (`name`,`location`) VALUES ("jinzhu",ST_PointFromText("POINT(100 100)"))

7.7 高階選項

(1)關聯建立(詳見後面關聯關係)

  • 建立關聯資料時,如果關聯值是非零值,這些關聯會被 upsert,且它們的 Hook 方法也會被呼叫
type CreditCard struct {
  gorm.Model
  Number   string
  UserID   uint
}

type User struct {
  gorm.Model
  Name       string
  CreditCard CreditCard
}

db.Create(&User{
  Name: "jinzhu",
  CreditCard: CreditCard{Number: "411111111111"}
})
// INSERT INTO `users` ...
// INSERT INTO `credit_cards` ...
  • 您也可以透過 SelectOmit 跳過關聯儲存,例如:
db.Omit("CreditCard").Create(&user)

// 跳過所有關聯
db.Omit(clause.Associations).Create(&user)

(2)預設值

  • 您可以透過標籤 default 為欄位定義預設值,如:
type User struct {
  ID   int64
  Name string `gorm:"default:galeone"`
  Age  int64  `gorm:"default:18"`
}
  • 插入記錄到資料庫時,預設值 會被用於 填充值為 零值 的欄位

注意 對於宣告瞭預設值的欄位,像 0''false 等零值是不會儲存到資料庫。您需要使用指標型別或 Scanner/Valuer 來避免這個問題,例如:

type User struct {
  gorm.Model
  Name string
  Age  *int           `gorm:"default:18"`
  Active sql.NullBool `gorm:"default:true"`
}

注意 若要資料庫有預設、虛擬/生成的值,你必須為欄位設定 default 標籤。若要在遷移時跳過預設值定義,你可以使用 default:(-),例如:

type User struct {
  ID        string `gorm:"default:uuid_generate_v3()"` // db func
  FirstName string
  LastName  string
  Age       uint8
  FullName  string `gorm:"->;type:GENERATED ALWAYS AS (concat(firstname,' ',lastname));default:(-);"`
}
  • 使用虛擬/生成的值時,你可能需要禁用它的建立、更新許可權,檢視 欄位級許可權 獲取詳情

(3)Upsert 及衝突(瞭解)

  • GORM 為不同資料庫提供了相容的 Upsert 支援

  • 有時候插入資料,報主鍵衝突,有upsert 既可以更新資料,又可以插入資料,來解決這個問題,如果主鍵存在就更新

import "gorm.io/gorm/clause"

// 在衝突時,什麼都不做
db.Clauses(clause.OnConflict{DoNothing: true}).Create(&user)

// 在`id`衝突時,將列更新為預設值
db.Clauses(clause.OnConflict{
  Columns:   []clause.Column{{Name: "id"}},
  DoUpdates: clause.Assignments(map[string]interface{}{"role": "user"}),
}).Create(&users)
// MERGE INTO "users" USING *** WHEN NOT MATCHED THEN INSERT *** WHEN MATCHED THEN UPDATE SET ***; SQL Server
// INSERT INTO `users` *** ON DUPLICATE KEY UPDATE ***; MySQL

// 使用SQL語句
db.Clauses(clause.OnConflict{
  Columns:   []clause.Column{{Name: "id"}},
  DoUpdates: clause.Assignments(map[string]interface{}{"count": gorm.Expr("GREATEST(count, VALUES(count))")}),
}).Create(&users)
// INSERT INTO `users` *** ON DUPLICATE KEY UPDATE `count`=GREATEST(count, VALUES(count));

// 在`id`衝突時,將列更新為新值
db.Clauses(clause.OnConflict{
  Columns:   []clause.Column{{Name: "id"}},
  DoUpdates: clause.AssignmentColumns([]string{"name", "age"}),
}).Create(&users)
// MERGE INTO "users" USING *** WHEN NOT MATCHED THEN INSERT *** WHEN MATCHED THEN UPDATE SET "name"="excluded"."name"; SQL Server
// INSERT INTO "users" *** ON CONFLICT ("id") DO UPDATE SET "name"="excluded"."name", "age"="excluded"."age"; PostgreSQL
// INSERT INTO `users` *** ON DUPLICATE KEY UPDATE `name`=VALUES(name),`age=VALUES(age); MySQL

// 在衝突時,更新除主鍵以外的所有列到新值----》這個用的多
db.Clauses(clause.OnConflict{
  UpdateAll: true,
}).Create(&users)
// INSERT INTO "users" *** ON CONFLICT ("id") DO UPDATE SET "name"="excluded"."name", "age"="excluded"."age", ...;
  • 您還可以檢視 高階查詢 中的 FirstOrInitFirstOrCreate

  • 檢視 原生 SQL 及構造器 獲取更多細節

八、Gorm查詢

8.1 檢索單個物件

  • GORM 提供了 FirstTakeLast 方法,以便從資料庫中檢索單個物件。當查詢資料庫時它新增了 LIMIT 1 條件,且沒有找到記錄時,它會返回 ErrRecordNotFound 錯誤
	var user User
	// 獲取第一條記錄(主鍵升序)
	db.First(&user)
	// SELECT * FROM users ORDER BY id LIMIT 1;

	// 獲取一條記錄,沒有指定排序欄位
	db.Take(&user)
	//SELECT * FROM users LIMIT 1;

	// 獲取最後一條記錄(主鍵降序)
	db.Last(&user)
	// SELECT * FROM users ORDER BY id DESC LIMIT 1;
	fmt.Println(user)

	result := db.First(&user)
	fmt.Println(result.RowsAffected) // 返回找到的記錄數
	fmt.Println(result.Error)       // returns error or nil

	// 檢查 返回的錯誤是否是沒找到記錄的錯誤  ErrRecordNotFound 錯誤
	fmt.Println(errors.Is(result.Error, gorm.ErrRecordNotFound))

如果你想避免ErrRecordNotFound錯誤,你可以使用Find,比如db.Limit(1).Find(&user)Find方法可以接受struct和slice的資料。

  • FirstLast 會根據主鍵排序,分別查詢第一條和最後一條記錄。 只有在目標 struct 是指標或者透過 db.Model() 指定 model 時,該方法才有效。 此外,如果相關 model 沒有定義主鍵,那麼將按 model 的第一個欄位進行排序。 例如:
var user User
var users []User

// works because destination struct is passed in
// 因為user自動和User彪啊關聯了
db.First(&user)
// SELECT * FROM `users` ORDER BY `users`.`id` LIMIT 1

// works because model is specified using `db.Model()`
// 使用map,需要指定跟那個表關聯
result := map[string]interface{}{}
db.Model(&User{}).First(&result)
// SELECT * FROM `users` ORDER BY `users`.`id` LIMIT 1

// doesn't work-->沒指定Model,無效
result := map[string]interface{}{}
db.Table("users").First(&result)

// works with Take,使用Take就生效
result := map[string]interface{}{}
db.Table("users").Take(&result)

// no primary key defined, results will be ordered by first field (i.e., `Code`)
// 沒有指定主鍵,就以第一個欄位排序
type Language struct {
  Code string
  Name string
}
db.First(&Language{})
// SELECT * FROM `languages` ORDER BY `languages`.`code` LIMIT 1

(1)用主鍵檢索

  • 如果主鍵是數字型別,您可以使用 內聯條件 來檢索物件。 傳入字串引數時,需要特別注意 SQL 注入問題,檢視 安全 獲取詳情.
	var user User
	var users []User
	db.First(&user, 10)
	// SELECT * FROM users WHERE id = 10;

	db.First(&user, "10")
	// SELECT * FROM users WHERE id = 10;

	db.Find(&users, []int{1,2,3})
	// SELECT * FROM users WHERE id IN (1,2,3);
	fmt.Println(user)
	fmt.Println(users)
  • 如果主鍵是字串(例如像 uuid),查詢將被寫成這樣:
db.First(&user, "id = ?", "1b74413f-f3b8-409f-ac47-e8c062e3472a")
// SELECT * FROM users WHERE id = "1b74413f-f3b8-409f-ac47-e8c062e3472a";
  • When the destination object has a primary value, the primary key will be used to build the condition, for example:
//當目標物件具有主鍵值時,主鍵將用於構建條件
var user = User{ID: 10}
db.First(&user)
// SELECT * FROM users WHERE id = 10;

var result User
db.Model(User{ID: 10}).First(&result)
// SELECT * FROM users WHERE id = 10;

8.2 檢索全部物件

	var users []User
	// Get all records
	result := db.Find(&users)
	// SELECT * FROM users;

	fmt.Println(result.RowsAffected) // returns found records count, equals `len(users)`
	fmt.Println(result.Error)        // returns error

8.3 條件

(1)String 條件

// Get first matched record
db.Where("name = ?", "jinzhu").First(&user)
// SELECT * FROM users WHERE name = 'jinzhu' ORDER BY id LIMIT 1;

// Get all matched records
db.Where("name <> ?", "jinzhu").Find(&users)
// SELECT * FROM users WHERE name <> 'jinzhu';

// IN
db.Where("name IN ?", []string{"jinzhu", "jinzhu 2"}).Find(&users)
// SELECT * FROM users WHERE name IN ('jinzhu','jinzhu 2');

// LIKE
db.Where("name LIKE ?", "%jin%").Find(&users)
// SELECT * FROM users WHERE name LIKE '%jin%';

// AND
db.Where("name = ? AND age >= ?", "jinzhu", "22").Find(&users)
// SELECT * FROM users WHERE name = 'jinzhu' AND age >= 22;

// Time
db.Where("updated_at > ?", lastWeek).Find(&users)
// SELECT * FROM users WHERE updated_at > '2000-01-01 00:00:00';

// BETWEEN
db.Where("created_at BETWEEN ? AND ?", lastWeek, today).Find(&users)
// SELECT * FROM users WHERE created_at BETWEEN '2000-01-01 00:00:00' AND '2000-01-08 00:00:00';

(2)Struct & Map 條件

// Struct
db.Where(&User{Name: "jinzhu", Age: 20}).First(&user)
// SELECT * FROM users WHERE name = "jinzhu" AND age = 20 ORDER BY id LIMIT 1;

// Map
db.Where(map[string]interface{}{"name": "jinzhu", "age": 20}).Find(&users)
// SELECT * FROM users WHERE name = "jinzhu" AND age = 20;

// Slice of primary keys
db.Where([]int64{20, 21, 22}).Find(&users)
// SELECT * FROM users WHERE id IN (20, 21, 22);

NOTE When querying with struct, GORM will only query with non-zero fields, that means if your field’s value is 0, '', false or other zero values, it won’t be used to build query conditions, for example:

// 當使用結構體作為查詢條件,gorm只會查詢非0欄位,如果欄位是`0`, `''`, `false` or other zero values,該欄位不會被用於構建查詢條件
db.Where(&User{Name: "jinzhu", Age: 0}).Find(&users)
// SELECT * FROM users WHERE name = "jinzhu";
  • To include zero values in the query conditions, you can use a map, which will include all key-values as query conditions, for example:
// 如果想要在查詢中包含0的欄位,可以使用map來做
db.Where(map[string]interface{}{"Name": "jinzhu", "Age": 0}).Find(&users)
// SELECT * FROM users WHERE name = "jinzhu" AND age = 0;
  • For more details, see Specify Struct search fields.

(3)指定結構體查詢欄位

  • When searching with struct, you can specify which particular values from the struct to use in the query conditions by passing in the relevant field name or the dbname to Where(), for example:
//在使用struct進行搜尋時,可以透過將相關欄位名或資料庫名傳遞給`Where(),來指定在查詢條件中使用struct中的哪些特定值`
db.Where(&User{Name: "jinzhu"}, "name", "Age").Find(&users)
// SELECT * FROM users WHERE name = "jinzhu" AND age = 0;

db.Where(&User{Name: "jinzhu"}, "Age").Find(&users)
// SELECT * FROM users WHERE age = 0;

(4)內聯條件

  • 查詢條件可以以類似於Where的方式內聯到'First'和'Find'等方法中
// 如果是非整形主鍵,根據主鍵獲取記錄
db.First(&user, "id = ?", "string_primary_key")
// SELECT * FROM users WHERE id = 'string_primary_key';

// Plain SQL
db.Find(&user, "name = ?", "jinzhu")
// SELECT * FROM users WHERE name = "jinzhu";

db.Find(&users, "name <> ? AND age > ?", "jinzhu", 20)
// SELECT * FROM users WHERE name <> "jinzhu" AND age > 20;

// Struct
db.Find(&users, User{Age: 20})
// SELECT * FROM users WHERE age = 20;

// Map
db.Find(&users, map[string]interface{}{"age": 20})
// SELECT * FROM users WHERE age = 20;

(5)Not 條件

  • Build NOT conditions, works similar to Where

  • 構建NOT條件,工作原理同Where

db.Not("name = ?", "jinzhu").First(&user)
// SELECT * FROM users WHERE NOT name = "jinzhu" ORDER BY id LIMIT 1;

// Not In
db.Not(map[string]interface{}{"name": []string{"jinzhu", "jinzhu 2"}}).Find(&users)
// SELECT * FROM users WHERE name NOT IN ("jinzhu", "jinzhu 2");

// Struct
db.Not(User{Name: "jinzhu", Age: 18}).First(&user)
// SELECT * FROM users WHERE name <> "jinzhu" AND age <> 18 ORDER BY id LIMIT 1;

// Not In slice of primary keys
db.Not([]int64{1,2,3}).First(&user)
// SELECT * FROM users WHERE id NOT IN (1,2,3) ORDER BY id LIMIT 1;

(6)Or 條件

db.Where("role = ?", "admin").Or("role = ?", "super_admin").Find(&users)
// SELECT * FROM users WHERE role = 'admin' OR role = 'super_admin';

// Struct
db.Where("name = 'jinzhu'").Or(User{Name: "jinzhu 2", Age: 18}).Find(&users)
// SELECT * FROM users WHERE name = 'jinzhu' OR (name = 'jinzhu 2' AND age = 18);

// Map
db.Where("name = 'jinzhu'").Or(map[string]interface{}{"name": "jinzhu 2", "age": 18}).Find(&users)
// SELECT * FROM users WHERE name = 'jinzhu' OR (name = 'jinzhu 2' AND age = 18);
  • For more complicated SQL queries. please also refer to Group Conditions in Advanced Query.

8.4 選擇特定欄位

  • Select允許您指定要從資料庫檢索的欄位。否則,GORM將預設選擇所有欄位
db.Select("name", "age").Find(&users)
// SELECT name, age FROM users;

db.Select([]string{"name", "age"}).Find(&users)
// SELECT name, age FROM users;

db.Table("users").Select("COALESCE(age,?)", 42).Rows()
// SELECT COALESCE(age,'42') FROM users;
  • Also check out Smart Select Fields

8.5 Order

  • Specify order when retrieving records from the database

  • 指定從資料庫檢索記錄時的順序

db.Order("age desc, name").Find(&users)
// SELECT * FROM users ORDER BY age desc, name;

// Multiple orders
db.Order("age desc").Order("name").Find(&users)
// SELECT * FROM users ORDER BY age desc, name;

db.Clauses(clause.OrderBy{
  Expression: clause.Expr{SQL: "FIELD(id,?)", Vars: []interface{}{[]int{1, 2, 3}}, WithoutParentheses: true},
}).Find(&User{})
// SELECT * FROM users ORDER BY FIELD(id,1,2,3)

8.6 Limit & Offset

  • Limit specify the max number of records to retrieve Offset specify the number of records to skip before starting to return the records

  • Limit指定要檢索的最大記錄數Offset指定開始返回記錄之前要跳過的記錄數

db.Limit(3).Find(&users)
// SELECT * FROM users LIMIT 3;

// Cancel limit condition with -1
db.Limit(10).Find(&users1).Limit(-1).Find(&users2)
// SELECT * FROM users LIMIT 10; (users1)
// SELECT * FROM users; (users2)

db.Offset(3).Find(&users)
// SELECT * FROM users OFFSET 3;

db.Limit(10).Offset(5).Find(&users)
// SELECT * FROM users OFFSET 5 LIMIT 10;

// Cancel offset condition with -1
db.Offset(10).Find(&users1).Offset(-1).Find(&users2)
// SELECT * FROM users OFFSET 10; (users1)
// SELECT * FROM users; (users2)
  • Refer to Pagination for details on how to make a paginator

8.7 Group By & Having

type result struct {
  Date  time.Time
  Total int
}

db.Model(&User{}).Select("name, sum(age) as total").Where("name LIKE ?", "group%").Group("name").First(&result)
// SELECT name, sum(age) as total FROM `users` WHERE name LIKE "group%" GROUP BY `name` LIMIT 1


db.Model(&User{}).Select("name, sum(age) as total").Group("name").Having("name = ?", "group").Find(&result)
// SELECT name, sum(age) as total FROM `users` GROUP BY `name` HAVING name = "group"

rows, err := db.Table("orders").Select("date(created_at) as date, sum(amount) as total").Group("date(created_at)").Rows()
defer rows.Close()
for rows.Next() {
  ...
}

rows, err := db.Table("orders").Select("date(created_at) as date, sum(amount) as total").Group("date(created_at)").Having("sum(amount) > ?", 100).Rows()
defer rows.Close()
for rows.Next() {
  ...
}

type Result struct {
  Date  time.Time
  Total int64
}
db.Table("orders").Select("date(created_at) as date, sum(amount) as total").Group("date(created_at)").Having("sum(amount) > ?", 100).Scan(&results)

8.8 Distinct

  • Selecting distinct values from the model

  • 從模型中選擇不同的值

db.Distinct("name", "age").Order("name, age desc").Find(&results)
  • Distinct works with Pluck and Count too

8.9 Joins

  • Specify Joins conditions-指定連線條件
type result struct {
  Name  string
  Email string
}

db.Model(&User{}).Select("users.name, emails.email").Joins("left join emails on emails.user_id = users.id").Scan(&result{})
// SELECT users.name, emails.email FROM `users` left join emails on emails.user_id = users.id

rows, err := db.Table("users").Select("users.name, emails.email").Joins("left join emails on emails.user_id = users.id").Rows()
for rows.Next() {
  ...
}

db.Table("users").Select("users.name, emails.email").Joins("left join emails on emails.user_id = users.id").Scan(&results)

// multiple joins with parameter
db.Joins("JOIN emails ON emails.user_id = users.id AND emails.email = ?", "jinzhu@example.org").Joins("JOIN credit_cards ON credit_cards.user_id = users.id").Where("credit_cards.number = ?", "411111111111").Find(&user)

(1)Joins 預載入

  • You can use Joins eager loading associations with a single SQL, for example:

  • 可以使用'Joins'來載入與單個SQL的關聯

db.Joins("Company").Find(&users)
// SELECT `users`.`id`,`users`.`name`,`users`.`age`,`Company`.`id` AS `Company__id`,`Company`.`name` AS `Company__name` FROM `users` LEFT JOIN `companies` AS `Company` ON `users`.`company_id` = `Company`.`id`;
  • Join with conditions
db.Joins("Company", DB.Where(&Company{Alive: true})).Find(&users)
// SELECT `users`.`id`,`users`.`name`,`users`.`age`,`Company`.`id` AS `Company__id`,`Company`.`name` AS `Company__name` FROM `users` LEFT JOIN `companies` AS `Company` ON `users`.`company_id` = `Company`.`id` AND `Company`.`alive` = true;
  • For more details, please refer to Preloading (Eager Loading).

(2)Joins a Derived Table

  • You can also use Joins to join a derived table.
type User struct {
    Id  int
    Age int
}

type Order struct {
    UserId     int
    FinishedAt *time.Time
}

query := db.Table("order").Select("MAX(order.finished_at) as latest").Joins("left join user user on order.user_id = user.id").Where("user.age > ?", 18).Group("order.user_id")
db.Model(&Order{}).Joins("join (?) q on order.finished_at = q.latest", query).Scan(&results)
// SELECT `order`.`user_id`,`order`.`finished_at` FROM `order` join (SELECT MAX(order.finished_at) as latest FROM `order` left join user user on order.user_id = user.id WHERE user.age > 18 GROUP BY `order`.`user_id`) q on order.finished_at = q.latest

8.10 Scan

  • Scanning results into a struct works similarly to the way we use Find--

  • 將結果掃描到結構體中的工作方式,與“Find”類似

type Result struct {
  Name string
  Age  int
}

var result Result
db.Table("users").Select("name", "age").Where("name = ?", "Antonio").Scan(&result)

// Raw SQL
db.Raw("SELECT name, age FROM users WHERE name = ?", "Antonio").Scan(&result)

九、高階查詢(瞭解)

9.1 智慧選擇欄位

  • GORM 允許透過 Select 方法選擇特定的欄位,如果您在應用程式中經常使用此功能,你也可以定義一個較小的結構體,以實現呼叫 API 時自動選擇特定的欄位,例如:
type User struct {
  ID     uint
  Name   string
  Age    int
  Gender string
  // 假設後面還有幾百個欄位...
}

type APIUser struct {
  ID   uint
  Name string
}

// 查詢時會自動選擇 `id`, `name` 欄位
db.Model(&User{}).Limit(10).Find(&APIUser{})
// SELECT `id`, `name` FROM `users` LIMIT 10

注意 QueryFields 模式會根據當前 model 的所有欄位名稱進行 select。

db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{
  QueryFields: true,
})

db.Find(&user)
// SELECT `users`.`name`, `users`.`age`, ... FROM `users` // 帶上這個選項

// Session Mode
db.Session(&gorm.Session{QueryFields: true}).Find(&user)
// SELECT `users`.`name`, `users`.`age`, ... FROM `users`

9.2 Locking (FOR UPDATE)

  • GORM 支援多種型別的鎖,例如:
db.Clauses(clause.Locking{Strength: "UPDATE"}).Find(&users)
// SELECT * FROM `users` FOR UPDATE

db.Clauses(clause.Locking{
  Strength: "SHARE",
  Table: clause.Table{Name: clause.CurrentTable},
}).Find(&users)
// SELECT * FROM `users` FOR SHARE OF `users`

db.Clauses(clause.Locking{
  Strength: "UPDATE",
  Options: "NOWAIT",
}).Find(&users)
// SELECT * FROM `users` FOR UPDATE NOWAIT
  • 檢視 原生 SQL 及構造器 獲取詳情

9.3 子查詢

  • 子查詢可以巢狀在查詢中,GORM 允許在使用 *gorm.DB 物件作為引數時生成子查詢
db.Where("amount > (?)", db.Table("orders").Select("AVG(amount)")).Find(&orders)
// SELECT * FROM "orders" WHERE amount > (SELECT AVG(amount) FROM "orders");

subQuery := db.Select("AVG(age)").Where("name LIKE ?", "name%").Table("users")
db.Select("AVG(age) as avgage").Group("name").Having("AVG(age) > (?)", subQuery).Find(&results)
// SELECT AVG(age) as avgage FROM `users` GROUP BY `name` HAVING AVG(age) > (SELECT AVG(age) FROM `users` WHERE name LIKE "name%")

(1)From 子查詢

  • GORM 允許您在 Table 方法中透過 FROM 子句使用子查詢,例如:
db.Table("(?) as u", db.Model(&User{}).Select("name", "age")).Where("age = ?", 18}).Find(&User{})
// SELECT * FROM (SELECT `name`,`age` FROM `users`) as u WHERE `age` = 18

subQuery1 := db.Model(&User{}).Select("name")
subQuery2 := db.Model(&Pet{}).Select("name")
db.Table("(?) as u, (?) as p", subQuery1, subQuery2).Find(&User{})
// SELECT * FROM (SELECT `name` FROM `users`) as u, (SELECT `name` FROM `pets`) as p

9.4 Group 條件

  • 使用 Group 條件可以更輕鬆的編寫複雜 SQL
db.Where(
    db.Where("pizza = ?", "pepperoni").Where(db.Where("size = ?", "small").Or("size = ?", "medium")),
).Or(
    db.Where("pizza = ?", "hawaiian").Where("size = ?", "xlarge"),
).Find(&Pizza{}).Statement

// SELECT * FROM `pizzas` WHERE (pizza = "pepperoni" AND (size = "small" OR size = "medium")) OR (pizza = "hawaiian" AND size = "xlarge")

9.5 帶多個列的 In

  • 帶多個列的 In 查詢
db.Where("(name, age, role) IN ?", [][]interface{}{{"jinzhu", 18, "admin"}, {"jinzhu2", 19, "user"}}).Find(&users)
// SELECT * FROM users WHERE (name, age, role) IN (("jinzhu", 18, "admin"), ("jinzhu 2", 19, "user"));

9.6 命名引數

  • GORM 支援 sql.NamedArgmap[string]interface{}{} 形式的命名引數,例如:
db.Where("name1 = @name OR name2 = @name", sql.Named("name", "jinzhu")).Find(&user)
// SELECT * FROM `users` WHERE name1 = "jinzhu" OR name2 = "jinzhu"

db.Where("name1 = @name OR name2 = @name", map[string]interface{}{"name": "jinzhu"}).First(&user)
// SELECT * FROM `users` WHERE name1 = "jinzhu" OR name2 = "jinzhu" ORDER BY `users`.`id` LIMIT 1
  • 檢視 原生 SQL 及構造器 獲取詳情

9.7 Find 至 map

  • GORM 允許掃描結果至 map[string]interface{}[]map[string]interface{},此時別忘了指定 ModelTable,例如:
result := map[string]interface{}{}
db.Model(&User{}).First(&result, "id = ?", 1)

var results []map[string]interface{}
db.Table("users").Find(&results)

9.8 FirstOrInit

  • 獲取第一條匹配的記錄,或者根據給定的條件初始化一個例項(僅支援 sturct 和 map 條件)
// 未找到 user,則根據給定的條件初始化一條記錄
db.FirstOrInit(&user, User{Name: "non_existing"})
// user -> User{Name: "non_existing"}

// 找到了 `name` = `jinzhu` 的 user
db.Where(User{Name: "jinzhu"}).FirstOrInit(&user)
// user -> User{ID: 111, Name: "Jinzhu", Age: 18}

// 找到了 `name` = `jinzhu` 的 user
db.FirstOrInit(&user, map[string]interface{}{"name": "jinzhu"})
// user -> User{ID: 111, Name: "Jinzhu", Age: 18}
  • 如果沒有找到記錄,可以使用包含更多的屬性的結構體初始化 user,Attrs 不會被用於生成查詢 SQL
// 未找到 user,則根據給定的條件以及 Attrs 初始化 user
db.Where(User{Name: "non_existing"}).Attrs(User{Age: 20}).FirstOrInit(&user)
// SELECT * FROM USERS WHERE name = 'non_existing' ORDER BY id LIMIT 1;
// user -> User{Name: "non_existing", Age: 20}

// 未找到 user,則根據給定的條件以及 Attrs 初始化 user
db.Where(User{Name: "non_existing"}).Attrs("age", 20).FirstOrInit(&user)
// SELECT * FROM USERS WHERE name = 'non_existing' ORDER BY id LIMIT 1;
// user -> User{Name: "non_existing", Age: 20}

// 找到了 `name` = `jinzhu` 的 user,則忽略 Attrs
db.Where(User{Name: "Jinzhu"}).Attrs(User{Age: 20}).FirstOrInit(&user)
// SELECT * FROM USERS WHERE name = jinzhu' ORDER BY id LIMIT 1;
// user -> User{ID: 111, Name: "Jinzhu", Age: 18}
  • 不管是否找到記錄,Assign 都會將屬性賦值給 struct,但這些屬性不會被用於生成查詢 SQL,也不會被儲存到資料庫
// 未找到 user,根據條件和 Assign 屬性初始化 struct
db.Where(User{Name: "non_existing"}).Assign(User{Age: 20}).FirstOrInit(&user)
// user -> User{Name: "non_existing", Age: 20}

// 找到 `name` = `jinzhu` 的記錄,依然會更新 Assign 相關的屬性
db.Where(User{Name: "Jinzhu"}).Assign(User{Age: 20}).FirstOrInit(&user)
// SELECT * FROM USERS WHERE name = jinzhu' ORDER BY id LIMIT 1;
// user -> User{ID: 111, Name: "Jinzhu", Age: 20}

9.9 FirstOrCreate

  • Get first matched record or create a new one with given conditions (only works with struct, map conditions), RowsAffected returns created/updated record’s count
// User not found, create a new record with give conditions
result := db.FirstOrCreate(&user, User{Name: "non_existing"})
// INSERT INTO "users" (name) VALUES ("non_existing");
// user -> User{ID: 112, Name: "non_existing"}
// result.RowsAffected // => 0

// Found user with `name` = `jinzhu`
result := db.Where(User{Name: "jinzhu"}).FirstOrCreate(&user)
// user -> User{ID: 111, Name: "jinzhu", "Age": 18}
// result.RowsAffected // => 0
  • 如果沒有找到記錄,可以使用包含更多的屬性的結構體建立記錄,Attrs 不會被用於生成查詢 SQL 。
// 未找到 user,根據條件和 Assign 屬性建立記錄
db.Where(User{Name: "non_existing"}).Attrs(User{Age: 20}).FirstOrCreate(&user)
// SELECT * FROM users WHERE name = 'non_existing' ORDER BY id LIMIT 1;
// INSERT INTO "users" (name, age) VALUES ("non_existing", 20);
// user -> User{ID: 112, Name: "non_existing", Age: 20}

// 找到了 `name` = `jinzhu` 的 user,則忽略 Attrs
db.Where(User{Name: "jinzhu"}).Attrs(User{Age: 20}).FirstOrCreate(&user)
// SELECT * FROM users WHERE name = 'jinzhu' ORDER BY id LIMIT 1;
// user -> User{ID: 111, Name: "jinzhu", Age: 18}
  • 不管是否找到記錄,Assign 都會將屬性賦值給 struct,並將結果寫回資料庫
// 未找到 user,根據條件和 Assign 屬性建立記錄
db.Where(User{Name: "non_existing"}).Assign(User{Age: 20}).FirstOrCreate(&user)
// SELECT * FROM users WHERE name = 'non_existing' ORDER BY id LIMIT 1;
// INSERT INTO "users" (name, age) VALUES ("non_existing", 20);
// user -> User{ID: 112, Name: "non_existing", Age: 20}

// 找到了 `name` = `jinzhu` 的 user,依然會根據 Assign 更新記錄
db.Where(User{Name: "jinzhu"}).Assign(User{Age: 20}).FirstOrCreate(&user)
// SELECT * FROM users WHERE name = 'jinzhu' ORDER BY id LIMIT 1;
// UPDATE users SET age=20 WHERE id = 111;
// user -> User{ID: 111, Name: "jinzhu", Age: 20}

9.10 最佳化器、索引提示

  • 最佳化器提示用於控制查詢最佳化器選擇某個查詢執行計劃,GORM 透過 gorm.io/hints 提供支援,例如:
import "gorm.io/hints"

db.Clauses(hints.New("MAX_EXECUTION_TIME(10000)")).Find(&User{})
// SELECT * /*+ MAX_EXECUTION_TIME(10000) */ FROM `users`
  • 索引提示允許傳遞索引提示到資料庫,以防查詢計劃器出現混亂。
import "gorm.io/hints"

db.Clauses(hints.UseIndex("idx_user_name")).Find(&User{})
// SELECT * FROM `users` USE INDEX (`idx_user_name`)

db.Clauses(hints.ForceIndex("idx_user_name", "idx_user_id").ForJoin()).Find(&User{})
// SELECT * FROM `users` FORCE INDEX FOR JOIN (`idx_user_name`,`idx_user_id`)"
  • 參考 最佳化器提示、索引、備註 獲取詳情

9.11 迭代

  • GORM 支援透過行進行迭代
rows, err := db.Model(&User{}).Where("name = ?", "jinzhu").Rows()
defer rows.Close()

for rows.Next() {
  var user User
  // ScanRows 方法用於將一行記錄掃描至結構體
  db.ScanRows(rows, &user)

  // 業務邏輯...
}

9.12 FindInBatches

  • 用於批次查詢並處理記錄
// 每次批次處理 100 條
result := db.Where("processed = ?", false).FindInBatches(&results, 100, func(tx *gorm.DB, batch int) error {
  for _, result := range results {
    // 批次處理找到的記錄
  }

  tx.Save(&results)

  tx.RowsAffected // 本次批次操作影響的記錄數

  batch // Batch 1, 2, 3

  // 如果返回錯誤會終止後續批次操作
  return nil
})

result.Error // returned error
result.RowsAffected // 整個批次操作影響的記錄數

9.13 查詢鉤子

  • 對於查詢操作,GORM 支援 AfterFind 鉤子,查詢記錄後會呼叫它,詳情請參考 鉤子
func (u *User) AfterFind(tx *gorm.DB) (err error) {
  if u.Role == "" {
    u.Role = "user"
  }
  return
}

9.14 Pluck

  • Pluck 用於從資料庫查詢單個列,並將結果掃描到切片。如果您想要查詢多列,您應該使用 SelectScan
var ages []int64
db.Model(&users).Pluck("age", &ages)

var names []string
db.Model(&User{}).Pluck("name", &names)

db.Table("deleted_users").Pluck("name", &names)

// Distinct Pluck
db.Model(&User{}).Distinct().Pluck("Name", &names)
// SELECT DISTINCT `name` FROM `users`

// 超過一列的查詢,應該使用 `Scan` 或者 `Find`,例如:
db.Select("name", "age").Scan(&users)
db.Select("name", "age").Find(&users)

9.15 Scope

  • Scopes 允許你指定常用的查詢,可以在呼叫方法時引用這些查詢
func AmountGreaterThan1000(db *gorm.DB) *gorm.DB {
  return db.Where("amount > ?", 1000)
}

func PaidWithCreditCard(db *gorm.DB) *gorm.DB {
  return db.Where("pay_mode_sign = ?", "C")
}

func PaidWithCod(db *gorm.DB) *gorm.DB {
  return db.Where("pay_mode_sign = ?", "C")
}

func OrderStatus(status []string) func (db *gorm.DB) *gorm.DB {
  return func (db *gorm.DB) *gorm.DB {
    return db.Where("status IN (?)", status)
  }
}

db.Scopes(AmountGreaterThan1000, PaidWithCreditCard).Find(&orders)
// 查詢所有金額大於 1000 的信用卡訂單

db.Scopes(AmountGreaterThan1000, PaidWithCod).Find(&orders)
// 查詢所有金額大於 1000 的貨到付款訂單

db.Scopes(AmountGreaterThan1000, OrderStatus([]string{"paid", "shipped"})).Find(&orders)
// 查詢所有金額大於 1000 且已付款或已發貨的訂單
  • 檢視 Scopes 獲取詳情

9.16 Count

  • Count 用於獲取匹配的記錄數
var count int64
db.Model(&User{}).Where("name = ?", "jinzhu").Or("name = ?", "jinzhu 2").Count(&count)
// SELECT count(1) FROM users WHERE name = 'jinzhu' OR name = 'jinzhu 2'

db.Model(&User{}).Where("name = ?", "jinzhu").Count(&count)
// SELECT count(1) FROM users WHERE name = 'jinzhu'; (count)

db.Table("deleted_users").Count(&count)
// SELECT count(1) FROM deleted_users;

// Count with Distinct
db.Model(&User{}).Distinct("name").Count(&count)
// SELECT COUNT(DISTINCT(`name`)) FROM `users`

db.Table("deleted_users").Select("count(distinct(name))").Count(&count)
// SELECT count(distinct(name)) FROM deleted_users

// Count with Group
users := []User{
  {Name: "name1"},
  {Name: "name2"},
  {Name: "name3"},
  {Name: "name3"},
}

db.Model(&User{}).Group("name").Count(&count)
count // => 3

十、更新

// 表模型變成
type User struct {
	gorm.Model
	Name         string
	Age          uint8
	Birthday     time.Time
	Active       bool
}
// 刪除表,重新遷移
db.AutoMigrate(&User{})
// 插入記錄
	var user =User{Name: "lqz",Age: 18,Birthday: time.Now(),Active: true}
	db.Create(&user)

10.1 儲存所有欄位

  • Save 會儲存所有的欄位,即使欄位是零值
var user User
db.First(&user)

user.Name = "lqz"
user.Age = 100
db.Save(&user)
// UPDATE users SET name='jinzhu 2', age=100, birthday='2016-01-01', updated_at = '2013-11-17 21:34:10' WHERE id=111;

10.2 更新單個列

  • 當使用 Update 更新單個列時,你需要指定條件,否則會返回 ErrMissingWhereClause 錯誤,檢視 Block Global Updates 獲取詳情。當使用了 Model 方法,且該物件主鍵有值,該值會被用於構建條件,例如:
// 條件更新(如果有多條,全更新)
db.Model(&User{}).Where("active = ?", true).Update("name", "hello")
// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE active=true;

// User 的 ID 是 `1`
var user User
db.First(&user)
db.Model(&user).Update("name", "lqz_nb")
// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE id=111;

// 根據條件和 model 的值進行更新
var user User
db.First(&user)
db.Model(&user).Where("active = ?", true).Update("name", "lqz_hello")
// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE id=111 AND active=true;

10.3 更新多列

  • Updates 方法支援 structmap[string]interface{} 引數。當使用 struct 更新時,預設情況下,GORM 只會更新非零值的欄位
// 根據 `struct` 更新屬性,只會更新非零值的欄位
	var user User
	db.First(&user)
	db.Model(&user).Updates(User{Name: "hello", Age: 99, Active: false})
// UPDATE users SET name='hello', age=18, updated_at = '2013-11-17 21:34:10' WHERE id = 111;

// 根據 `map` 更新屬性
var user User
db.First(&user)
db.Model(&user).Updates(map[string]interface{}{"name": "lqz_01", "age": 88, "active": false})
// UPDATE users SET name='hello', age=18, active=false, updated_at='2013-11-17 21:34:10' WHERE id=111;

注意 當透過 struct 更新時,GORM 只會更新非零欄位。 如果您想確保指定欄位被更新,你應該使用 Select 更新選定欄位,或使用 map 來完成更新操作

10.4 更新選定欄位

  • 如果您想要在更新時選定、忽略某些欄位,您可以使用 SelectOmit
// 使用 Map 進行 Select
// User's ID is `111`:
var user User
db.First(&user)
// 只更新name
db.Model(&user).Select("name").Updates(map[string]interface{}{"name": "lqz_002", "age": 66, "active": false})
// UPDATE users SET name='hello' WHERE id=111;

var user User
db.First(&user)
// 除了name都更新
db.Model(&user).Omit("name").Updates(map[string]interface{}{"name": "hello", "age": 18, "active": true})
}
// UPDATE users SET age=18, active=false, updated_at='2013-11-17 21:34:10' WHERE id=111;

// 使用 Struct 進行 Select(會 select 零值的欄位),零值欄位也會更新
// 注意 user的id不能為0
var user User
db.First(&user)
db.Model(&user).Select("Name", "Age").Updates(User{Name: "new_name", Age: 0})
// UPDATE users SET name='new_name', age=0 WHERE id=111;

// Select 所有欄位(查詢包括零值欄位的所有欄位)
var user User
db.First(&user)
fmt.Println(user)
db.Model(&user).Select("*").Updates(User{Name: "lqz_004", Birthday:time.Now(), Age: 33,Active: false, Model:gorm.Model{ID:3,CreatedAt: time.Now(),UpdatedAt: time.Now()}})



// Select 除 "CreatedAt","birthday"外的所有欄位(包括零值欄位的所有欄位),注意id會變成0
db.Model(&user).Select("*").Omit("CreatedAt","birthday").Updates(User{Name: "jinzhu", Age: 0})

10.5 更新 Hook

  • 對於更新操作,GORM 支援 BeforeSaveBeforeUpdateAfterSaveAfterUpdate 鉤子,這些方法將在更新記錄時被呼叫,詳情請參閱 鉤子
func (u *User) BeforeUpdate(tx *gorm.DB) (err error) {
    if u.Role == "admin" {
        return errors.New("admin user not allowed to update")
    }
    return
}
// 測試
func (u *User) BeforeUpdate(tx *gorm.DB) (err error) {
	fmt.Println("修改了")
	return
}

10.6 批次更新

  • 如果您尚未透過 Model 指定記錄的主鍵,則 GORM 會執行批次更新
// 根據 struct 更新--->批次更新不能用Hook
db.Model(User{}).Where("active = ?", true).Updates(User{Name: "hello", Age: 18})
//  UPDATE `users` SET `updated_at`='2022-05-05 01:01:53.001',`name`='hello',`age`=18 WHERE active = true AND `users`.`deleted_at` IS NULL


// 根據 map 更新
db.Table("users").Where("id IN ?", []int{1, 2}).Updates(map[string]interface{}{"name": "lqz_009", "age": 99})

// UPDATE `users` SET `age`=99,`name`='lqz_009' WHERE id IN (1,2)

(1)阻止全域性更新

  • 如果在沒有任何條件的情況下執行批次更新,預設情況下,GORM 不會執行該操作,並返回 ErrMissingWhereClause 錯誤

  • 對此,你必須加一些條件,或者使用原生 SQL,或者啟用 AllowGlobalUpdate 模式,例如:

db.Model(&User{}).Update("name", "jinzhu").Error //報錯 gorm.ErrMissingWhereClause

db.Model(&User{}).Where("1 = 1").Update("name", "jinzhu") //可以
// UPDATE users SET `name` = "jinzhu" WHERE 1=1


// 方式一:直接執行原生sql可以全域性更新
db.Exec("UPDATE users SET name = ?", "jinzhu")
// UPDATE users SET name = "jinzhu"

// 方式二:使用session
db.Session(&gorm.Session{AllowGlobalUpdate: true}).Model(&User{}).Update("name", "lqz_010")
// UPDATE users SET `name` = "lqz_010"

(2)更新的記錄數

  • 獲取受更新影響的行數
// 透過 `RowsAffected` 得到更新的記錄數
result := db.Model(User{}).Where("active = ?", true).Updates(User{Name: "hello", Age: 18})
fmt.Println(result.RowsAffected) // 更新的記錄數
fmt.Println(result.Error) // 錯誤

10.7 高階選項(瞭解)

(1)使用 SQL 表示式更新

  • GORM 允許使用 SQL 表示式更新列,例如:
// user 的 ID 是 `1`
var user User
db.First(&user)
db.Model(&user).Update("age", gorm.Expr("age * ? + ?", 2, 100))
//  UPDATE `users` SET `age`=age * 2 + 100,`updated_at`='2022-05-05 01:11:16.242' WHERE `users`.`deleted_at` IS NULL AND `id` = 1



var user User
db.First(&user)
db.Model(&user).Updates(map[string]interface{}{"age": gorm.Expr("age - ? + ?", 2, 100)})
//  UPDATE `users` SET `age`=age - 2 + 100,`updated_at`='2022-05-05 01:13:13.302' WHERE `users`.`deleted_at` IS NULL AND `id` = 1

db.First(&user)
db.Model(&user).UpdateColumn("age", gorm.Expr("age - ?", 10))
// UPDATE `users` SET `age`=age - 10 WHERE `users`.`deleted_at` IS NULL AND `id` = 1

var user User
db.First(&user)
db.Model(&user).Where("age > 100").UpdateColumn("age", gorm.Expr("age - ?", 100))
// UPDATE `users` SET `age`=age - 100 WHERE age > 100 AND `users`.`deleted_at` IS NULL AND `id` = 1
  • 並且 GORM 也允許使用 SQL 表示式、自定義資料型別的 Context Valuer 來更新,例如:
// 根據自定義資料型別建立
type Location struct {
    X, Y int
}

func (loc Location) GormValue(ctx context.Context, db *gorm.DB) clause.Expr {
  return clause.Expr{
    SQL:  "ST_PointFromText(?)",
    Vars: []interface{}{fmt.Sprintf("POINT(%d %d)", loc.X, loc.Y)},
  }
}

db.Model(&User{ID: 1}).Updates(User{
  Name:  "jinzhu",
  Location: Location{X: 100, Y: 100},
})
// UPDATE `user_with_points` SET `name`="jinzhu",`location`=ST_PointFromText("POINT(100 100)") WHERE `id` = 1

(2)根據子查詢進行更新

  • 使用子查詢更新表
db.Model(&user).Update("company_name", db.Model(&Company{}).Select("name").Where("companies.id = users.company_id"))
// UPDATE "users" SET "company_name" = (SELECT name FROM companies WHERE companies.id = users.company_id);

db.Table("users as u").Where("name = ?", "jinzhu").Update("company_name", db.Table("companies as c").Select("name").Where("c.id = u.company_id"))
//UPDATE users as u SET `company_name`=(SELECT name FROM companies as c WHERE c.id = u.company_id) WHERE name = 'jinzhu'



db.Table("users as u").Where("name = ?", "jinzhu").Updates(map[string]interface{}{"company_name": db.Table("companies as c").Select("name").Where("c.id = u.company_id")})
//UPDATE users as u SET `company_name`=(SELECT name FROM companies as c WHERE c.id = u.company_id) WHERE name = 'jinzhu'

(3)不使用 Hook 和時間追蹤

  • 如果您想在更新時跳過 Hook 方法且不追蹤更新時間,可以使用 UpdateColumnUpdateColumns,其用法類似於 UpdateUpdates
// 更新單個列
db.Model(&user).UpdateColumn("name", "hello")
// UPDATE users SET name='hello' WHERE id = 111;

// 更新多個列
db.Model(&user).UpdateColumns(User{Name: "hello", Age: 18})
// UPDATE users SET name='hello', age=18 WHERE id = 111;

// 更新選中的列
db.Model(&user).Select("name", "age").UpdateColumns(User{Name: "hello", Age: 0})
// UPDATE users SET name='hello', age=0 WHERE id = 111;

(4)返回修改行的資料(瞭解->mysql不支援)

  • 返回被修改的資料,僅適用於支援 Returning 的資料庫,例如:
// 返回所有列
var users []User
DB.Model(&users).Clauses(clause.Returning{}).Where("role = ?", "admin").Update("salary", gorm.Expr("salary * ?", 2))
// UPDATE `users` SET `salary`=salary * 2,`updated_at`="2021-10-28 17:37:23.19" WHERE role = "admin" RETURNING *
// users => []User{{ID: 1, Name: "jinzhu", Role: "admin", Salary: 100}, {ID: 2, Name: "jinzhu.2", Role: "admin", Salary: 1000}}

// 返回指定的列
DB.Model(&users).Clauses(clause.Returning{Columns: []clause.Column{{Name: "name"}, {Name: "salary"}}}).Where("role = ?", "admin").Update("salary", gorm.Expr("salary * ?", 2))
// UPDATE `users` SET `salary`=salary * 2,`updated_at`="2021-10-28 17:37:23.19" WHERE role = "admin" RETURNING `name`, `salary`
// users => []User{{ID: 0, Name: "jinzhu", Role: "", Salary: 100}, {ID: 0, Name: "jinzhu.2", Role: "", Salary: 1000}}

(5)檢查欄位是否有變更?

  • GORM 提供了 Changed 方法,它可以被用在 Before Update Hook 裡,它會返回欄位是否有變更的布林值

  • Changed 方法只能與 UpdateUpdates 方法一起使用,並且它只是檢查 Model 物件欄位的值與 UpdateUpdates 的值是否相等,如果值有變更,且欄位沒有被忽略,則返回 true

func (u *User) BeforeUpdate(tx *gorm.DB) (err error) {
	// 如果 age 欄位有變更,不允許,直接報錯
	if tx.Statement.Changed("age") {
		fmt.Println("xx")
		return errors.New("age not allowed to change")
	}
	// 如果 Name 欄位有變更,把年齡改為18
	if tx.Statement.Changed("Name") {
		tx.Statement.SetColumn("age", 18)
	}

	// 如果任意欄位有變更,修改CreatedAt時間
	if tx.Statement.Changed() {
		tx.Statement.SetColumn("CreatedAt", time.Now())

	}
	return nil
}



//修改名字,把age 設為18
db.Model(&User{Model:gorm.Model{ID: 1} , Name: "lqz"}).Updates(map[string]interface{}{"name": "lqz_nb"})

// 名字修改,但是名字其實並沒有變,所以年齡不會被修改
db.Model(&User{Model:gorm.Model{ID: 1}, Name: "lqz_nb"}).Updates(map[string]interface{}{"name": "lqz_nb"})

//修改名字,把age 設為18
db.Model(&User{Model:gorm.Model{ID: 1}, Name: "lqz_nb"}).Updates(User{Name: "jinzhu2"})

//名字修改,但是名字其實並沒有變,所以年齡不會被修改
db.Model(&User{Model:gorm.Model{ID: 1}, Name: "jinzhu2"}).Updates(User{Name: "jinzhu2"})

// 不允許修改age
db.Model(&User{}).Where("id=?",1).Updates(map[string]interface{}{"age": 100})

//任意欄位有變更,更新CreatedAt時間
db.Model(&User{}).Where("id=?",1).Updates(map[string]interface{}{"active": false})

(6)在 Update 時修改值

  • 若要在 Before 鉤子中改變要更新的值,如果它是一個完整的更新,可以使用 Save;否則,應該使用 SetColumn ,例如:
func (user *User) BeforeSave(tx *gorm.DB) (err error) {
  if pw, err := bcrypt.GenerateFromPassword(user.Password, 0); err == nil {
    tx.Statement.SetColumn("EncryptedPassword", pw)
  }

  if tx.Statement.Changed("Code") {
    user.Age += 20
    tx.Statement.SetColumn("Age", user.Age)
  }
}

db.Model(&user).Update("Name", "jinzhu")
var user User
db.First(&user)
db.Model(&user).Update("Name", "lqz008")

// 只要更新名字,就把名字加密
func (user *User) BeforeSave(tx *gorm.DB) (err error) {
	if genName, err := bcrypt.GenerateFromPassword([]byte(user.Name), 0); err == nil {
		tx.Statement.SetColumn("name", genName)
	}
	return nil
}

十一、刪除

11.1 刪除一條記錄

  • 刪除一條記錄時,刪除物件需要指定主鍵,否則會觸發 批次 Delete,例如:
// user 的 ID 是 `1`--->軟刪除
var user User
db.First(&user)
db.Delete(&user)

// UPDATE `users` SET `deleted_at`='2022-05-05 02:05:16.7' WHERE `users`.`id` = 1 AND `users`.`deleted_at` IS NULL


// 帶額外條件的刪除
var user User
db.First(&user)
db.Where("name = ?", "hello").Delete(&user)

//db.Where("name = ?", "hello").Delete(&User{})
// UPDATE `users` SET `deleted_at`='2022-05-05 02:08:30.857' WHERE name = 'hello' AND `users`.`deleted_at` IS NULL

11.2 根據主鍵刪除

  • GORM 允許透過主鍵(可以是複合主鍵)和內聯條件來刪除物件,它可以使用數字(如以下例子。也可以使用字串——譯者注)。檢視 查詢-內聯條件(Query Inline Conditions) 瞭解詳情。
// 先插入
var user =User{Name: "lqz001",Age: 99,Birthday: time.Now()}
db.Create(&user)

db.Delete(&User{}, 3)


db.Delete(&User{}, "10")

b.Delete(&User{}, []int{1,2,3})

11.3 Delete Hook

  • 對於刪除操作,GORM 支援 BeforeDeleteAfterDelete Hook,在刪除記錄時會呼叫這些方法,檢視 Hook 獲取詳情
func (u *User) BeforeDelete(tx *gorm.DB) (err error) {
    if u.Role == "admin" {
        return errors.New("admin user not allowed to delete")
    }
    return
}

11.4 批次刪除

  • 如果指定的值不包括主屬性,那麼 GORM 會執行批次刪除,它將刪除所有匹配的記錄
db.Where("name LIKE ?", "%lqz%").Delete(&User{})

db.Delete(&User{}, "name LIKE ?", "%lqz%")

(1)阻止全域性刪除

  • 如果在沒有任何條件的情況下執行批次刪除,GORM 不會執行該操作,並返回 ErrMissingWhereClause錯誤

  • 對此,你必須加一些條件,或者使用原生 SQL,或者啟用 AllowGlobalUpdate 模式,例如:

db.Delete(&User{}).Error // gorm.ErrMissingWhereClause

db.Where("1 = 1").Delete(&User{})
// DELETE FROM `users` WHERE 1=1

db.Exec("DELETE FROM users")
// DELETE FROM users

db.Session(&gorm.Session{AllowGlobalUpdate: true}).Delete(&User{})
// DELETE FROM users

(2)返回刪除行的資料(瞭解)

  • 返回被刪除的資料,僅適用於支援 Returning 的資料庫,例如:
// 返回所有列
var users []User
DB.Clauses(clause.Returning{}).Where("role = ?", "admin").Delete(&users)
// DELETE FROM `users` WHERE role = "admin" RETURNING *
// users => []User{{ID: 1, Name: "jinzhu", Role: "admin", Salary: 100}, {ID: 2, Name: "jinzhu.2", Role: "admin", Salary: 1000}}

// 返回指定的列
DB.Clauses(clause.Returning{Columns: []clause.Column{{Name: "name"}, {Name: "salary"}}}).Where("role = ?", "admin").Delete(&users)
// DELETE FROM `users` WHERE role = "admin" RETURNING `name`, `salary`
// users => []User{{ID: 0, Name: "jinzhu", Role: "", Salary: 100}, {ID: 0, Name: "jinzhu.2", Role: "", Salary: 1000}}

11.5 軟刪除

  • 如果您的模型包含了一個 gorm.deletedat 欄位(gorm.Model 已經包含了該欄位),它將自動獲得軟刪除的能力!

  • 擁有軟刪除能力的模型呼叫 Delete 時,記錄不會被資料庫。但 GORM 會將 DeletedAt 置為當前時間, 並且你不能再透過普通的查詢方法找到該記錄。

// user 的 ID 是 `111`
db.Delete(&user)
// UPDATE users SET deleted_at="2013-10-29 10:23" WHERE id = 111;

// 批次刪除
db.Where("age = ?", 20).Delete(&User{})
// UPDATE users SET deleted_at="2013-10-29 10:23" WHERE age = 20;

// 在查詢時會忽略被軟刪除的記錄
db.Where("age = 20").Find(&user)
// SELECT * FROM users WHERE age = 20 AND deleted_at IS NULL;
  • 如果您不想引入 gorm.Model,您也可以這樣啟用軟刪除特性:
type User struct {
  ID      int
  Deleted gorm.DeletedAt
  Name    string
}

(1)查詢被軟刪除的記錄

  • 您可以使用 Unscoped 找到被軟刪除的記錄
var users []User
db.Unscoped().Where("age = 99").Find(&users)
fmt.Println(users)

(2)永久刪除

  • 您也可以使用 Unscoped 永久刪除匹配的記錄
var users []User
db.Unscoped().Where("age = 99").Find(&users)
db.Unscoped().Delete(&users)

(3)Delete Flag

  • 將 unix 時間戳作為 delete flag
// go get -u gorm.io/plugin/soft_delete
//db.AutoMigrate(&User2{})
import "gorm.io/plugin/soft_delete"

type User2 struct {
  ID        uint
  Name      string
  DeletedAt soft_delete.DeletedAt
}

// 建立
var user2 User2=User2{Name: "lqz"}
db.Create(&user2)
// 刪除
db.Delete(&user2)

// 查詢
SELECT * FROM users WHERE deleted_at = 0;

// 刪除
UPDATE users SET deleted_at = /* current unix second */ WHERE ID = 1;

INFO 在配合 unique 欄位使用軟刪除時,您需要使用這個基於 unix 時間戳的 DeletedAt 欄位建立一個複合索引,例如:

import "gorm.io/plugin/soft_delete"

type User struct {
ID        uint
Name      string                `gorm:"uniqueIndex:udx_name"`
DeletedAt soft_delete.DeletedAt `gorm:"uniqueIndex:udx_name"`
}
  • 使用 1 / 0 作為 delete flag
import "gorm.io/plugin/soft_delete"

type User struct {
  ID    uint
  Name  string
  IsDel soft_delete.DeletedAt `gorm:"softDelete:flag"`
}

// 查詢
SELECT * FROM users WHERE is_del = 0;

// 刪除
UPDATE users SET is_del = 1 WHERE ID = 1;

十二、SQL 構建器

12.1 原生 SQL

  • 原生查詢 SQL 和 Scan
type User3 struct {
	ID   int
	Name string
	Age  int
}

// 建立表
db.AutoMigrate(&User3{})
// 插入資料

var user User3
db.Raw("SELECT id, name, age FROM user3 WHERE name = ?", "lqz").Scan(&user)
fmt.Println(user)

var age int
db.Raw("SELECT SUM(age) FROM user3 WHERE name like ?", "%lqz%").Scan(&age)
fmt.Println(age)

var users []User3
db.Raw("SELECT * FROM user3 WHERE id > ?", 0).Scan(&users)
fmt.Println(users)


// mysql 不支援
var users []User3
db.Raw("UPDATE user3 SET name = ? WHERE age = ? RETURNING id, name", "lqz", 12).Scan(&users)
fmt.Println(users)
  • Exec 原生 SQL
db.Exec("DROP TABLE users")
db.Exec("UPDATE orders SET shipped_at = ? WHERE id IN ?", time.Now(), []int64{1, 2, 3})

// Exec with SQL Expression
db.Exec("UPDATE users SET money = ? WHERE name = ?", gorm.Expr("money * ? + ?", 10000, 1), "jinzhu")

注意 GORM 允許快取預編譯 SQL 語句來提高效能,檢視 效能 獲取詳情

12.2 命名引數

  • GORM 支援 sql.NamedArgmap[string]interface{}{} 或 struct 形式的命名引數,例如:
// 測試
var user []User3
db.Where("name = @name OR age = @age", sql.Named("name", "lqz"),sql.Named("age", "12")).Find(&user)
fmt.Println(user)
	
var users []User3
db.Where("name = @name OR age = @age", sql.Named("name", "lqz"),sql.Named("age", "12")).Find(&users)
fmt.Println(users)

// 其他一樣
db.Where("name1 = @name OR name2 = @name", map[string]interface{}{"name": "jinzhu2"}).First(&result3)
// SELECT * FROM `users` WHERE name1 = "jinzhu2" OR name2 = "jinzhu2" ORDER BY `users`.`id` LIMIT 1

// 原生 SQL 及命名引數
db.Raw("SELECT * FROM users WHERE name1 = @name OR name2 = @name2 OR name3 = @name",
   sql.Named("name", "jinzhu1"), sql.Named("name2", "jinzhu2")).Find(&user)
// SELECT * FROM users WHERE name1 = "jinzhu1" OR name2 = "jinzhu2" OR name3 = "jinzhu1"

db.Exec("UPDATE users SET name1 = @name, name2 = @name2, name3 = @name",
   sql.Named("name", "jinzhunew"), sql.Named("name2", "jinzhunew2"))
// UPDATE users SET name1 = "jinzhunew", name2 = "jinzhunew2", name3 = "jinzhunew"

db.Raw("SELECT * FROM users WHERE (name1 = @name AND name3 = @name) AND name2 = @name2",
   map[string]interface{}{"name": "jinzhu", "name2": "jinzhu2"}).Find(&user)
// SELECT * FROM users WHERE (name1 = "jinzhu" AND name3 = "jinzhu") AND name2 = "jinzhu2"

type NamedArgument struct {
    Name string
    Name2 string
}

db.Raw("SELECT * FROM users WHERE (name1 = @Name AND name3 = @Name) AND name2 = @Name2",
     NamedArgument{Name: "jinzhu", Name2: "jinzhu2"}).Find(&user)
// SELECT * FROM users WHERE (name1 = "jinzhu" AND name3 = "jinzhu") AND name2 = "jinzhu2"

12.3 DryRun 模式

  • 在不執行的情況下生成 SQL 及其引數,可以用於準備或測試生成的 SQL,詳情請參考 Session
var user User3
//session := db.Session(&gorm.Session{DryRun: true})
//session.First(&user, 1)
//fmt.Println(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}

12.4 ToSQL

  • 返回生成的 SQL 但不執行。

  • GORM使用 database/sql 的引數佔位符來構建 SQL 語句,它會自動轉義引數以避免 SQL 注入,但我們不保證生成 SQL 的安全,請只用於除錯。

var users []User3
sql := db.ToSQL(func(tx *gorm.DB) *gorm.DB {
		return tx.Model(&User3{}).Where("id = ?", 100).Limit(10).Order("age desc").Find(&users)
	})
fmt.Println(sql)
//SELECT * FROM `user3` WHERE id = 100 ORDER BY age desc LIMIT 10

12.5 Row & Rows

  • 獲取 *sql.Row 結果
// 使用 GORM API 構建 SQL
var name string
var age  int
row := db.Table("user3").Where("name = ?", "lqz").Select("name", "age").Row()
row.Scan(&name, &age)
fmt.Println(name)
fmt.Println(age)

// 使用原生 SQL
var name string
var age  int
row := db.Raw("select name, age from user3 where name = ?", "lqz").Row()
row.Scan(&name, &age)
fmt.Println(name)
fmt.Println(age)
  • 獲取 *sql.Rows 結果
// 使用 GORM API 構建 SQL
var name string
var age int
rows, _ := db.Model(&User3{}).Where("name like ?", "%lqz%").Select("name, age").Rows()
defer rows.Close()
for rows.Next() {
		rows.Scan(&name, &age)
		fmt.Printf("name是:%s,age是:%d\n",name,age)
}

// 原生 SQL
var name string
var age int
rows, err := db.Raw("select name, age from user3 where name like ?", "%lqz%").Rows()
defer rows.Close()
for rows.Next() {
		rows.Scan(&name, &age)
		fmt.Printf("name是:%s,age是:%d\n",name,age)
}
  • 轉到 FindInBatches 獲取如何在批次中查詢和處理記錄的資訊, 轉到 Group 條件 獲取如何構建複雜 SQL 查詢的資訊

12.6 將 sql.Rows 掃描至 model

  • 使用 ScanRows 將一行記錄掃描至 struct,例如:
rows, err := db.Model(&User3{}).Where("name like ?", "%lqz%").Select("name, age").Rows()
defer rows.Close()
var user User3
for rows.Next() {
	// ScanRows 將一行掃描至 user
	db.ScanRows(rows, &user)
	fmt.Println(user)
}

12.7 Connection

  • Run mutliple SQL in same db tcp connection (not in a transaction)
db.Connection(func(tx *gorm.DB) error {
  tx.Exec("SET my.role = ?", "admin")

  tx.First(&User{})
})

12.8 Advanced(瞭解)

(1)子句(Clause)

  • GORM uses SQL builder generates SQL internally, for each operation, GORM creates a *gorm.Statement object, all GORM APIs add/change Clause for the Statement, at last, GORM generated SQL based on those clauses

  • For example, when querying with First, it adds the following clauses to the Statement

clause.Select{Columns: "*"}
clause.From{Tables: clause.CurrentTable}
clause.Limit{Limit: 1}
clause.OrderByColumn{
  Column: clause.Column{Table: clause.CurrentTable, Name: clause.PrimaryKey},
}
  • Then GORM build finally querying SQL in the Query callbacks like:
Statement.Build("SELECT", "FROM", "WHERE", "GROUP BY", "ORDER BY", "LIMIT", "FOR")
  • Which generate SQL:
SELECT * FROM `users` ORDER BY `users`.`id` LIMIT 1
  • You can define your own Clause and use it with GORM, it needs to implements Interface

  • Check out examples for reference

(2)子句構造器

  • For different databases, Clauses may generate different SQL, for example:
db.Offset(10).Limit(5).Find(&users)
// Generated for SQL Server
// SELECT * FROM "users" OFFSET 10 ROW FETCH NEXT 5 ROWS ONLY
// Generated for MySQL
// SELECT * FROM `users` LIMIT 5 OFFSET 10
  • Which is supported because GORM allows database driver register Clause Builder to replace the default one, take the Limit as example

(3)子句選項

  • GORM defined Many Clauses, and some clauses provide advanced options can be used for your application

  • Although most of them are rarely used, if you find GORM public API can’t match your requirements, may be good to check them out, for example:

db.Clauses(clause.Insert{Modifier: "IGNORE"}).Create(&user)
// INSERT IGNORE INTO users (name,age...) VALUES ("jinzhu",18...);

(4)StatementModifier

  • GORM provides interface StatementModifier allows you modify statement to match your requirements, take Hints as example
import "gorm.io/hints"

db.Clauses(hints.New("hint")).Find(&User{})
// SELECT * /*+ hint */ FROM `users`

十三、關聯之Belongs To

13.1 Belongs To

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

  • 例如,您的應用包含 user 和 company,並且每個 user 能且只能被分配給一個 company。下面的型別就表示這種關係。 注意,在 User 物件中,有一個和 Company 一樣的 CompanyID。 預設情況下, CompanyID 被隱含地用來在 UserCompany 之間建立一個外來鍵關係, 因此必須包含在 User 結構體中才能填充 Company 內部結構體。

// `User` 屬於 `Company`,`CompanyID` 是外來鍵
type User struct {
  gorm.Model
  Name      string
  CompanyID int
  Company   Company
}

type Company struct {
  ID   int
  Name string
}


db.AutoMigrate(&User{},&Company{})
  • 請參閱 預載入 以瞭解內部結構的詳細資訊。

13.2 重寫外來鍵

  • 要定義一個 belongs to 關係,資料庫的表中必須存在外來鍵。預設情況下,外來鍵的名字,使用擁有者的型別名稱加上表的主鍵的欄位名字

  • 例如,定義一個User實體屬於Company實體,那麼外來鍵的名字一般使用CompanyID。

  • GORM同時提供自定義外來鍵名字的方式,如下例所示。

type User struct {
  gorm.Model
  Name         string
  CompanyRefer int // 外來鍵改名字為CompanyRefer
  Company      Company `gorm:"foreignKey:CompanyRefer"`
  // 使用 CompanyRefer 作為外來鍵
}

type Company struct {
  ID   int
  Name string
}

13.3 重寫引用(一般不用)

  • 對於 belongs to 關係,GORM 通常使用資料庫表,主表(擁有者)的主鍵值作為外來鍵參考。 正如上面的例子,我們使用主表Company中的主鍵欄位ID作為外來鍵的參考值。

  • 如果在Company實體中設定了User實體,那麼GORM會自動把Company中的ID屬性儲存到User的CompanyID屬性中。

  • 同樣的,您也可以使用標籤 references 來更改它,例如:

type User struct {
  gorm.Model
  Name      string
  CompanyCode int
  Company   Company `gorm:"foreignKey:CompanyCode;references:Code"` // 指定外來鍵欄位為CompanyCode,指定與公司表中Code欄位關聯
}

type Company struct {
  ID   int
  Code int `gorm:"primarykey"`
  Name string
}

13.4 Belongs to 的 CRUD

  • 點選 關聯模式 連結獲取 belongs to 相關的用法

13.5 預載入

  • GORM允許透過使用Preload或者Joins來主動載入實體的關聯關係,具體內容請參考,預載入(主動載入)

13.6 外來鍵約束

  • 你可以透過OnUpdate, OnDelete配置標籤來增加關聯關係的級聯操作,如下面的例子,透過GORM可以完成使用者和公司的級聯更新和級聯刪除操作:
type User struct {
	gorm.Model
	Name      string
	Age int
	UserDetailID int
	UserDetail   UserDetail `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"` // 級聯更新,刪除時置空
}

type UserDetail struct {
	ID   int
	Addr string
}

// Navicat的設計表中可以檢視

13.7 案例

package main

import (
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
	"log"
	"os"
	"time"
)

type User struct {
	gorm.Model
	Name      string
	CompanyID int // 資料庫中欄位
	Company   Company // 不在資料庫生成,只用來快速查詢用
}

type Company struct {
	ID   int
	Name string
}

func main() {
	// 日誌配置
	newLogger := logger.New(
		log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer(日誌輸出的目標,字首和日誌包含的內容——譯者注)
		logger.Config{
			SlowThreshold:             time.Second, // 慢 SQL 閾值
			LogLevel:                  logger.Info, // 日誌級別為info
			IgnoreRecordNotFoundError: true,        // 忽略ErrRecordNotFound(記錄未找到)錯誤
			Colorful:                  true,        // 彩色列印
		},
	)

	dsn := "root:123@tcp(127.0.0.1:3306)/gorm_test?charset=utf8mb4&parseTime=True&loc=Local"
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
		Logger: newLogger,
	})
	if err != nil {
		panic(err)
	}
	//db.AutoMigrate(&User{}) // Company也會被一起建立好,並設定了外來鍵

	// 方式一:插入資料,兩個表都會被插入
	//db.Create(&User{
	//	Name:"lqz",
	//	Company: Company{
	//		ID: 1,
	//		Name: "抖音",
	//	},
	//})
	// 方式一:使用CompanyID插入
	//db.Create(&User{
	//	Name:"lqz2",
	//	CompanyID: 1,
	//})
	// 方式三:使用Company插入,但ID存在了
	db.Create(&User{
		Name:"lqz3",
		Company: Company{
			ID:1,
		},
	})
}

十四、透過preload和joins查詢多表

package main

import (
	"fmt"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
	"log"
	"os"
	"time"
)

type User struct {
	gorm.Model
	Name      string
	CompanyID int // 資料庫中欄位
	Company   Company // 不在資料庫生成,只用來快速查詢用
}

type Company struct {
	ID   int
	Name string
}

func main() {
	// 日誌配置
	newLogger := logger.New(
		log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer(日誌輸出的目標,字首和日誌包含的內容——譯者注)
		logger.Config{
			SlowThreshold:             time.Second, // 慢 SQL 閾值
			LogLevel:                  logger.Info, // 日誌級別為info
			IgnoreRecordNotFoundError: true,        // 忽略ErrRecordNotFound(記錄未找到)錯誤
			Colorful:                  true,        // 彩色列印
		},
	)

	dsn := "root:123@tcp(127.0.0.1:3306)/gorm_test?charset=utf8mb4&parseTime=True&loc=Local"
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
		Logger: newLogger,
	})
	if err != nil {
		panic(err)
	}
	// 1 普通查詢
	var user User
	//db.First(&user)
	//fmt.Println(user)
	//fmt.Println(user.Company) // 空的,沒有查出來,也就是預設並不會二次查詢
	
	// 3 Preload預載入
	//db.Preload("Company").First(&user)
	//fmt.Println(user.Company)

	// 4 Joins連結串列查
	db.Joins("Company").First(&user)
	fmt.Println(user.Company)
}

十五、has many關係

package main

import (
	"fmt"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
	"log"
	"os"
	"time"
)

type User struct {
	gorm.Model
	CreditCards []CreditCard `gorm:"foreignkey:UserID;constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"`
}

type CreditCard struct {
	gorm.Model
	Number string
	UserID uint
}

func main() {
	// 日誌配置
	newLogger := logger.New(
		log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer(日誌輸出的目標,字首和日誌包含的內容——譯者注)
		logger.Config{
			SlowThreshold:             time.Second, // 慢 SQL 閾值
			LogLevel:                  logger.Info, // 日誌級別為info
			IgnoreRecordNotFoundError: true,        // 忽略ErrRecordNotFound(記錄未找到)錯誤
			Colorful:                  true,        // 彩色列印
		},
	)

	dsn := "root:123@tcp(127.0.0.1:3306)/gorm_test?charset=utf8mb4&parseTime=True&loc=Local"
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
		Logger: newLogger,
	})
	if err != nil {
		panic(err)
	}
	// 1 建立表
	//db.AutoMigrate(&User{},&CreditCard{})

	//2 插入資料
	//var user=User{}
	//db.Create(&user) // 插入user
	//db.Create(&CreditCard{ // 插入CreditCard
	//	Number:"1001",
	//	UserID:user.ID,
	//})
	//db.Create(&CreditCard{ // 插入CreditCard
	//	Number:"1002",
	//	UserID:user.ID,
	//})

	// 3 查詢
	var user User
	db.Preload("CreditCards").First(&user)
	fmt.Println(user.CreditCards)

}

十六、多對多的關係

package main

import (
	"fmt"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
	"log"
	"os"
	"time"
)

// User 擁有並屬於多種 language,`user_languages` 是連線表
type User struct {
	gorm.Model
	Languages []Language `gorm:"many2many:user_languages;"`
}

type Language struct {
	gorm.Model
	Name string
}

func main() {
	// 日誌配置
	newLogger := logger.New(
		log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer(日誌輸出的目標,字首和日誌包含的內容——譯者注)
		logger.Config{
			SlowThreshold:             time.Second, // 慢 SQL 閾值
			LogLevel:                  logger.Info, // 日誌級別為info
			IgnoreRecordNotFoundError: true,        // 忽略ErrRecordNotFound(記錄未找到)錯誤
			Colorful:                  true,        // 彩色列印
		},
	)

	dsn := "root:123@tcp(127.0.0.1:3306)/gorm_test?charset=utf8mb4&parseTime=True&loc=Local"
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
		Logger: newLogger,
	})
	if err != nil {
		panic(err)
	}
	// 1 建立表
	//db.AutoMigrate(&User{}) //language表和中間表會自動建立

	// 2 插入資料
	//var lan =[]Language{}
	//lan=append(lan,Language{
	//	Name: "Go語言",
	//})
	//lan=append(lan,Language{
	//	Name: "Python語言",
	//})
	//db.Create(&User{
	//	Languages:lan,
	//})

	// 3 Preload查詢資料
	//var user User
	//db.Preload("Languages").First(&user)
	//fmt.Println(user.Languages)

	// 4 沒有使用Preload已經拿到User,再想獲取Languages
	var user User
	db.First(&user)
	fmt.Println(user.Languages)
	var lan []Language
	db.Model(&user).Association("Languages").Find(&lan)
	fmt.Println(lan)

}

十七、gorm其它操作

17.1 自定義表名

(1)完全自定義表名

// 給表結構體繫結方法即可
func (user *User) TableName ()string {
	return "lqz_user"
}

(2)給所有表名增加字首

db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{
  NamingStrategy: schema.NamingStrategy{
    TablePrefix: "t_",   // table name prefix, table for `User` would be `t_users`
    SingularTable: true, // use singular table name, table for `User` would be `user` with this option enabled
    NoLowerCase: true, // skip the snake_casing of names
    NameReplacer: strings.NewReplacer("CID", "Cid"), // use name replacer to change struct/field name before convert it to db name
  },
})
package main

import (
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
	"gorm.io/gorm/schema"
	"log"
	"os"
	"time"
)

type User struct {
	gorm.Model
	Languages []Language `gorm:"many2many:user_languages;"`
}

//func (user *User) TableName ()string {
//	return "lqz_user"
//}
type Language struct {
	gorm.Model
	Name string
}

func main() {
	// 日誌配置
	newLogger := logger.New(
		log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer(日誌輸出的目標,字首和日誌包含的內容——譯者注)
		logger.Config{
			SlowThreshold:             time.Second, // 慢 SQL 閾值
			LogLevel:                  logger.Info, // 日誌級別為info
			IgnoreRecordNotFoundError: true,        // 忽略ErrRecordNotFound(記錄未找到)錯誤
			Colorful:                  true,        // 彩色列印
		},
	)

	dsn := "root:123@tcp(127.0.0.1:3306)/gorm_test?charset=utf8mb4&parseTime=True&loc=Local"
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
		Logger: newLogger,
    // 跟上面的方式衝突,優先用上面方式
		NamingStrategy: schema.NamingStrategy{
			TablePrefix: "lqz_",
		},

	})
	if err != nil {
		panic(err)
	}
	// 1 建立表
	db.AutoMigrate(&User{})

}

17.2 自定義事件欄位

package main

import (
   "gorm.io/driver/mysql"
   "gorm.io/gorm"
   "gorm.io/gorm/logger"
   "gorm.io/gorm/schema"
   "log"
   "os"
   "time"
)

type User struct {
   ID uint
   Name string
   AddTime time.Time
}


func main() {
   // 日誌配置
   newLogger := logger.New(
      log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer(日誌輸出的目標,字首和日誌包含的內容——譯者注)
      logger.Config{
         SlowThreshold:             time.Second, // 慢 SQL 閾值
         LogLevel:                  logger.Info, // 日誌級別為info
         IgnoreRecordNotFoundError: true,        // 忽略ErrRecordNotFound(記錄未找到)錯誤
         Colorful:                  true,        // 彩色列印
      },
   )

   dsn := "root:123@tcp(127.0.0.1:3306)/gorm_test?charset=utf8mb4&parseTime=True&loc=Local"
   db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
      Logger: newLogger,
      NamingStrategy: schema.NamingStrategy{
         TablePrefix: "lqz_",
      },

   })
   if err != nil {
      panic(err)
   }
   // 1 建立表
   //db.AutoMigrate(&User{})
   // 報錯
   //db.Create(&User{
   // Name: "lqz",
   //})

   // 可以
   //db.Create(&User{
   // Name: "lqz",
   // AddTime: time.Now(),
   //})

   // 寫了鉤子後,不用手動寫時間
   db.Create(&User{
      Name: "lqz2",
   })


}
// 編寫鉤子
func (u *User) BeforeCreate(tx *gorm.DB) (err error) {
   u.AddTime=time.Now()
   return
}

相關文章