Gorm 的使用心得和一些常用擴充套件 (一)

jeremy1127發表於2019-09-12

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 != ''

Gorm的使用心得和一些常用擴充套件(二)

程式碼地址:Github:Ksloveyuan/gorm-ex 歡迎Start~

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章