業務的upsert
? 在寫業務時,大家一開始都會以順序流程的方式開始著手寫代碼,CR時再看代碼,會有不一樣的感覺。
1. 需求描述
? 現有一張數據庫表,表字段結構如下:
字段名稱 | 類型 | 描述 |
---|---|---|
uuid | string | 數據的唯一鍵 |
data | string | 業務數據 |
version | int | 請求時業務的版本好 |
checksum | string | data 內容的哈希值 |
updated_at | time.Time | 更新時間 |
? 需求: 若干個請求方會調用接口并帶有該次請求的版本號來更新數據
? 更新規則:
- 數據的
checksum
不一致時,更新數據 - 當
version
版本號為0時,默認要更新數據; 當version
版本號大于db中版本號時, 更新數據
2. 需求實現
? 該部分會根據業務場景不斷演化
2.1 簡單實現
? 一開始看到這個需求, 很簡單啊, 有手就行!
type BusinessData struct {ID int64 `gorm:"column:id;type:int(11);primary_key;AUTO_INCREMENT"`UUID string `gorm:"column:uuid;type:varchar(255);uniqueIndex:uniq_uuid"`Checksum string `gorm:"column:checksum;type:varchar(255)"`Version int `gorm:"column:version;type:int(11);default:0"`CreatedAt time.Time `gorm:"column:created_at;type:datetime(3);default:CURRENT_TIMESTAMP(3)"`UpdatedAt time.Time `gorm:"column:updated_at;type:datetime(3);update:CURRENT_TIMESTAMP(3)"`
}func GetDbData(uuids []string) ([]*BusinessData, error) {var (res []*BusinessDataerr error)dsn := "user:password@tcp(localhost:3306)/database_name?charset=utf8mb4&parseTime=True&loc=Local"db, oErr := gorm.Open(mysql.Open(dsn), &gorm.Config{})if oErr != nil {fmt.Println(oErr)return nil, oErr}err = db.Table("business").Where("uuid IN ?", uuids).Find(&res).Errorif err != nil {fmt.Println(err)return nil, err}return res, nil
}func UpsertData(data []*BusinessData) error {dsn := "user:password@tcp(localhost:3306)/database_name?charset=utf8mb4&parseTime=True&loc=Local"db, oErr := gorm.Open(mysql.Open(dsn), &gorm.Config{})if oErr != nil {fmt.Println(oErr)return oErr}err := db.Table("business").Clauses(clause.OnConflict{Columns: []clause.Column{{Name: "uuid"},},DoUpdates: clause.AssignmentColumns([]string{"data", "version", "checksum", "updated_at"}),}).Create(data).Errorif err != nil {return err}return nil
}func main() {var (datas []*BusinessDatatoUpsertDataList []*BusinessDatauuids []string)for _, data := range datas {uuids = append(uuids, data.UUID)}dbDatas, err := GetDbData(uuids)if err != nil {fmt.Println(err)return}existDataMap := make(map[string]*BusinessData)for _, dbData := range dbDatas {existDataMap[dbData.UUID] = dbData}for _, data := range datas {if _, ok := existDataMap[data.UUID]; !ok {toUpsertDataList = append(toUpsertDataList, data)continue}if data.Checksum == existDataMap[data.UUID].Checksum {continue}if data.Version != 0 && data.Version < existDataMap[data.UUID].Version {continue}toUpsertDataList = append(toUpsertDataList, data)}err = UpsertData(toUpsertDataList)if err != nil {fmt.Println(err)}
}
? 代碼很簡單,讀取db中的數據到內存, 根據checksum和version的要求進行過濾, 符合條件的data便進行更新/插入
2.2 進階實現
2.2.1 思考1
? 將db中的數據讀到內存后進行uuid匹配, 是不是跟upsert時利用唯一鍵沖突更新數據重復了呢???
2.2.2 思考2
? 難道說利用唯一鍵沖突就是滿足更新的所有條件嗎?
? 在db進行update時,我們使用where來過濾數據, 那么在upsert時是不是也可以通過什么手段過濾數據呢???
將兩個簡單思考進行實現(這里只展示UpsertData方法)
err := db.Table("business").Clauses(clause.OnConflict{Columns: []clause.Column{{Name: "uuid"}},DoUpdates: clause.Assignments(map[string]interface{}{"checksum": gorm.Expr("IF((version <= VALUES(version) OR VALUES(version) = 0) AND checksum != VALUES(checksum), VALUES(checksum), checksum)"),"updated_at": gorm.Expr("IF((version <= VALUES(version) OR VALUES(version) = 0) AND checksum != VALUES(checksum), VALUES(updated_at), updated_at)"),"version": gorm.Expr("IF((version <= VALUES(version) OR VALUES(version) = 0) AND checksum != VALUES(checksum), VALUES(version), version)"),}),}).Create(data).Error
是不是大功告成! 準備完結撒花🎉了
ON DUPLICATE KEY UPDATE 語序會有影響結果
也就是說, 按順序執行時, 先更新checksum(正常), 再更新updated_at和version(異常)
異常原因: checksum更新后, IF條件中checksum == VALUES(checksum) 成立
2.2.3 思考3
? 那這樣的話, 我改變語序不就好了嗎, 直接就是完結撒花🎉!
? 那假如,我有很多個字段,我一個一個調試看看該字段會不會影響其他字段太浪費時間了, 那有沒有什么辦法能夠記錄快照狀態呢
? 對, 快照!
我們把當前的條件字段
變成快照狀態, 用快照進行條件判斷,就不用擔心語序問題了
開始實現:
condition := "(version <= VALUES(version) OR VALUES(version) = 0) AND checksum != VALUES(checksum)"err := db.Table("business").Clauses(clause.OnConflict{Columns: []clause.Column{{Name: "uuid"},},DoUpdates: []clause.Assignment{{Column: clause.Column{Name: "version"},Value: gorm.Expr(fmt.Sprintf("IF(@should_update := %s, VALUES(%s), %s)", condition, "version", "version")),},{Column: clause.Column{Name: "checksum"},Value: gorm.Expr(fmt.Sprintf("IF(@should_update := %s, VALUES(%s), %s)", condition, "checksum", "checksum")),},{Column: clause.Column{Name: "updated_at"},Value: gorm.Expr(fmt.Sprintf("IF(@should_update := %s, VALUES(%s), %s)", condition, "updated_at", "updated_at")),},},}).Create(data).Error
美化一下
// UpsertExprs _
func UpsertExprs(condition string, columns []string) clause.Set {if len(columns) < 1 {return clause.Set{}}var assignments []clause.Assignmentfor i, column := range columns {if i == 0 {assignments = append(assignments, clause.Assignment{Column: clause.Column{Name: column},Value: gorm.Expr(fmt.Sprintf("IF(@should_update := %s, VALUES(%s), %s)", condition, column, column)),})continue}assignments = append(assignments, clause.Assignment{Column: clause.Column{Name: column},Value: gorm.Expr(fmt.Sprintf("IF(@should_update, VALUES(%s), %s)", column, column)),})}return assignments
}func UpsertData(data []*BusinessData) error {dsn := "user:password@tcp(localhost:3306)/database_name?charset=utf8mb4&parseTime=True&loc=Local"db, oErr := gorm.Open(mysql.Open(dsn), &gorm.Config{})if oErr != nil {fmt.Println(oErr)return oErr}condition := "(version <= VALUES(version) OR VALUES(version) = 0) AND checksum != VALUES(checksum)"err := db.Table("business").Clauses(clause.OnConflict{Columns: []clause.Column{{Name: "uuid"},},DoUpdates: UpsertExprs(condition, []string{"version", "checksum", "updated_at"}),}).Create(data).Errorif err != nil {return err}return nil
}
那就真的完結撒花捏🎉