59

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

 5 years ago
source link: https://www.tuicool.com/articles/VJRbQzU
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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 = "" where id in (1,2) and age > 30 and description != ''

未完待续……

代码地址: Github:Ksloveyuan/gorm-ex


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK