# gORM ###### tags: `gorm` ## Read list 1. [Returning Data From Modified Rows](https://gorm.io/docs/update.html#Returning-Data-From-Modified-Rows) 2. [Check Field has changed](https://gorm.io/docs/update.html#Check-Field-has-changed) 3. [Optimizer/Index Hints](https://gorm.io/docs/advanced_query.html#Optimizer-x2F-Index-Hints) 4. [Locking (FOR UPDATE)](https://gorm.io/docs/advanced_query.html#Locking-FOR-UPDATE) 5. [[Note] GORM 筆記 | PJCHENder 未整理筆記](https://pjchender.dev/golang/note-gorm/) - Hook - skip hook `Session{SkipHooks: true}` - `BeforeSave`, `BeforeCreate`, `AfterSave`, `AfterCreate` - 順序 1. `.Create(&obj)` 2. call `obj.Before_()` method with object data 3. call `obj.After_()` method with object data 4. execute Insert statement ## 坑的紀錄 - `Where()` 帶入 nil 的 pointer variable,SQL 語法會是 `col = NULL` - e.g. `Where("is_enable = ?", nil)` → `WHERE is_enable = NULL` - `Save()` 的 Entity PK 為預設值的話,會有不預期的執行動作(TODO: 測試該項目) - `Updates()` 的 Entity PK 為預設值的話,並不會將 PK 帶入 WHERE - e.g. `tx.Updates(&User{ID: 0, Age: 10})` → `UPDATE users SET age = 10` - 解決方法 - 手動帶入 `tx.Where("pk = ?", pk)` - `First()` - 當找不到資料時,會存錯誤訊息在 `result.Error` 欄位(`ErrRecordNotFound`) - 解決方法/替代方案 1. 使用 `Limit(1).Find()` 2. 使用 `result.RowAffected == ?` ## Create <aside> 💡 When creating from `map`, hooks won’t be invoked, associations won’t be saved and primary key values won’t be back filled. </aside> 1. 回傳建立數量 1. `RowsAffected` 2. 限定特定欄位建立 1. `Select("col").Create(&obj)` 3. 移除特定欄位建立 1. 移除欄位 `Omit("col").Create()` 2. 移除關聯 `Omit("CreditCard").Create(&user)` 3. 移除所有關聯 `Omit(clause.Associations).Create(&user)` 4. 分批建立 1. 建立大量資料時使用 2. `CreateInBatches(objs, 100)` batch size 100 3. 設定分批建立 1. 初始化設定 `gorm.Config{CreateBatchSize: 100}` 2. 使用 Session `Session(&gorm.Session{CreateBatchSize: 100})` 5. 連帶建立關聯(With Associations) - ⚠️ if its associations value is not zero-value, those associations will be upserted, and its `Hooks` methods will be invoked 6. With SQL expression [#Link](https://gorm.io/docs/create.html#Create-From-SQL-Expression-x2F-Context-Valuer) 7. `Upsert` / `On Conflict` [#Link](https://gorm.io/docs/create.html#Upsert-x2F-On-Conflict) ```go tx.Clauses(clause.OnConflict{ Columns: []clause.Column{{Name: "id"}}, DoUpdates: clause.Assignments(map[string]interface{}{"role": "user"}), }).Create(&users) ``` - `clause.OnConflict` Fields: ```go // 覆蓋該欄位值 DoUpdates: clause.AssignmentColumns([]string{"account", "is_enable"}) // 覆蓋該欄位值(支援使用 functions) DoUpdates: clause.Assignments(map[string]interface{}{"account": gorm.Expr("func(?)")}) // 覆蓋所有值 UpdateAll: true, ``` ## Get & Query ### 單一搜尋 - `First()` - `ORDER BY id LIMIT1` - 找不到資料時,會回傳錯誤`ErrRecordNotFound` - `Limit(1).Find()` - `Take()` - `Limit 1` - `Last()` - `ORDER BY id DESC LIMIT 1` - 其他 - `FirstOrInit()` [#Link](https://gorm.io/docs/advanced_query.html#FirstOrInit) - `FirstOrCreate()` [#Link](https://gorm.io/docs/advanced_query.html#FirstOrCreate) ### 搜尋 By WHERE - 使用 `struct` & `map` [#Link](https://gorm.io/docs/query.html#Struct-amp-Map-Conditions) - `Where(&User{Age: 1}).First(&user)` → `SELECT * users WHERE age = 1` <aside> 💡 When querying with struct, GORM will only query with non-zero fields. #除非使用 `map` </aside> - `Where([]int64{1, 2}).Find(&users)` → `SELECT * FROM users WHERE id IN (1, 2);` - Group rows ```go rows, err := tx.Model().Rows() defer rows.Close() for rows.Next(){ var user User tx.ScanRows(rows, &user) } ``` - `IN` with multiple columns ```go tx.Where( "(a, b) IN ?", [][]interface{}{ {"1-1", "1-2"}, {"2-1", "2-2"}, }). Find(&users) // SELECT * FROM users WHERE (a, b) IN (("1-1", "1-2"), ("2-1", "2-2")); ``` - 其他 - `Not` condition [#Link](https://gorm.io/docs/query.html#Specify-Struct-search-fields) - `Or` condition [#Link](https://gorm.io/docs/query.html#Or-Conditions) ## Update 1. 使用 `Update()` 1. 僅更新單一欄位 2. `tx.Model().Where().Update("col", "value")` 3. 支援運算帶入`tx.Model(&user).Update("salary", gorm.Expr("salary * ?", 2))` 4. 可使用 `Model(&obj)` 自動抓取 obj PK value 加入條件判斷。 1. ⚠️ 當出現 default value 則並不會帶入。 2. 使用 `Updates()` 1. 支援 `struct`, `map` 2. ⚠️ 當 Entity 的 PK 為 default value 時,並不會將 PK 自動帶入 `WHERE` 1. 解決辦法 1. 使用 `Where("pk = ?", pk)`。儘管 PK 為 0 也能帶入條件 2. 更新前先檢查該 Entity PK 是否為 default value 3. 當 Entity 的欄位為 default value 時,並不會更新該欄位 1. 解決辦法 1. 使用 `Select()`來指定更新的欄位,或是更新所有欄位。 1. 可使用 `*` 來更新 entity 中的所有欄位 2. 可僅更新特定欄位 + Hook ```go tx.Select("is_enable", "age").Where("id = ?", 1).Updates(&obj) ``` 2. 使用 `map[string]interface{}` 帶入,會連同 default value 的欄位也會自動更新。 ```go tx.Where("id = ?", 1).Updates(map[string]interface{}{ "is_enable": true, }) ``` 3. Entity 該欄位屬性改成 pointer(`*`),當該欄位不為 nil 則可更新該欄位。 ```go type Entity struct{ ID int64 `gorm:"column:id"` IsEnable *bool `gorm:"column: is_enable"` } tx.Where("id = ?", 1).Updates(&Entity{ IsEnable: req.IsEnable, }) ``` 4. ❗(待驗證)使用 `Save()` 更新 entity 所有欄位 1. 當 PK 為 default value 照樣建立/更新 `WHERE pk = 0` 2. 當 PK 已存在時,會更新該筆資料 3. 建議使用前先抓去舊資料,修改後再進行帶入。 4. 其他 1. 當 `Where(pk = ?, 1).Update(&User{PK: 2})`, 會以 `Where()` 的 PK 條件帶入而非 Entity 的 PK。→ `WHERE pk = 1` 3. 使用 `UpdateColumn()`, `UpdateColumns()` 1. 會將預設帶入的條件移除 2. e.g. 移除`is_del = 0`, `deleted_at IS NULL` 自動帶入條件 4. Error handle 1. 當 WHERE 無條件時,會出錯 `ErrMissingWhereClause` 5. 使用 SubQuery 帶入條件 ```go tx. Model(&user). Update( "company_name", tx. Model(&Company{}). Select("name"). Where("companies.id = users.company_id") ) ``` ```sql UPDATE users SET "company_name" = ( SELECT name FROM companies WHERE companies.id = users.company_id ); ``` ## Join 關聯 - Tag `gorm:"foreignKey:<PkOfEntity2>;references:<FkOfEntity1>"` - Method `Joins("FieldName")` - With condition`.Joins("Company", db.Where(&Company{Alive: true})` - Joins a Derived Table ```go query := tx.Table("").Joins("") tx.Model().Joins("", query) ``` - Method `Preload()` - 會拆分多 Query 連線 ## Transcation - 當 Rollback 後,下次進行Query 的話會出錯誤 - err = "transaction has already been committed or rolled back" ## Entity tags 1. Permission - `gorm:"<-:create"` : allow read and create - `gorm:"->;<-:create"` : allow read and create - `gorm:"<-:update"` : allow read and update - `gorm:"<-"` : allow read and write ( create and update) - `gorm:"<-:false"` : allow read, disable write permission - `gorm:"->"` : read only - `gorm:"->:false;<-:create"` : create only - `gorm:"-"` : ignore this field when write and read with struct 2. FK 1. `gorm:"foreignKey:<PkOfEntity2>;references:[FKOfEntity1]"` 3. SQL attribute - `gorm:"default:uuid_generate_v4()"` - `default:(-)`(if you want to skip a default value definition when migrating) --- ## 建議 1. 更新資料規定 1. 使用 `Updates()` 代替 `Save()` 2. 每使用更動資料的方法時,請加入 `Where()` 條件,非直接帶入 entity without where method 2. 取得資料規定 1. 使用 `Find()` + `Limit()` 代替 `First()` 3. 其他 1. 預設帶入條件 ```go Query default condition - .`is_del` = 0 - .`deleted_at` IS NULL Update default condition // 當無手動加入 WHERE 時,並不會報錯 - .`is_del` = 0 - .`deleted_at` IS NUL Delete default condition // 當沒加上 Where 時,儘管有預設 Where 還是會報錯(沒手動加上 Where),但帶入的 obj 的 PK 有值的話就能夠執行 - .`is_del` = 0 - .`deleted_at` IS NULL ``` --- ## 待測試 1. 使用 `Updates()` + `Select()` 特定欄位 + 放入 struct 會怎麼更新 1. 只更新該 `Select()` 欄位 及 預測的條件判斷 2. use Save with where and PK-> ? 3. test Updates and Save into the object(PK = 0) without the Where() 4. test Raw with SQL injection by request parameters. 5. `Save()` 的 Entity PK 為預設值的話,會有不預期的執行動作(TODO: 測試該項目) 6. 重複的 TX 1. 不能直接使用同一個 tx 進行分割 2. 可以使用避包回傳 TX 來解決 7. 更新可以連帶 foreign entity? 8. SavePoint, RollbackTo 9. add defer with rollback in transaction 10. [[link](https://gorm.io/docs/transactions.html%23A-Specific-Example)](https://gorm.io/docs/transactions.html#A-Specific-Example) ## Jimmy理解後的Gorm懶人包 - updates > 可以帶入物件更新但如果更新的欄位是預設值帶入**default** **(ex : int=0,tinyint=0)** 則無法更新 但如果欄位型別是 **pointer** ,帶入 **default** ,還是可以更新 map可以選擇要的欄位更新,要更新的欄位是default也沒差 - update > 一次只能針對一個欄位更新,遇到更新 **default** 值也不會有影響 - save > 更新資料前,如果沒有找到該筆資料,則會自動更新資料,也是會受到 **default** 值影響 - first與find > first如果沒有找到資料會有 **Error** ,find則不會,所以會建議一律使用find加上 **limit** ,要注意limit要在find前面 範例 : ```go var goal modelTable tb.MysqlDB.Where("id = ?", 123).Limit(1).Find(&goal) ``` - 補充 > golang nil.屬性一樣會報錯,panlic