---
# System prepended metadata

title: gORM
tags: [gorm]

---

# 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