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 != ''
未完待续……