Gorm 數據庫 ORM 庫
官方文檔:GORM - The fantastic ORM library for Golang, aims to be developer friendly.
開源倉庫:go-gorm/gorm: The fantastic ORM library for Golang, aims to be developer friendly (github.com)
在 go 社區中,對於數據庫交互這一塊,有兩派人,一派人更喜歡簡潔的sqlx這一類的庫,功能並不那麼強大但是自己可以時時刻刻把控 sql,性能優化到極致。另一派人喜歡為了開發效率而生的 ORM,可以省去開發過程中許多不必要的麻煩。而提到 ORM,在 go 語言社區中就絕對繞不開gorm,它是一個非常老牌的 ORM,與之類似的還有相對比較年輕的xorm,ent等。這篇文章講的就是關於 gorm 的內容,本文只是對它的基礎入門內容做一個講解,權當是拋磚引玉,想要了解更深的細節可以閱讀官方文檔,它的中文文檔已經相當完善了,並且筆者也是 gorm 文檔的翻譯人員之一。
特點
- 全功能 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 當然也有一些缺點,比如幾乎所有的方法參數都是空接口類型,不去看文檔恐怕根本就不知道到底該傳什麼參數,有時候可以傳結構體,有時候可以傳字符串,有時候可以傳 map,有時候可以傳切片,語義比較模糊,並且很多情況還是需要自己手寫 SQL。
作為替代的有兩個 orm 可以試一試,第一個是aorm,剛開源不久,它不再需要去自己手寫表的字段名,大多情況下都是鏈式操作,基於反射實現,由於 star 數目不多,可以再觀望下。第二個就是ent,是facebook開源的 orm,它同樣支持鏈式操作,並且大多數情況下不需要自己去手寫 SQL,它的設計理念上是基於圖(數據結構裡面的那個圖),實現上基於代碼生成而非反射(比較認同這個),但是文檔是全英文的,有一定的上手門檻。
安裝
安裝 gorm 庫
$ go get -u gorm.io/gorm連接
gorm 目前支持以下幾種數據庫
- MySQL :
"gorm.io/driver/mysql" - PostgreSQL:
"gorm.io/driver/postgres" - SQLite:
"gorm.io/driver/sqlite" - SQL Server:
"gorm.io/driver/sqlserver" - TIDB:
"gorm.io/driver/mysql",TIDB 兼容 mysql 協議 - ClickHouse:
"gorm.io/driver/clickhouse"
除此之外,還有一些其它的數據庫驅動是由第三方開發者提供的,比如 oracle 的驅動CengSin/oracle。本文接下來將使用 MySQL 來進行演示,使用的什麼數據庫,就需要安裝什麼驅動,這裡安裝 Mysql 的 gorm 驅動。
$ go get -u gorm.io/driver/mysql然後使用 dsn(data source name)連接到數據庫,驅動庫會自行將 dsn 解析為對應的配置
package main
import (
"gorm.io/driver/mysql"
"gorm.io/gorm"
"log/slog"
)
func main() {
dsn := "root:123456@tcp(192.168.48.138:3306)/hello?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn))
if err != nil {
slog.Error("db connect error", err)
}
slog.Info("db connect success")
}或者手動傳入配置
package main
import (
"gorm.io/driver/mysql"
"gorm.io/gorm"
"log/slog"
)
func main() {
db, err := gorm.Open(mysql.New(mysql.Config{}))
if err != nil {
slog.Error("db connect error", err)
}
slog.Info("db connect success")
}兩種方法都是等價的,看自己使用習慣。
連接配置
通過傳入gorm.Config配置結構體,我們可以控制 gorm 的一些行為
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})下面是一些簡單的解釋,使用時可以根據自己的需求來進行配置。
type Config struct {
// 禁用默認事務,gorm在單個創建和更新時都會開啟事務以保持數據一致性
SkipDefaultTransaction bool
// 自定義的命名策略
NamingStrategy schema.Namer
// 保存完整的關聯
FullSaveAssociations bool
// 自定義logger
Logger logger.Interface
// 自定義nowfunc,用於注入CreatedAt和UpdatedAt字段
NowFunc func() time.Time
// 只生成sql不執行
DryRun bool
// 使用預編譯語句
PrepareStmt bool
// 建立連接後,ping一下數據庫
DisableAutomaticPing bool
// 在遷移數據庫時忽略外鍵
DisableForeignKeyConstraintWhenMigrating bool
// 在遷移數據庫時忽略關聯引用
IgnoreRelationshipsWhenMigrating bool
// 禁用嵌套事務
DisableNestedTransaction bool
// 運行全局更新,就是不加where的update
AllowGlobalUpdate bool
// 對表的所有字段進行查詢
QueryFields bool
// 批量創建的size
CreateBatchSize int
// 啟用錯誤轉換
TranslateError bool
// ClauseBuilders clause builder
ClauseBuilders map[string]clause.ClauseBuilder
// ConnPool db conn pool
ConnPool ConnPool
// Dialector database dialector
Dialector
// Plugins registered plugins
Plugins map[string]Plugin
callbacks *callbacks
cacheStore *sync.Map
}模型
在 gorm 中,模型與數據庫表相對應,它通常由結構體的方式展現,例如下面的結構體。
type Person struct {
Id uint
Name string
Address string
Mom string
Dad string
}結構體的內部可以由基本數據類型與實現了sql.Scanner和 sql.Valuer接口的類型組成。在默認情況下,Person結構體所映射的表名為perons,其為蛇形復數風格,以下劃線分隔。列名同樣是以蛇形風格,比如Id對應列名id,gorm 同樣也提供了一些方式來對其進行配置。
指定列名
通過結構體標簽,我們可以對結構體字段指定列名,這樣在實體映射的時候,gorm 就會使用指定的列名。
type Person struct {
Id uint `gorm:"column:ID;"`
Name string `gorm:"column:Name;"`
Address string
Mom string
Dad string
}指定表名
通過實現Table接口,就可以指定表明,它只有一個方法,就是返回表名。
type Tabler interface {
TableName() string
}在實現的方法中,它返回了字符串person,在數據庫遷移的時候,gorm 會創建名為person的表。
type Person struct {
Id uint `gorm:"column:ID;"`
Name string `gorm:"column:Name;"`
Address string
Mom string
Dad string
}
func (p Person) TableName() string {
return "person"
}對於命名策略,也可以在創建連接時傳入自己的策略實現來達到自定義的效果。
時間追蹤
type Person struct {
Id uint
Name string
Address string
Mom string
Dad string
CreatedAt sql.NullTime
UpdatedAt sql.NullTime
}
func (p Person) TableName() string {
return "person"
}當包含CreatedAt或UpdatedAt字段時,在創建或更新記錄時,如果其為零值,那麼 gorm 會自動使用time.Now()來設置時間。
db.Create(&Person{
Name: "jack",
Address: "usa",
Mom: "lili",
Dad: "tom",
})
// INSERT INTO `person` (`name`,`address`,`mom`,`dad`,`created_at`,`updated_at`) VALUES ('jack','usa','lili','tom','2023-10-25 14:43:57.16','2023-10-25 14:43:57.16')gorm 也支持時間戳追蹤
type Person struct {
Id uint `gorm:"primaryKey;"`
Name string `gorm:"primaryKey;"`
Address string
Mom string
Dad string
// nanoseconds
CreatedAt uint64 `gorm:"autoCreateTime:nano;"`
// milliseconds
UpdatedAt uint64 `gorm:"autoUpdateTime;milli;"`
}那麼在Create執行時,等價於下面的 SQL
INSERT INTO `person` (`name`,`address`,`mom`,`dad`,`created_at`,`updated_at`) VALUES ('jack','usa','lili','tom',1698216540519000000,1698216540)在實際情況中,如果有時間追蹤的需要,我更推薦後端存儲時間戳,在跨時區的情況下,處理更為簡單。
Model
gorm 提供了一個預設的Model結構體,它包含 ID 主鍵,以及兩個時間追蹤字段,和一個軟刪除記錄字段。
type Model struct {
ID uint `gorm:"primarykey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt DeletedAt `gorm:"index"`
}在使用時只需要將其嵌入到你的實體模型中即可。
type Order struct {
gorm.Model
Name string
}這樣它就會自動具備gorm.Model所有的特性。
主鍵
在默認情況下,名為Id的字段就是主鍵,使用結構體標簽可以指定主鍵字段
type Person struct {
Id uint `gorm:"primaryKey;"`
Name string
Address string
Mom string
Dad string
CreatedAt sql.NullTime
UpdatedAt sql.NullTime
}多個字段形成聯合主鍵
type Person struct {
Id uint `gorm:"primaryKey;"`
Name string `gorm:"primaryKey;"`
Address string
Mom string
Dad string
CreatedAt sql.NullTime
UpdatedAt sql.NullTime
}索引
通過index結構體標簽可以指定列索引
type Person struct {
Id uint `gorm:"primaryKey;"`
Name string `gorm:"primaryKey;"`
Address string `gorm:"index:idx_addr,unique,sort:desc;"`
Mom string
Dad string
// nanoseconds
CreatedAt uint64 `gorm:"autoCreateTime:nano;"`
// milliseconds
UpdatedAt uint64 `gorm:"autoUpdateTime;milli;"`
}在上面的結構體中,對Address字段建立了唯一索引。兩個字段使用同一個名字的索引就會創建復合索引
type Person struct {
Id uint `gorm:"primaryKey;"`
Name string `gorm:"primaryKey;"`
Address string `gorm:"index:idx_addr,unique;"`
School string `gorm:"index:idx_addr,unique;"`
Mom string
Dad string
// nanoseconds
CreatedAt uint64 `gorm:"autoCreateTime:nano;"`
// milliseconds
UpdatedAt uint64 `gorm:"autoUpdateTime;milli;"`
}外鍵
在結構體中定義外鍵關系,是通過嵌入結構體的方式來進行的,比如
type Person struct {
Id uint `gorm:"primaryKey;"`
Name string
MomId uint
Mom Mom `gorm:"foreignKey:MomId;"`
DadId uint
Dad Dad `gorm:"foreignKey:DadId;"`
}
type Mom struct {
Id uint
Name string
Persons []Person `gorm:"foreignKey:MomId;"`
}
type Dad struct {
Id uint
Name string
Persons []Person `gorm:"foreignKey:DadId;"`
}例子中,Person結構體有兩個外鍵,分別引用了Dad和Mom兩個結構體的主鍵,默認引用也就是主鍵。Person對於Dad和Mom是一對一的關系,一個人只能有一個爸爸和媽媽。Dad和Mom對於Person是一對多的關系,因為爸爸和媽媽可以有多個孩子。
Mom Mom `gorm:"foreignKey:MomId;"`嵌入結構體的作用是為了方便指定外鍵和引用,在默認情況下,外鍵字段名格式是被引用類型名+Id,比如MomId。默認情況下是引用的主鍵,通過結構體標簽可以指定引用某一個字段
type Person struct {
Id uint `gorm:"primaryKey;"`
Name string
MomId uint
Mom Mom `gorm:"foreignKey:MomId;references:Sid;constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"`
DadId uint
Dad Dad `gorm:"foreignKey:DadId;constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"`
}
type Mom struct {
Id uint
Sid uint `gorm:"uniqueIndex;"`
Name string
Persons []Person `gorm:"foreignKey:MomId;"`
}其中constraint:OnUpdate:CASCADE,OnDelete:SET NULL;便是定義的外鍵約束。
鉤子
一個實體模型可以自定義鉤子
- 創建
- 更新
- 刪除
- 查詢
對應的接口分別如下
// 創建前觸發
type BeforeCreateInterface interface {
BeforeCreate(*gorm.DB) error
}
// 創建後觸發
type AfterCreateInterface interface {
AfterCreate(*gorm.DB) error
}
// 更新前觸發
type BeforeUpdateInterface interface {
BeforeUpdate(*gorm.DB) error
}
// 更新後觸發
type AfterUpdateInterface interface {
AfterUpdate(*gorm.DB) error
}
// 保存前觸發
type BeforeSaveInterface interface {
BeforeSave(*gorm.DB) error
}
// 保存後觸發
type AfterSaveInterface interface {
AfterSave(*gorm.DB) error
}
// 刪除前觸發
type BeforeDeleteInterface interface {
BeforeDelete(*gorm.DB) error
}
// 刪除後觸發
type AfterDeleteInterface interface {
AfterDelete(*gorm.DB) error
}
// 查詢後觸發
type AfterFindInterface interface {
AfterFind(*gorm.DB) error
}結構體通過實現這些接口,可以自定義一些行為。
標簽
下面是 gorm 支持的一些標簽
| 標簽名 | 說明 |
|---|---|
column | 指定 db 列名 |
type | 列數據類型,推薦使用兼容性好的通用類型,例如:所有數據庫都支持 bool、int、uint、float、string、time、bytes 並且可以和其他標簽一起使用,例如:not null、size, autoIncrement… 像 varbinary(8) 這樣指定數據庫數據類型也是支持的。在使用指定數據庫數據類型時,它需要是完整的數據庫數據類型,如:MEDIUMINT UNSIGNED not NULL AUTO_INCREMENT |
serializer | 指定將數據序列化或反序列化到數據庫中的序列化器, 例如: serializer:json/gob/unixtime |
size | 定義列數據類型的大小或長度,例如 size: 256 |
primaryKey | 將列定義為主鍵 |
unique | 將列定義為唯一鍵 |
default | 定義列的默認值 |
precision | 指定列的精度 |
scale | 指定列大小 |
not null | 指定列為 NOT NULL |
autoIncrement | 指定列為自動增長 |
autoIncrementIncrement | 自動步長,控制連續記錄之間的間隔 |
embedded | 嵌套字段 |
embeddedPrefix | 嵌入字段的列名前綴 |
autoCreateTime | 創建時追蹤當前時間,對於 int 字段,它會追蹤時間戳秒數,您可以使用 nano/milli 來追蹤納秒、毫秒時間戳,例如:autoCreateTime:nano |
autoUpdateTime | 創建/更新時追蹤當前時間,對於 int 字段,它會追蹤時間戳秒數,您可以使用 nano/milli 來追蹤納秒、毫秒時間戳,例如:autoUpdateTime:milli |
index | 根據參數創建索引,多個字段使用相同的名稱則創建復合索引,查看 索引 open in new window 獲取詳情 |
uniqueIndex | 與 index 相同,但創建的是唯一索引 |
check | 創建檢查約束,例如 check:age > 13,查看 約束 open in new window 獲取詳情 |
<- | 設置字段寫入的權限, <-:create 只創建、<-:update 只更新、<-:false 無寫入權限、<- 創建和更新權限 |
-> | 設置字段讀的權限,->:false 無讀權限 |
- | 忽略該字段,- 表示無讀寫,-:migration 表示無遷移權限,-:all 表示無讀寫遷移權限 |
comment | 遷移時為字段添加注釋 |
foreignKey | 指定當前模型的列作為連接表的外鍵 |
references | 指定引用表的列名,其將被映射為連接表外鍵 |
polymorphic | 指定多態類型,比如模型名 |
polymorphicValue | 指定多態值、默認表名 |
many2many | 指定連接表表名 |
joinForeignKey | 指定連接表的外鍵列名,其將被映射到當前表 |
joinReferences | 指定連接表的外鍵列名,其將被映射到引用表 |
constraint | 關系約束,例如:OnUpdate、OnDelete |
遷移
AutoMigrate方法會幫助我們進行自動遷移,它會創建表,約束,索引,外鍵等等。
func (db *DB) AutoMigrate(dst ...interface{}) error例如
type Person struct {
Id uint `gorm:"primaryKey;"`
Name string `gorm:"type:varchar(100);uniqueIndex;"`
Address string
}
type Order struct {
Id uint
Name string
}
db.AutoMigrate(Person{}, Order{})
// CREATE TABLE `person` (`id` bigint unsigned AUTO_INCREMENT,`name` varchar(100),`address` longtext,PRIMARY KEY (`id`),UNIQUE INDEX `idx_person_name` (`name`))
// CREATE TABLE `orders` (`id` bigint unsigned AUTO_INCREMENT,`name` longtext,PRIMARY KEY (`id`))或者也可以我們手動來操作,通過Migrator方法訪問Migrator接口
func (db *DB) Migrator() Migrator它支持以下接口方法
type Migrator interface {
// AutoMigrate
AutoMigrate(dst ...interface{}) error
// Database
CurrentDatabase() string
FullDataTypeOf(*schema.Field) clause.Expr
GetTypeAliases(databaseTypeName string) []string
// Tables
CreateTable(dst ...interface{}) error
DropTable(dst ...interface{}) error
HasTable(dst interface{}) bool
RenameTable(oldName, newName interface{}) error
GetTables() (tableList []string, err error)
TableType(dst interface{}) (TableType, error)
// Columns
AddColumn(dst interface{}, field string) error
DropColumn(dst interface{}, field string) error
AlterColumn(dst interface{}, field string) error
MigrateColumn(dst interface{}, field *schema.Field, columnType ColumnType) error
HasColumn(dst interface{}, field string) bool
RenameColumn(dst interface{}, oldName, field string) error
ColumnTypes(dst interface{}) ([]ColumnType, error)
// Views
CreateView(name string, option ViewOption) error
DropView(name string) error
// Constraints
CreateConstraint(dst interface{}, name string) error
DropConstraint(dst interface{}, name string) error
HasConstraint(dst interface{}, name string) bool
// Indexes
CreateIndex(dst interface{}, name string) error
DropIndex(dst interface{}, name string) error
HasIndex(dst interface{}, name string) bool
RenameIndex(dst interface{}, oldName, newName string) error
GetIndexes(dst interface{}) ([]Index, error)
}方法列表中涉及到了數據庫,表,列,視圖,索引,約束多個維度,對需要自定義的用戶來說可以更加精細化的操作。
指定表注釋
在遷移時,如果想要添加表注釋,可以按照如下方法來設置
db.Set("gorm:table_options", " comment 'person table'").Migrator().CreateTable(Person{})需要注意的是如果使用的是AutoMigrate()方法來進行遷移,且結構體之間具引用關系,gorm 會進行遞歸先創建引用表,這就會導致被引用表和引用表的注釋都是重復的,所以推薦使用CreateTable方法來創建。
TIP
在創建表時CreateTable方法需要保證被引用表比引用表先創建,否則會報錯,而AutoMigrate方法則不需要,因為它會順著關系引用關系遞歸創建。
創建
Create
在創建新的記錄時,大多數情況都會用到Create方法
func (db *DB) Create(value interface{}) (tx *DB)現有如下的結構體
type Person struct {
Id uint `gorm:"primaryKey;"`
Name string
}創建一條記錄
user := Person{
Name: "jack",
}
// 必須傳入引用
db = db.Create(&user)
// 執行過程中發生的錯誤
err = db.Error
// 創建的數目
affected := db.RowsAffected創建完成後,gorm 會將主鍵寫入 user 結構體中,所以這也是為什麼必須得傳入指針。如果傳入的是一個切片,就會批量創建
user := []Person{
{Name: "jack"},
{Name: "mike"},
{Name: "lili"},
}
db = db.Create(&user)同樣的,gorm 也會將主鍵寫入切片中。當數據量過大時,也可以使用CreateInBatches方法分批次創建,因為生成的INSERT INTO table VALUES (),()這樣的 SQL 語句會變的很長,每個數據庫對 SQL 長度是有限制的,所以必要的時候可以選擇分批次創建。
db = db.CreateInBatches(&user, 50)除此之外,Save方法也可以創建記錄,它的作用是當主鍵匹配時就更新記錄,否則就插入。
func (db *DB) Save(value interface{}) (tx *DB)user := []Person{
{Name: "jack"},
{Name: "mike"},
{Name: "lili"},
}
db = db.Save(&user)Upsert
Save方法只能是匹配主鍵,我們可以通過構建Clause來完成更加自定義的 upsert。比如下面這行代碼
db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "name"}},
DoNothing: false,
DoUpdates: clause.AssignmentColumns([]string{"address"}),
UpdateAll: false,
}).Create(&p)它的作用是當字段name沖突後,更新字段address的值,不沖突的話就會創建一個新的記錄。也可以在沖突的時候什麼都不做
db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "name"}},
DoNothing: true,
}).Create(&p)或者直接更新所有字段
db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "name"}},
UpdateAll: true,
}).Create(&p)在使用 upsert 之前,記得給沖突字段添加索引。
查詢
First
gorm 對於查詢而言,提供了相當多的方法可用,第一個就是First方法
func (db *DB) First(dest interface{}, conds ...interface{}) (tx *DB)它的作用是按照主鍵升序查找第一條記錄,例如
var person Person
result := db.First(&person)
err := result.Error
affected := result.RowsAffected傳入dest指針方便讓 gorm 將查詢到的數據映射到結構體中。
或者使用Table和Model方法可以指定查詢表,前者接收字符串表名,後者接收實體模型。
db.Table("person").Find(&p)
db.Model(Person{}).Find(&p)TIP
如果傳入的指針元素包含實體模型比如說結構體指針,或者是結構體切片的指針,那麼就不需要手動使用指定查哪個表,這個規則適用於所有的增刪改查操作。
Take
Take方法與First類似,區別就是不會根據主鍵排序。
func (db *DB) Take(dest interface{}, conds ...interface{}) (tx *DB)var person Person
result := db.Take(&person)
err := result.Error
affected := result.RowsAffectedPluck
Pluck方法用於批量查詢一個表的單列,查詢的結果可以收集到一個指定類型的切片中,不一定非得是實體類型的切片。
func (db *DB) Pluck(column string, dest interface{}) (tx *DB)比如將所有人的地址搜集到一個字符串切片中
var adds []string
// SELECT `address` FROM `person` WHERE name IN ('jack','lili')
db.Model(Person{}).Where("name IN ?", []string{"jack", "lili"}).Pluck("address", &adds)其實就等同於
db.Select("address").Where("name IN ?", []string{"jack", "lili"}).Find(&adds)Count
Count方法用於統計實體記錄的數量
func (db *DB) Count(count *int64) (tx *DB)看一個使用示例
var count int64
// SELECT count(*) FROM `person`
db.Model(Person{}).Count(&count)Find
批量查詢最常用的是Find方法
func (db *DB) Find(dest interface{}, conds ...interface{}) (tx *DB)它會根據給定的條件查找出所有符合的記錄
// SELECT * FROM `person`
var ps []Person
db.Find(&ps)Select
gorm 在默認情況下是查詢所有字段,我們可以通過Select方法來指定字段
func (db *DB) Select(query interface{}, args ...interface{}) (tx *DB)比如
// SELECT `address`,`name` FROM `person` ORDER BY `person`.`id` LIMIT 1
db.Select("address", "name").First(&p)等同於
db.Select([]string{"address", "name"}).First(&p)同時,還可以使用Omit方法來忽略字段
func (db *DB) Omit(columns ...string) (tx *DB)比如
// SELECT `person`.`id`,`person`.`name` FROM `person` WHERE id IN (1,2,3,4)
db.Omit("address").Where("id IN ?", []int{1, 2, 3, 4}).Find(&ps)``由Select和Omit選擇或忽略的字段,在創建更新查詢的時候都會起作用。
Where
條件查詢會用到Where方法
func (db *DB) Where(query interface{}, args ...interface{}) (tx *DB)下面是一個簡單的示例
var p Person
db.Where("id = ?", 1).First(&p)在鏈式操作中使用多個Where會構建多個AND語句,比如
// SELECT * FROM `person` WHERE id = 1 AND name = 'jack' ORDER BY `person`.`id` LIMIT 1
db.Where("id = ?", 1).Where("name = ?", "jack").First(&p)或者使用Or方法來構建OR語句
func (db *DB) Or(query interface{}, args ...interface{}) (tx *DB)// SELECT * FROM `person` WHERE id = 1 OR name = 'jack' AND address = 'usa' ORDER BY `person`.`id` LIMIT 1
db.Where("id = ?", 1).
Or("name = ?", "jack").
Where("address = ?", "usa").
First(&p)還有Not方法,都是類似的
func (db *DB) Not(query interface{}, args ...interface{}) (tx *DB)// SELECT * FROM `person` WHERE id = 1 OR name = 'jack' AND NOT name = 'mike' AND address = 'usa' ORDER BY `person`.`id` LIMIT 1
db.Where("id = ?", 1).
Or("name = ?", "jack").
Not("name = ?", "mike").
Where("address = ?", "usa").
First(&p)對於IN條件,可以直接在Where方法裡面傳入切片。
db.Where("address IN ?", []string{"cn", "us"}).Find(&ps)或者多列IN條件,需要用[][]any類型來承載參數
// SELECT * FROM `person` WHERE (id, name, address) IN ((1,'jack','uk'),(2,'mike','usa'))
db.Where("(id, name, address) IN ?", [][]any{{1, "jack", "uk"}, {2, "mike", "usa"}}).Find(&ps)gorm 支持 where 分組使用,就是將上述幾個語句結合起來
db.Where(
db.Where("name IN ?", []string{"cn", "uk"}).Where("id IN ?", []uint{1, 2}),
).Or(
db.Where("name IN ?", []string{"usa", "jp"}).Where("id IN ?", []uint{3, 4}),
).Find(&ps)
// SELECT * FROM `person` WHERE (name IN ('cn','uk') AND id IN (1,2)) OR (name IN ('usa','jp') AND id IN (3,4))Order
排序會用到Order方法
func (db *DB) Order(value interface{}) (tx *DB)來看個使用的例子
var ps []Person
// SELECT * FROM `person` ORDER BY name ASC, id DESC
db.Order("name ASC, id DESC").Find(&ps)也可以多次調用
// SELECT * FROM `person` ORDER BY name ASC, id DESC,address
db.Order("name ASC, id DESC").Order("address").Find(&ps)Limit
Limit和Offset方法常常用於分頁查詢
func (db *DB) Limit(limit int) (tx *DB)
func (db *DB) Offset(offset int) (tx *DB)下面是一個簡單的分頁示例
var (
ps []Person
page = 2
size = 10
)
// SELECT * FROM `person` LIMIT 10 OFFSET 10
db.Offset((page - 1) * size).Limit(size).Find(&ps)Group
Group和Having方法多用於分組操作
func (db *DB) Group(name string) (tx *DB)
func (db *DB) Having(query interface{}, args ...interface{}) (tx *DB)下面看個例子
var (
ps []Person
)
// SELECT `address` FROM `person` GROUP BY `address` HAVING address IN ('cn','us')
db.Select("address").Group("address").Having("address IN ?", []string{"cn", "us"}).Find(&ps)Distinct
Distinct方法多用於去重
func (db *DB) Distinct(args ...interface{}) (tx *DB)看一個示例
// SELECT DISTINCT `name` FROM `person` WHERE address IN ('cn','us')
db.Where("address IN ?", []string{"cn", "us"}).Distinct("name").Find(&ps)子查詢
子查詢就是嵌套查詢,例如想要查詢出所有id值大於平均值的人
// SELECT * FROM `person` WHERE id > (SELECT AVG(id) FROM `person`
db.Where("id > (?)", db.Model(Person{}).Select("AVG(id)")).Find(&ps)from 子查詢
// SELECT * FROM (SELECT * FROM `person` WHERE address IN ('cn','uk')) as p
db.Table("(?) as p", db.Model(Person{}).Where("address IN ?", []string{"cn", "uk"})).Find(&ps)鎖
gorm 使用clause.Locking子句來提供鎖的支持
// SELECT * FROM `person` FOR UPDATE
db.Clauses(clause.Locking{Strength: "UPDATE"}).Find(&ps)
// SELECT * FROM `person` FOR SHARE NOWAIT
db.Clauses(clause.Locking{Strength: "SHARE", Options: "NOWAIT"}).Find(&ps)迭代
通過Rows方法可以獲取一個迭代器
func (db *DB) Rows() (*sql.Rows, error)通過遍歷迭代器,使用ScanRows方法可以將每一行的結果掃描到結構體中。
rows, err := db.Model(Person{}).Rows()
if err != nil {
return
}
defer rows.Close()
for rows.Next() {
var p Person
err := db.ScanRows(rows, &p)
if err != nil {
return
}
}修改
save
在創建的時候提到過Save方法,它也可以用來更新記錄,並且它會更新所有字段,即便有些結構體的字段是零值,不過如果主鍵匹配不到的話就會進行插入操作了。
var p Person
db.First(&p)
p.Address = "poland"
// UPDATE `person` SET `name`='json',`address`='poland' WHERE `id` = 2
db.Save(&p)可以看到它把除了主鍵以外的字段全都添到了SET語句中。
update
所以大多數情況下,建議使用Update方法
func (db *DB) Update(column string, value interface{}) (tx *DB)它主要是用來更新單列字段
var p Person
db.First(&p)
// UPDATE `person` SET `address`='poland' WHERE id = 2
db.Model(Person{}).Where("id = ?", p.Id).Update("address", "poland")updates
Updates方法用於更新多列,接收結構體和 map 作為參數,並且當結構體字段為零值時,會忽略該字段,但在 map 中不會。
func (db *DB) Updates(values interface{}) (tx *DB)下面是一個例子
var p Person
db.First(&p)
// UPDATE `person` SET `name`='jojo',`address`='poland' WHERE `id` = 2
db.Model(p).Updates(Person{Name: "jojo", Address: "poland"})
// UPDATE `person` SET `address`='poland',`name`='jojo' WHERE `id` = 2
db.Model(p).Updates(map[string]any{"name": "jojo", "address": "poland"})SQL 表達式
有些時候,常常會會需要對字段進行一些自增或者自減等與自身進行運算的操作,一般是先查再計算然後更新,或者是使用 SQL 表達式。
func Expr(expr string, args ...interface{}) clause.Expr看下面的一個例子
// UPDATE `person` SET `age`=age + age,`name`='jojo' WHERE `id` = 2
db.Model(p).Updates(map[string]any{"name": "jojo", "age": gorm.Expr("age + age")})
// UPDATE `person` SET `age`=age * 2 + age,`name`='jojo' WHERE `id` = 2
db.Model(p).Updates(map[string]any{"name": "jojo", "age": gorm.Expr("age * 2 + age")})刪除
在 gorm 中,刪除記錄會用到Delete方法,它可以直接傳實體結構,也可以傳條件。
func (db *DB) Delete(value interface{}, conds ...interface{}) (tx *DB)例如直接傳結構體
var p Person
db.First(&p)
// // DELETE FROM `person` WHERE `person`.`id` = 2
db.Delete(&p)或者
var p Person
db.First(&p)
// DELETE FROM `person` WHERE `person`.`id` = 2
db.Model(p).Delete(nil)或者指定條件
// DELETE FROM `person` WHERE id = 2
db.Model(Person{}).Where("id = ?", p.Id).Delete(nil)也可以簡寫成
var p Person
db.First(&p)
// DELETE FROM `person` WHERE id = 2
db.Delete(&Person{}, "id = ?", 2)
// DELETE FROM `person` WHERE `person`.`id` = 2
db.Delete(&Person{}, 2)批量刪除的話就是傳入切片
// DELETE FROM `person` WHERE id IN (1,2,3)
db.Delete(&Person{}, "id IN ?", []uint{1, 2, 3})
// DELETE FROM `person` WHERE `person`.`id` IN (1,2,3)
db.Delete(&Person{}, []uint{1, 2, 3})軟刪除
假如你的實體模型使用了軟刪除,那麼在刪除時,默認進行更新操作,若要永久刪除的話可以使用Unscope方法
db.Unscoped().Delete(&Person{}, []uint{1, 2, 3})關聯定義
gorm 提供了表關聯的交互能力,通過嵌入結構體和字段的形式來定義結構體與結構體之間的關聯。
一對一
一對一關系是最簡單的,正常情況下一個人只能有一個母親,看下面的結構體
type Person struct {
Id uint
Name string
Address string
Age uint
MomId sql.NullInt64
Mom Mom `gorm:"foreignKey:MomId;"`
}
type Mom struct {
Id uint
Name string
}Person結構體通過嵌入Mom結構體,實現了對Mom類型的引用,其中Person.MomId就是引用字段,主鍵Mom.Id就是被引用字段,這樣就完成了一對一關系的關聯。如何自定義外鍵以及引用和約束還有默認的外鍵規則這些已經在外鍵定義中已經講到過,就不再贅述
TIP
對於外鍵字段,推薦使用sql包提供的類型,因為外鍵默認可以為NULL,在使用Create創建記錄時,如果使用普通類型,零值0也會被創建,不存在的外鍵被創建顯然是不被允許的。
一對多
下面加一個學校結構體,學校與學生是一對多的關系,一個學校有多個學生,但是一個學生只能在一個學校上學。
type Person struct {
Id uint
Name string
Address string
Age uint
MomId sql.NullInt64
Mom Mom `gorm:"foreignKey:MomId;"`
SchoolId sql.NullInt64
School School `gorm:"foreignKey:SchoolId;"`
}
type Mom struct {
Id uint
Name string
}
type School struct {
Id uint
Name string
Persons []Person `gorm:"foreignKey:SchoolId;"`
}school.Persons是[]person類型,表示著可以擁有多個學生,而Person則必須要有包含引用School的外鍵,也就是Person.SchoolId。
多對多
一個人可以擁有很多房子,一個房子也可以住很多人,這就是一個多對多的關系。
type Person struct {
Id uint
Name string
Address string
Age uint
MomId sql.NullInt64
Mom Mom `gorm:"foreignKey:MomId;"`
SchoolId sql.NullInt64
School School `gorm:"foreignKey:SchoolId;"`
Houses []House `gorm:"many2many:person_house;"`
}
type Mom struct {
Id uint
Name string
}
type School struct {
Id uint
Name string
Persons []Person
}
type House struct {
Id uint
Name string
Persons []Person `gorm:"many2many:person_house;"`
}
type PersonHouse struct {
PersonId sql.NullInt64
Person Person `gorm:"foreignKey:PersonId;"`
HouseId sql.NullInt64
House House `gorm:"foreignKey:HouseId;"`
}Person和House互相持有對方的切片類型表示多對多的關系,多對多關系一般需要創建連接表,通過many2many來指定連接表,連接表的外鍵必須要指定正確。
創建完結構體後讓 gorm 自動遷移到數據庫中
tables := []any{
School{},
Mom{},
Person{},
House{},
PersonHouse{},
}
for _, table := range tables {
db.Migrator().CreateTable(&table)
}注意引用表與被引用表的先後創建順序。
關聯操作
在創建完上述三種關聯關系後,接下來就是如何使用關聯來進行增刪改查。這主要會用到Association方法
func (db *DB) Association(column string) *Association它接收一個關聯參數,它的值應該是嵌入引用結構體中的被引用類型的字段名。
db.Model(&person).Association("Mom").Find(&mom)比如關聯查找一個人的母親,Association的參數就是Mom,也就是Person.Mom字段名。
創建關聯
// 定義好數據
jenny := Mom{
Name: "jenny",
}
mit := School{
Name: "MIT",
Persons: nil,
}
h1 := House{
Id: 0,
Name: "h1",
Persons: nil,
}
h2 := House{
Name: "h2",
Persons: nil,
}
jack := Person{
Name: "jack",
Address: "usa",
Age: 18,
}
mike := Person{
Name: "mike",
Address: "uk",
Age: 20,
}
// INSERT INTO `people` (`name`,`address`,`age`,`mom_id`,`school_id`) VALUES ('jack','usa',18,NULL,NULL)
db.Create(&jack)
// INSERT INTO `schools` (`name`) VALUES ('MIT')
db.Create(&mit)
// 添加Person與Mom的關聯,一對一關聯
// INSERT INTO `moms` (`name`) VALUES ('jenny') ON DUPLICATE KEY UPDATE `id`=`id`
// UPDATE `people` SET `mom_id`=1 WHERE `id` = 1
db.Model(&jack).Association("Mom").Append(&jenny)
// 添加school與Person的關聯,一對多關聯
// INSERT INTO `people` (`name`,`address`,`age`,`mom_id`,`school_id`,`id`) VALUES ('jack','usa',18,1,1,1),('mike','uk',20,NULL,1,DEFAULT) ON DUPLICATE KEY UPDATE `school_id`=VALUES(`school_id`)
db.Model(&mit).Association("Persons").Append([]Person{jack, mike})
// 添加Person與Houses的關聯,多對多關聯
// INSERT INTO `houses` (`name`) VALUES ('h1'),('h2') ON DUPLICATE KEY UPDATE `id`=`id`
// INSERT INTO `person_house` (`person_id`,`house_id`) VALUES (1,1),(1,2) ON DUPLICATE KEY UPDATE `person_id`=`person_id`
db.Model(&jack).Association("Houses").Append([]House{h1, h2})假如所有的記錄都不存在,在進行關聯創建時,也會先創建記錄再創建關聯。
查找關聯
下面演示如何進行查找關聯。
// 一對一關聯查找
var person Person
var mom Mom
// SELECT * FROM `people` ORDER BY `people`.`id` LIMIT 1
db.First(&person)
// SELECT * FROM `moms` WHERE `moms`.`id` = 1
db.Model(person).Association("Mom").Find(&mom)
// 一對多關聯查找
var school School
var persons []Person
// SELECT * FROM `schools` ORDER BY `schools`.`id` LIMIT 1
db.First(&school)
// SELECT * FROM `people` WHERE `people`.`school_id` = 1
db.Model(&school).Association("Persons").Find(&persons)
// 多對多關聯查找
var houses []House
// SELECT `houses`.`id`,`houses`.`name` FROM `houses` JOIN `person_house` ON `person_house`.`house_id` = `houses`.`id` AND `person_house`.`person_id` IN (1,2)
db.Model(&persons).Association("Houses").Find(&houses)關聯查找會根據已有的數據,去引用表中查找符合條件的記錄,對於多對多關系而言,gorm 會自動完成表連接這一過程。
更新關聯
下面演示如何進行更新關聯
// 一對一關聯更新
var jack Person
lili := Mom{
Name: "lili",
}
// SELECT * FROM `people` WHERE name = 'jack' ORDER BY `people`.`id` LIMIT 1
db.Where("name = ?", "jack").First(&jack)
// INSERT INTO `moms` (`name`) VALUES ('lili')
db.Create(&lili)
// INSERT INTO `moms` (`name`,`id`) VALUES ('lili',2) ON DUPLICATE KEY UPDATE `id`=`id`
// UPDATE `people` SET `mom_id`=2 WHERE `id` = 1
db.Model(&jack).Association("Mom").Replace(&lili)
// 一對多關聯更新
var mit School
newPerson := []Person{{Name: "bob"}, {Name: "jojo"}}
// INSERT INTO `people` (`name`,`address`,`age`,`mom_id`,`school_id`) VALUES ('bob','',0,NULL,NULL),('jojo','',0,NULL,NULL)
db.Create(&newPerson)
// SELECT * FROM `schools` WHERE name = 'mit' ORDER BY `schools`.`id` LIMIT 1
db.Where("name = ?", "mit").First(&mit)
// INSERT INTO `people` (`name`,`address`,`age`,`mom_id`,`school_id`,`id`) VALUES ('bob','',0,NULL,1,4),('jojo','',0,NULL,1,5) ON DUPLICATE KEY UPDATE `school_id`=VALUES(`school_id`)
// UPDATE `people` SET `school_id`=NULL WHERE `people`.`id` NOT IN (4,5) AND `people`.`school_id` = 1
db.Model(&mit).Association("Persons").Replace(newPerson)
// 多對多關聯更新
// INSERT INTO `houses` (`name`) VALUES ('h3'),('h4'),('h5') ON DUPLICATE KEY UPDATE `id`=`id`
// INSERT INTO `person_house` (`person_id`,`house_id`) VALUES (1,3),(1,4),(1,5) ON DUPLICATE KEY UPDATE `person_id`=`person_id`
// DELETE FROM `person_house` WHERE `person_house`.`person_id` = 1 AND `person_house`.`house_id` NOT IN (3,4,5)
db.Model(&jack).Association("Houses").Replace([]House{{Name: "h3"}, {Name: "h4"}, {Name: "h5"}})在關聯更新時,如果被引用數據和引用數據都不存在,gorm 會嘗試創建它們。
刪除關聯
下面演示如何刪除關聯
// 一對一關聯刪除
var (
jack Person
lili Mom
)
// SELECT * FROM `people` WHERE name = 'jack' ORDER BY `people`.`id` LIMIT 1
db.Where("name = ?", "jack").First(&jack)
// SELECT * FROM `moms` WHERE name = 'lili' ORDER BY `moms`.`id` LIMIT 1
db.Where("name = ?", "lili").First(&lili)
// UPDATE `people` SET `mom_id`=NULL WHERE `people`.`id` = 1 AND `people`.`mom_id` = 2
db.Model(&jack).Association("Mom").Delete(&lili)
// 一對多關聯刪除
var (
mit School
persons []Person
)
// SELECT * FROM `schools` WHERE name = 'mit' ORDER BY `schools`.`id` LIMIT 1
db.Where("name = ?", "mit").First(&mit)
// SELECT * FROM `people` WHERE name IN ('jack','mike')
db.Where("name IN ?", []string{"jack", "mike"}).Find(&persons)
// UPDATE `people` SET `school_id`=NULL WHERE `people`.`school_id` = 1 AND `people`.`id` IN (1,2)
db.Model(&mit).Association("Persons").Delete(&persons)
// 多對多關聯刪除
var houses []House
// SELECT * FROM `houses` WHERE name IN ('h3','h4')
db.Where("name IN ?", []string{"h3", "h4"}).Find(&houses)
// DELETE FROM `person_house` WHERE `person_house`.`person_id` = 1 AND `person_house`.`house_id` IN (3,4)
db.Model(&jack).Association("Houses").Delete(&houses)關聯刪除時只會刪除它們之間的引用關系,並不會刪除實體記錄。我們還可以使用Clear方法來直接清空關聯
db.Model(&jack).Association("Houses").Clear()如果想要刪除對應的實體記錄,可以在Association操作後面加上Unscoped操作(不會影響 many2many)
db.Model(&jack).Association("Houses").Unscoped().Delete(&houses)對於一對多和多對多而言,可以使用Select操作來刪除記錄
var (
mit School
)
db.Where("name = ?", "mit").First(&mit)
db.Select("Persons").Delete(&mit)預加載
預加載用於查詢關聯數據,對於具有關聯關系的實體而言,它會先預先加載被關聯引用的實體。之前提到的關聯查詢是對關聯關系進行查詢,預加載是直接對實體記錄進行查詢,包括所有的關聯關系。從語法上來說,關聯查詢需要先查詢指定的[]Person,然後再根據[]Person 去查詢關聯的[]Mom,預加載從語法上直接查詢[]Person,並且也會將所有的關聯關系順帶都加載了,不過實際上它們執行的 SQL 都是差不多的。下面看一個例子
var users []Person
// SELECT * FROM `moms` WHERE `moms`.`id` = 1
// SELECT * FROM `people`
db.Preload("Mom").Find(&users)這是一個一對一關聯查詢的例子,它的輸出
[{Id:1 Name:jack Address:usa Age:18 MomId:{Int64:1 Valid:true} Mom:{Id:1 Name:jenny} SchoolId:{Int64:1 Valid:true} School:{Id:0 Name: Persons:[]} Houses:[]} {Id:2 Name:mike Address:uk Age:20 MomId:{Int64:0 Valid:false} Mom:{Id:0 Name:} SchoolId:{Int64:1 Valid:true} School:{Id:0 Name: Persons:[]} Houses:[]}]可以看到將關聯的Mom一並查詢出來了,但是沒有預加載學校關系,所有School結構體都是零值。還可以使用clause.Associations表示預加載全部的關系,除了嵌套的關系。
db.Preload(clause.Associations).Find(&users)下面來看一個嵌套預加載的例子,它的作用是查詢出所有學校關聯的所有學生以及每一個學生所關聯的母親和每一個學生所擁有的房子,而且還要查詢出每一個房子的主人集合,學校->學生->房子->學生。
var schools []School
db.Preload("Persons").
Preload("Persons.Mom").
Preload("Persons.Houses").
Preload("Persons.Houses.Persons").Find(&schools)
// 輸出代碼,邏輯可以忽略
for _, school := range schools {
fmt.Println("school", school.Name)
for _, person := range school.Persons {
fmt.Println("person", person.Name)
fmt.Println("mom", person.Mom.Name)
for _, house := range person.Houses {
var persons []string
for _, p := range house.Persons {
persons = append(persons, p.Name)
}
fmt.Println("house", house.Name, "owner", persons)
}
fmt.Println()
}
}輸出為
school MIT
person jack
mom jenny
house h1 owner [jack]
house h2 owner [jack]
person mike
mom可以看到輸出了每一個學校的每一個學生的母親以及它們的房子,還有房子的所有主人。
事務
gorm 默認開啟事務,任何插入和更新操作失敗後都會回滾,可以在連接配置中關閉,性能大概會提升 30%左右。gorm 中事務的使用有多種方法,下面簡單介紹下。
自動
閉包事務,通過Transaction方法,傳入一個閉包函數,如果函數返回值不為 nil,那麼就會自動回滾。
func (db *DB) Transaction(fc func(tx *DB) error, opts ...*sql.TxOptions) (err error)下面看一個例子,閉包中的操作應該使用參數tx,而非外部的db。
var ps []Person
db.Transaction(func(tx *gorm.DB) error {
err := tx.Create(&ps).Error
if err != nil {
return err
}
err = tx.Create(&ps).Error
if err != nil {
return err
}
err = tx.Model(Person{}).Where("id = ?", 1).Update("name", "jack").Error
if err != nil {
return err
}
return nil
})手動
比較推薦使用手動事務,由我們自己來控制何時回滾,何時提交。手動事務會用到下面三個方法
// Begin方法用於開啟事務
func (db *DB) Begin(opts ...*sql.TxOptions) *DB
// Rollback方法用於回滾事務
func (db *DB) Rollback() *DB
// Commit方法用於提交事務
func (db *DB) Commit() *DB下面看一個例子,開啟事務後,就應該使用tx來操作 orm。
var ps []Person
tx := db.Begin()
err := tx.Create(&ps).Error
if err != nil {
tx.Rollback()
return
}
err = tx.Create(&ps).Error
if err != nil {
tx.Rollback()
return
}
err = tx.Model(Person{}).Where("id = ?", 1).Update("name", "jack").Error
if err != nil {
tx.Rollback()
return
}
tx.Commit()可以指定回滾點
var ps []Person
tx := db.Begin()
err := tx.Create(&ps).Error
if err != nil {
tx.Rollback()
return
}
tx.SavePoint("createBatch")
err = tx.Create(&ps).Error
if err != nil {
tx.Rollback()
return
}
err = tx.Model(Person{}).Where("id = ?", 1).Update("name", "jack").Error
if err != nil {
tx.RollbackTo("createBatch")
return
}
tx.Commit()總結
如果你閱讀完了上面的所有內容,並動手敲了代碼,那麼你就可以使用 gorm 進行對數據庫進行增刪改查了,gorm 除了這些操作以外,還有其它許多功能,更多細節可以前往官方文檔了解。
