美文网首页go
Gorm的使用心得和一些常用扩展

Gorm的使用心得和一些常用扩展

作者: 海之方 | 来源:发表于2019-07-13 14:49 被阅读36次

    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

    相关文章

      网友评论

        本文标题:Gorm的使用心得和一些常用扩展

        本文链接:https://www.haomeiwen.com/subject/ldxmkctx.html