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

Ksloveyuan · · 1401 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

Gorm是golang的一个orm框架,它提供了对数据库操作的封装,使用起来相当便利。 但在项目开发中,代码写的多了,还是发现在它之上还是有再次封装的空间,比如说添加错误日志、或者是一些使用频率非常高的对单个表的条件查询、分页查询、数据更新等。再则是,关于相同的功能操作,gorm也提供多种实现方式,对新学多少有些困惑,不知道该用哪个好。 于是,我基于自己在项目中的使用经验和编码习惯,做了如下一些扩展,供大家参考。 ## 准备 为了兼容gorm的使用方法,我使用了内嵌类型来扩展。 定义如下: ```golang type DBExtension struct { *gorm.DB logger DBLogger } ``` 这样子定义的wrapper对象是最小侵入式的扩展,不仅可以直接点出gorm的原有方法,也可以点出扩展的方法。 ## 新增 关于新建数据,我建议使用**Save**方法,当匹配主键的数据不存在时,它的效果是插入一条新数据,而当匹配主键的数据存在时,则**更新全部字段**,再说一遍,**它会更新全部字段**! **无论字段是否做了修改或者是否是定义类型的默认值**。 请再次注意:默认值是否生效在gorm的不同方法中处理的方式是不一样的,需要非常小心才行。 举个例子,如果你定义了一个User的结构体,里面有个Age的字段类型是int。(*注:以后的例子,都默认已定义这个结构体*) ```golang 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方法有这样一个坑,为什么还要用它呢? 简单来说,不用显示的判断是新增数据和更新数据,可以让代码更加简洁,利大于弊,不是吗? 扩展代码如下,增加了一些错误判断和日志: ```golang 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 } ``` 使用代码如下: ```golang 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虽然提供了很多操作方法,但也是最让人困惑的。 在这种场景我常用的处理方式有两种,一是定义一个专门的结构体,如: ```golang 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方法,用如下方式更新: ```golang 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就无法满足了。于是,我做了如下扩展: ```golang 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 ``` 现在,可以有如下几种实现方式 - 写法一 ```go udateAttrs := User{Description: "A programmer"} condition := User{Id: 1} if err := dw.Update(&udateAttrs, condition); err != nil{ // error handling return err } ``` - 写法二 ```go udateAttrs := User{Description: "A programmer"} if err := dw.Update(&udateAttrs, "id = ?", 1); err != nil{ // error handling return err } ``` - 写法三 ```go udateAttrs := NewUpdateAttrs("test.user") udateAttrs["description"] = "A programmer" if err := dw.Update(&udateAttrs, "id = ?", 1); err != nil{ // error handling return err } ``` - 写法四 ```go 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清空,如果像下面这样写: ```go udateAttrs := User{Description: ""} condition := User{Id: 1} if err := dw.Update(&udateAttrs, condition); err != nil{ // error handling return err } ``` descritpion是不会被更新的,这是就需要写法三或者写法四了,以写法四为例 ```go 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 ``` 而写法二(三)的强大之处在于可以更自由的指定匹配条件,比如: ```go 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的使用心得和一些常用扩展(二)](https://juejin.im/post/5d3093625188251b2569f10e) 代码地址:[Github:Ksloveyuan/gorm-ex](https://github.com/Ksloveyuan/gorm-ex) 欢迎Start~

有疑问加站长微信联系(非本文作者))

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

1401 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传