Gorm是golang的一個orm框架,它提供了對資料庫操作的封裝,使用起來相當便利。
但在專案開發中,程式碼寫的多了,還是發現在它之上還是有再次封裝的空間,比如說新增錯誤日誌、或者是一些使用頻率非常高的對單個表的條件查詢、分頁查詢、資料更新等。再則是,關於相同的功能操作,gorm也提供多種實現方式,對新學多少有些困惑,不知道該用哪個好。
於是,我基於自己在專案中的使用經驗和編碼習慣,做了如下一些擴充套件,供大家參考。
準備
為了相容gorm的使用方法,我使用了內嵌型別來擴充套件。 定義如下:
type DBExtension struct {
*gorm.DB
logger DBLogger
}
這樣子定義的wrapper物件是最小侵入式的擴充套件,不僅可以直接點出gorm的原有方法,也可以點出擴充套件的方法。
新增
關於新建資料,我建議使用Save方法,當匹配主鍵的資料不存在時,它的效果是插入一條新資料,而當匹配主鍵的資料存在時,則更新全部欄位,再說一遍,它會更新全部欄位!
無論欄位是否做了修改或者是否是定義型別的預設值。
請再次注意:預設值是否生效在gorm的不同方法中處理的方式是不一樣的,需要非常小心才行。
舉個例子,如果你定義了一個User的結構體,裡面有個Age的欄位型別是int。(注:以後的例子,都預設已定義這個結構體)
type User struct {
Id int `gorm:"column:id; type:int(11);primary_key"`
Name string `gorm:"column:name; type:varchar(32);"`
Age int `gorm:"column:age; type:int(11);"`
Description string `gorm:"column:description; type:varchar(512);"`
}
func (User) TableName() string {
return "test.user"
}
++請特別注意Id的定義中的primary_key, 如果沒有加個這個Save方法是無法正常工作的。++
如果在定義時,沒有給Age賦值,那麼這條資料的Age將被置為0。
對於新增資料,可能問題不大,但是對於資料更新,那這就可就是一個隱晦的bug了!
那既然Save方法有這樣一個坑,為什麼還要用它呢?
簡單來說,不用顯示的判斷是新增資料和更新資料,可以讓程式碼更加簡潔,利大於弊,不是嗎?
擴充套件程式碼如下,增加了一些錯誤判斷和日誌:
type TableNameAble interface {
TableName() string
}
// Update All Fields
func (dw *DBExtension) SaveOne(value TableNameAble) error {
tableNameAble, ok := value.(TableNameAble)
if !ok {
return errors.New("value doesn't implement TableNameAble")
}
var err error
if err = dw.Save(value).Error; err != nil {
dw.logger.LogErrorc("mysql", err, fmt.Sprintf("Failed to save %s, the value is %+v", tableNameAble.TableName(), value))
}
return err
}
使用程式碼如下:
user1 := User{Id:1, Name:"Jeremy", Age: 30, Description: "A gopher"}
if err := dw.SaveOne(&instInfo); err != nil{
// error handling
return err
}
當記錄不存在時,執行的Sql語句是:
insert into test.user(id ,name, age, description) values(1, "Jeremy", 30, "A gopher")
當記錄存在時,執行的語句就是:
update test.user set name = "Jeremy", age = 30, description = "A gohper" where id = 1
這樣寫新建,還兼顧了全欄位更新的情況,是不是一舉兩得呢?
PS: 如果主鍵Id是一個自增列,在新建時,可以不用給Id賦值。當資料成功插入後,這條資料的Id還會自動更新到Id欄位,這個特性在一些場景下特別有用。
SaveOne方法是全量更新,但大部分情況是,可能只是更新某條資料的部分欄位,又或者是隻想更新改過的欄位。關於這部分操作,gorm雖然提供了很多操作方法,但也是最讓人困惑的。
在這種場景我常用的處理方式有兩種,一是定義一個專門的結構體,如:
type UserDesc struct {
Id int `gorm:"column:id; type:int(11);primary_key"`
Description string `gorm:"column:description; type:varchar(512);"`
}
func (UserDesc) TableName() string {
return "test.user"
}
這時就可以使用SaveOne方法,用如下方式更新:
userDesc := UserDesc{Id:1, Description: "A programmer"}
if err := dw.SaveOne(&userDesc); err != nil{
// error handling
return err
}
執行的sql語句是:
update test.user set description = "A programmer" where id = 1
但是更多的時候,是想按匹配條件更新的匹配的資料,這時SaveOne就無法滿足了。於是,我做了如下擴充套件:
const table_name = "$Table_Name$"
type UpdateAttrs map[string]interface{}
func NewUpdateAttrs(tableName string) UpdateAttrs {
attrMap := make(map[string]interface{})
attrMap[table_name] = tableName
return attrMap
}
// Update selected Fields, if attrs is an object, it will ignore default value field; if attrs is map, it will ignore unchanged field.
func (dw *DBExtension) Update(attrs interface{}, query interface{}, args ...interface{}) error {
var (
tableNameAble TableNameAble
ok bool
tableName string
)
if tableNameAble, ok = query.(TableNameAble); ok {
tableName = tableNameAble.TableName()
}else if tableNameAble, ok = attrs.(TableNameAble); ok {
tableName = tableNameAble.TableName()
} else if attrMap, isUpdateAttrs := attrs.(UpdateAttrs); isUpdateAttrs {
tableName = attrMap[table_name].(string)
delete(attrMap, table_name)
}
if tableName == "" {
return errors.New("can't get table name from both attrs and query")
}
var err error
db := dw.Table(tableName).Where(query, args...).Update(attrs)
if err = db.Error; err != nil {
dw.logger.LogErrorc("mysql", err, fmt.Sprintf("failed to update %s, query is %+v, args are %+v, attrs is %+v", tableName, query, args, attrs))
}
if db.RowsAffected == 0 {
dw.logger.LogWarnc("mysql",nil, fmt.Sprintf("No rows is updated.For %s, query is %+v, args are %+v, attrs is %+v", tableName, query, args, attrs))
}
return err
}
下面,我將結合Sql語句,逐一解釋如何使用。
還是先以要執行下面這條語句為例:
update test.user set description = "A programmer" where id = 1
現在,可以有如下幾種實現方式
- 寫法一
udateAttrs := User{Description: "A programmer"}
condition := User{Id: 1}
if err := dw.Update(&udateAttrs, condition); err != nil{
// error handling
return err
}
-
寫法二
udateAttrs := User{Description: "A programmer"} if err := dw.Update(&udateAttrs, "id = ?", 1); err != nil{ // error handling return err }
-
寫法三
udateAttrs := NewUpdateAttrs("test.user") udateAttrs["description"] = "A programmer" if err := dw.Update(&udateAttrs, "id = ?", 1); err != nil{ // error handling return err }
-
寫法四
udateAttrs := NewUpdateAttrs("test.user")
udateAttrs["description"] = "A programmer"
condition := User{Id: 1}
if err := dw.Update(&udateAttrs, condition); err != nil{
// error handling
return err
}
咋一看,四種寫法很相似。那麼,為什麼要搞這麼多種寫法呢?
這可是不是為了炫耀回字的幾種寫法, 而是因為gorm原生的Update方法對於struct的引數是會忽略預設值的。
比如說,如果你想把descritpion清空,如果像下面這樣寫:
udateAttrs := User{Description: ""}
condition := User{Id: 1}
if err := dw.Update(&udateAttrs, condition); err != nil{
// error handling
return err
}
descritpion是不會被更新的,這是就需要寫法三或者寫法四了,以寫法四為例
udateAttrs := NewUpdateAttrs("test.user")
udateAttrs["description"] = ""
condition := User{Id: 1}
if err := dw.Update(&udateAttrs, condition); err != nil{
// error handling
return err
}
才會如願執行:
update test.user set description = "" where id = 1
而寫法二(三)的強大之處在於可以更自由的指定匹配條件,比如:
udateAttrs := User{Description: "A programmer"}
if err := dw.Update(&udateAttrs, "id in (?) and age > ? and description != ?", []int{1,2}, 30, ""); err != nil{
// error handling
return err
}
執行的sql是:
update test.user set description = "A programmer" where id in (1,2) and age > 30 and description != ''
程式碼地址:Github:Ksloveyuan/gorm-ex 歡迎Start~
本作品採用《CC 協議》,轉載必須註明作者和本文連結