[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
}
Delete
要把與 User 關聯的 Product 一併刪除,只需要在刪除的方法中加上 db.Select(clause.Associations)
:
- 使用
db.Select(clause.Associations)
會直接把所有和 User 關聯的 nested model 一併刪除 - 使用
db.Select("Product")
則只會刪除和 User 關聯的 Product Model - 若希望保留相關連的 Model 而不要一併刪除,則不要使用
db.Select()
// database/user.go
func (d *GormDatabase) DeleteUserByID(userID uuid.UUID) error {
return d.DB.Select(clause.Associations).Delete(&model.User{
ID: userID,
}).Error
}
Method Chaining
keywords: method chaining
, immediate methods
, scope
Method Chaining @ GORM
把條件填入 SQL
可以使用 Method Chaining 的方式串連多個 where 條件,這些條件只有在 immediate method 被呼叫時才會產收 SQL query 並傳送到 database 中被執行。immediate method 多是 CRUD 時使用,例如:Create
, First
, Find
, Take
, Save
, UpdateXXX
, Delete
, Scan
, Row
, `Rows。
func (d *GormDatabase) GetActivitiesGroupByDate(begin int64, end int64) (
[]model.ActivityResp, error,
) {
var activities []model.ActivityDaily
// tx := d.DB.Where("")
tx := d.DB.Table("activities"). // 也可以寫 tx := d.DB.Model(&model.Activity{})
Select("patient_id, date_trunc('day', TO_TIMESTAMP(recorded_at)) AS date, SUM(count) as count")
if begin != 0 {
tx.Where("recorded_at >= ?", begin)
}
if end != 0 {
tx.Where("recorded_at <= ?", end)
}
// start query with finish method "Find"
tx.Group("patient_id, date").Find(&activities)
return activitiesResp, nil
}
把條件直接帶入 ORM
- 在
GetProducts
方法參數condition
可以接收許多的 condition - 搭配
.Where
的 method chaining .Where
第一個參數一定要給;第二個餐數則可以用陣列展開,因此根據條件數目(len(conditions)
)進行判斷
// ./database/product
// https://github.com/gotify/server/blob/master/database/user.go
func (d *GormDatabase) GetProducts(conditions ...interface{}) ([]*model.Product, error) {
var products []*model.Product
tx := d.DB.Model(&model.Product{})
if len(condition) == 1 {
tx.Where(conditions[0])
} else if len(condition) > 1 {
tx.Where(conditions[0], conditions[1:]...)
}
if err := tx.Find(&products).Error; err != nil {
return nil, err
}
return products, nil
}
在 API 的地方可以直接對於沒有要查詢的條件可以直接預設值:
// ./runner/product
type ProductQuery struct {
Name string
Price uint
SN uint
Comment string
}
func (r *ProductRunner) GetProducts(
qs ProductQuery,
) ([]*model.Product, error) {
// 如果 qs 中的欄位是 zero value 則不會進行查詢
products, err := r.DB.GetProducts(
&model.Product{
Name: qs.Name,
SN: qs.SN,
Price: qs.Price,
Comment: qs.Comment,
}
)
if err != nil {
return nil, err
}
return products, nil
}
使用時可以直接帶入 struct:
func main() {
// 直接建立 query 的 struct
qs := model.ProductQuery{
SN: 2,
Comment: "first",
}
// 直接把 qs 帶入即可查詢符合條件的資料
products, _ := productRunner.GetProducts(qs)
}
簡易的使用 where 串連條件
如果條件沒有太複雜,也可以直接使用 where 串連:
func getCount(t Tag, db *gorm.DB) (int64, error) {
var count int64
if t.Name != "" {
db = db.Where("name = ?", t.Name)
}
db = db.Where("state = ?", t.State)
err := db.Model(&t).Where("is_del = ?", 0).Count(&count).Error
if err != nil {
return 0, err
}
return count, nil
}