[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。
一對多(has many, belongs to)
情境
一個 User 有多個 Product,而一個 Product 只屬於一個 User。
Model 定義
User has many products
hasMany.go @ gist
- 使用
Products []Product
表示 User 有很多 Product - 如果沒有 Products 不希望顯示空陣列,可以在 json tag 中使用
omitempty
type User struct {
Name string `json:"name"`
Products []Product `json:"products"`
}
type Product struct {
Name string `json:"name"`
UserID uuid.UUID `json:"userId"`
User User `json:"user"`
}
Product belong to User
在 Product
model 需要
- 有
UserID
作為 foreign key(預設) - 有
User
Model 表示 belongs to,型別使用*User
搭配 json struct field tag 的omitempty
則可以在 Product 沒有 User 撈出資料時不要顯示此欄位
type Product struct {
Name string `json:"name"`
UserID uuid.UUID `json:"userId"`
User *User `json:"user,omitempty"`
}
CRUD 使用
Read: 使用 Preload 將 User 中關聯的 Product 取出
- 使用
db.Preload(clause.Associations)
可以一此把所有和 User 相關連的欄位(即,Product)一併撈出 - 使用
db.Preload("Products"
則可以指定要一併撈出的欄位名稱)
// database/user.go
func (d *GormDatabase) GetUserByID(userID uuid.UUID) (*model.User, error) {
user := &model.User{}
if err := d.DB.Preload(clause.Associations).Take(&user, userID).Error; err != nil {
return nil, err
}
return user, nil
}
取得的結果會像這樣:
{
"name": "aaron",
"products": [
{
"name": "Product 2",
"price": "200",
"sn": 2,
"comment": "Second Product",
"userId": "61ecf82b-8385-4606-83e9-70e13a4cdb5d"
},
{
"name": "Product 3",
"price": "300",
"sn": 3,
"comment": "Third Product",
"userId": "61ecf82b-8385-4606-83e9-70e13a4cdb5d"
}
]
}
Create
建立資料的時候,只需要把 User 中的 Product 帶入後即會自動建立和 User 相關連的 Product:
// database/user.go
// CreateUser 會自動把 nested 的 Product 一併建立起來
func (d *GormDatabase) CreateUser(user *model.User) error {
return d.DB.Create(user).Error
}
使用時直接把和 User 有關聯了 Products 放入即可:
// api/user.go
func TestUserHasManyProducts() {
assertWithT := assert.New(s.T())
mockUser := mock.GetUser()
mockProducts := mock.GetProductsWithoutUser()
// 直接把和 User 有關聯了 Products 放入即可
mockUser.Products = []model.Product{
mockProducts[0],
mockProducts[1],
}
/* create patient with contacts */
assertWithT.NoError(s.db.CreateUser(&mockUser))
}
Update
使用
Update
要小心 zero value 不會被更新的。
UpdateUser 時,當 user 中的 Product(下面的 id
是指 Product
的 ID):
- 沒有帶 id 時會自動建立新的
- 有帶 id 但此 id 不存在時,會自動建立新的
- 有帶 id 且此 id 存在時,會更新此 product 的 UserID
// database/user.go
func (d *GormDatabase) UpdateUser(user *model.User) error {
return d.DB.Updates(user).Error
}
如果想要在更新 User 時一併更新 nested 的 Product 可以自己判斷:
- 使用
db.Updates()
的話,User 或 Contact 中的 zero value 都不會更新到 - 使用
db.Save()
的 話,雖然所有 zero value 都會處理得到,但在 save contact 的時候,Contact 欄位中一定要提供 UserID,不然 - 會出現 foreign key error
// database/user.go
// SaveUserAndProduct 會更新所有欄位(包含 zero value)
func (d *GormDatabase) SaveUserAndProduct(userToBeUpdate *model.User) (*model.User, error) {
// 最外層用 Transaction 包起來
err := d.DB.Transaction(func(tx *gorm.DB) error {
// 移除 products(原本有,後來沒有的,要刪除)
user := &model.User{}
if err := d.DB.Preload(clause.Associations).Take(user, userToBeUpdate.ID).Error; err != nil {
return err
}
productToBeDelete := filter(user.Products, func(c model.Product) bool {
return !contains(userToBeUpdate.Products, c)
})
if len(productToBeDelete) > 0 {
if err := d.DB.Delete(&productToBeDelete).Error; err != nil {
return err
}
}
// 更新 products(原本有,後來也有的,要更新)
user = &model.User{}
if err := d.DB.Preload(clause.Associations).Take(user, userToBeUpdate.ID).Error; err != nil {
return err
}
productToBeUpdate := filter(userToBeUpdate.Products, func(c model.Product) bool {
return contains(user.Products, c)
})
if len(productToBeUpdate) > 0 {
for _, c := range productToBeUpdate {
err := d.DB.Save(c).Error
if err != nil {
return err
}
}
}
// 更新 user 並新增不存在的 product
// Product 中一定樣提供 UserID 才能成功建立 Product
if err := d.DB.Save(userToBeUpdate).Error; err != nil {
return err
}
return nil
})
if err != nil {
return nil, err
}
updatedUser := &model.User{}
if err := d.DB.Preload(clause.Associations).Take(updatedUser, userToBeUpdate.ID).Error; err != nil {
return nil, err
}
return updatedUser, nil
}
// utils
func filter(searchElement []model.Contact, handler func(contact model.Contact) bool) []model.Contact {
n := 0
for _, element := range searchElement {
if handler(element) {
searchElement[n] = element
n++
}
}
return searchElement[:n]
}
func contains(searchElement []model.Contact, target model.Contact) bool {
for _, element := range searchElement {
if element.ID == target.ID {
return true
}
}
return false
}