[Note] GORM 筆記
TL;DR
- query @ gorm
- method chaining @ form
db.
Table("table_name"). // Model(&model{})
Select("select syntax").
Group("field_names").
Order("field_name").
Find(&model{}) // Rows()
- methods 可以分成 chain methods、finisher methods、new session method。
與 Database 連線
- Connecting to a Database @ gorm
除了下述說明外,建議可以參考 go-snippets/pkg/gorm:
import (
"gorm.io/driver/postgres"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
func initDatabase(dsn string) (*database.GormDatabase, error) {
db, err := database.New(dsn, &gorm.Config{
Logger: logger.Default.LogMode(logger.Info),
})
if err != nil {
return nil, err
}
if err := db.AutoMigrate(new(model.Organization)); err != nil {
log.Fatal(err.Error())
}
fmt.Println("Database connected ...")
return db, nil
}
func main() {
dsn := "sslmode=disable host=localhost port=5432 user=postgres dbname=gorm_sandbox"
db, err := initDatabase(dsn)
}
CRUD Interface
template/database/product @ go-snippets
Update
Update @ gorm
db.Update()
在 Update 中可以透過 struct
或 map
一次更新多個欄位,但要特別留意的是
- 使用 struct 的方式只會更新 non-zero value,也就是說如果值是
false
或0
這類的都不會被保存 - 使用
map
的方式才會更新 zero-value,因此個人會建議使用 map 的方式來避免可能的 bug
// Update attributes with `struct`, will only update non-zero fields
db.Model(&user).Updates(User{Name: "hello", Age: 18, Active: false})
// UPDATE users SET name='hello', age=18, updated_at = '2013-11-17 21:34:10' WHERE id = 111;
// Update attributes with `map`
db.Model(&user).Updates(map[string]interface{}{"name": "hello", "age": 18, "active": false})
// UPDATE users SET name='hello', age=18, active=false, updated_at='2013-11-17 21:34:10' WHERE id=111;
💡 會建議使用 map 的方式(而非 struct)來進行更新避免可能的 bug
db.Save()
使用 db.Save()
時,沒給的值會全部被清空(包括 createdAt
、除了 updatedAt
),所以如果要用 save,一定要把舊有的資料先拉出來,對要改變的內容進行修改後,在呼叫 save
:
// https://gorm.io/docs/update.html
// 先把原有資料拉出來
db.First(&user)
// 修改要改動的欄位就好
user.Name = "jinzhu 2"
user.Age = 100
// 使用 Save
db.Save(&user)
⚠️ 使用
db.Save()
時所有的欄位都會更新,因此若沒給的話會導致該值變回 zero-value,記得一定要先把資料拉出來後,修改要改的資料後,在呼叫 Save。
Upsert
Upsert-On-Conflict @ Create > Upsert
- 使用 Gorm 內建的 upsert 方法會類似 Save,有指定更新欄位的一定要給值,否則會被清空
- 使用
Where
+ Assign + FirstOrCreate` 組出來的 upsert 方法會類似 update,需要留意當使用者想要清空原本的資料時,因為後端接收到的是 zero value,所以不會執行清空的動作
根據單一欄位
upsert 的欄位需要是 unique
的才能,假設 upsert 時要根據的欄位只有一個,只需要將該欄位改成 unique
後即可:
type Product struct {
ID uuid.UUID `gorm:"primaryKey;type:uuid;default:uuid_generate_v4()"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
Name string
Price uint
SN uint `gorm:"unique"`
Comment string
}
接著就可以根據這個欄位來決定要不要 Upsert:
⚠️ 如果希望被刪除的資料事後 Upsert 時可以復原,則在
Columns
中要加入deleted_at
。如果希望updated_at
在 upsert 時會一併更新,也要加在Columns
中。
func (d *GormDatabase) UpsertProduct(product *model.Product) error {
if err := d.DB.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "sn"}}, // 根據 sn 這個欄位判斷要不要 upsert
DoUpdates: clause.AssignmentColumns([]string{
"price", "comment", "name", "updated_at",
}), // 要更新的欄位
}).Create(&product).Error; err != nil {
return err
}
return nil
}
⚠️ 在
clause.AssignmentColumns()
有列出的欄位名稱都會被儲存,因此若沒給值的話,原本的值會被清空。
也可以直接使用 UpdateAll: true
:
// Update all columns expects "primary keys" to new value on conflict
db.Clauses(clause.OnConflict{
UpdateAll: true
}).Create(&users)
根據多個欄位
若 upsert 時要根據的欄位不只一個時,需要將多個欄位建立出一個 unique index
,舉例來說,想要同時根據 name
和 sn
來做 upsert 的方法。
model 的部分,會把 Name 和 SN 又定義相同的 uniqueIndex
(即,gorm:"uniqueIndex:name_sn"
):
type Product struct {
ID uuid.UUID `gorm:"primaryKey;type:uuid;default:uuid_generate_v4()"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
Name string `gorm:"uniqueIndex:name_sn"`
Price uint
SN uint `gorm:"uniqueIndex:name_sn"`
Comment string
}
接著就可以同時根據這兩個欄位來做 upsert:
func (d *GormDatabase) UpsertProduct(product *model.Product) error {
if err := d.DB.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "sn"}, {Name: "name"}}, // 同時根據兩個欄位
DoUpdates: clause.AssignmentColumns([]string{"price", "comment"}), // 要更新的欄位
}).Create(&product).Error; err != nil {
return err
}
return nil
}
需要兩個欄位同時都一樣時才會執行 Upsert,只有一個一樣不會 Upsert。
如果希望只 upsert 有 提供的欄位
使用者種方式時,要留意 zero value 的情況,也就是如果使用者想要把該欄位清空時,因為清空時該欄位值會是 zero value,可能會更新不到
// UpsertDevice returns the client for the given id or nil.
func (d *GormDatabase) UpsertDevice(deviceInfo *model.Device) error {
device := model.Device{}
if err := d.DB.
Where("box_device_id = ? AND nis_patient_id = ?", deviceInfo.BoxDeviceID, deviceInfo.NISPatientID).
Assign(deviceInfo).
FirstOrCreate(&device).Error; err != nil {
return err
}
if err := d.DB.Take(deviceInfo, device.ID).Error; err != nil {
return err
}
return nil
}
Model
慣例(conventions)
在 GORM 中,慣例上會:
- 以
ID
當作 PK- 如果在一個 model 中設定多個
primaryKey
則會是 Composite Primary Key。
- 如果在一個 model 中設定多個
- 以複數的
snake_cases
當作 table name,例如product_tags
- 以
snake_case
當作欄位的名稱,例如is_publish
- 使用
CreatedAt
和UpdatedAt
來追蹤新增和修改的時間
覆蓋掉預設的資料表名稱
keywords: Tabler
, TableName
透過實作 Tabler interface 中的 TableName() string
方法,可以修改 GORM 在 auto migrate 時建立的 Table 名稱。例如下述程式碼最終會在 DB 中建立名為 blog_tag
的資料表:
type Tag struct {
// ...
}
// 實作 Tabler interface,最終會在 DB 中建立名為 `blog_tag` 的資料表
func (t Tag) TableName() string {
return "blog_tag"
}
透過
TableName() string
可以覆蓋預設的資料表名稱,但若是希望動態產生不同的資料表名稱則需使用db.Scopes()
來達到,參考這裡和 NamingStrategy。
如果想要修改預設的欄位名稱,可以在定義 struct 時使用 gorm:"column_name:foo_bar"
或使用 NamingStrategy。
gorm.Model
GORM 在定義 Model 時,可以使用 gorm.Model
,這預設會帶入 這些欄位到該 Model 中:
ID
,CreatedAt
UpdateAt
DeleteAt
type User struct {
gorm.Model
Author *string `json:"author"` // 字串是 "",數值是 0
Title *string `json:"name"` // 使用 pointer 的話,存到 database 的預設值會是 null,否則會是 zero value
Rating int `json:"rating"` // 注意這裡 rating 沒有用雙引號
}
如果想要自己定義接收 JSON 中屬性的名稱,記得要加上雙引號。
- 接收 JSON 屬性為
"author"
,存到 database 後,該欄位名稱也是author
,從 database 取出時亦會以小寫開頭 - 接收 JSON 屬性為
"name"
,存到 database 後,該欄位名稱是title
,從 database 取出時亦會以小寫開頭 - 這裡
rating
沒有用雙引號,從 database 取出時會以「大寫」開頭,即Rating
Embedded Struct
如果需要在 Model 中直接嵌入(embedded)另一個 struct,而並非透過「參照」的方式指稱到另一個 model,可以使用 embedded
。
anonymous fields
// 程式來源:https://gorm.io/docs/models.html#Embedded-Struct
type User struct {
gorm.Model
Name string
}
// equals
type User struct {
ID uint `gorm:"primaryKey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
Name string
}
嵌入自己定義的 Struct
keywords: gorm:"embedded"
如果是想要 embedded 其他 struct,而不是要當成 has_one 或 belongs_to 這種關聯式 Model 使用的話,也可以加上 gorm:"embedded"
使用:
也就是說,可以直接透過 Blog.Name
取得作者的名稱,而不是 Blog.Author.Name
:
// 程式來源:https://gorm.io/docs/models.html#Embedded-Struct
type Author struct {
Name string
Email string
}
type Blog struct {
ID int
Author Author `gorm:"embedded"`
Upvotes int32
}
// equals
type Blog struct {
ID int64
Name string
Email string
Upvotes int32
}
在嵌入的欄位加上前綴
keywords: embeddedPrefix
若有需要在嵌入時為每個 struct 加上前綴的話,則可以加上 gorm:"embedded;embeddedPrefix:foo_"
的用法:
如此要原本 Author 欄位嵌入到 Blog 中後,會變成 AuthorName
和 AuthorEmail
:
// 程式來源:https://gorm.io/docs/models.html#Embedded-Struct
type Blog struct {
ID int
Author Author `gorm:"embedded;embeddedPrefix:author_"`
Upvotes int32
}
// equals
type Blog struct {
ID int64
AuthorName string
AuthorEmail string
Upvotes int32
}