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

Table of Contents

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