# 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