前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Go项目重构经验分享——ORM框架实践

Go项目重构经验分享——ORM框架实践

原创
作者头像
siri
发布2023-09-25 11:22:47
4200
发布2023-09-25 11:22:47
举报
文章被收录于专栏:siri的开发之路siri的开发之路

一、背景介绍

本文介绍了笔者在重构一个 Go 项目的实践经验,老项目由于迭代速度快,导致了接口杂乱,结构扁平,代码耦合度高等问题,在项目复杂度增加的情况下不再适合扩展,因此对整个项目进行了重构。篇幅有限,本文主要介绍 ORM 层的重构。

二、框架选择

项目的特点是重业务逻辑,且大多数逻辑依赖于数据库操作,相对地,对并发和性能的要求不是特别高

目前主流的 ORM 框架 gormxorm 在功能上都能满足项目的要求,xorm 的性能更高,接口设计简洁;gorm 提供了更多高级功能,如事务、预加载、回调、软删除等,且文档非常详细,缺点是由于内部使用了反射的原因会导致额外的性能开销。鉴于对性能要求不算很高,笔者选择的是功能丰富、文档详细的 gorm 框架。

三、ORM层目录结构

老项目的目录结构比较简单,重构后根据标准Go项目目录结构 推荐的方式重新整理,当前的目录结构大致如下:

代码语言:txt
复制
.
├── internal
│   ├── handler
│   ├── usecase
│   ├── repository
│   ├── model
│   └── router
├── cmd
│   └── main
├── configs
├── go.mod
├── tools
├── test

internal 包中的是非共享的核心代码,一些公共库代码在重构时被移入了其他开源公共库,因此这里没有额外创建 pkg 包

handler 包中为业务逻辑,router负责路由注册,剩下的 usecaserepository 以及 model 均为数据库相关的包

四、重构过程

gorm 推荐的方式,model

1. model层

model包下的代码原为数据库表映射的结构体,这部分手工生成既麻烦也容易出现错误,可以直接通过gorm提供的GEN 工具进行生成

通过 go get 引入库代码,调用库提供的脚本即可方便地生成model文件

代码语言:txt
复制
go get -u gorm.io/gen

GEN 生成的不仅是由表映射的结构体,还包括一些基本的增删查改的操作接口,这部分接口是否保留可以视具体项目而定

下面是通过 GEN 生成的一张表:

代码语言:txt
复制
// PubInfo mapped from table <pub_info>
type PubInfo struct {
    Id          NullInt64 `gorm:"column:id;type:bigint(20);primaryKey;autoIncrement:true" json:"id"`
    CreateTime  NullTime   `gorm:"column:create_time;type:datetime" json:"create_time"`   
    UpdateTime  NullTime   `gorm:"column:update_time;type:datetime" json:"update_time"`   
    Status      NullInt64  `gorm:"column:status;type:tinyint(4)" json:"status"`          
    Env         NullString `gorm:"column:env;type:varchar(128);default:dev" json:"env"`
    SType       NullInt64  `gorm:"column:stype;type:tinyint(4);default:(-)" json:"stype"` 
}

// TableName PubInfo's table name
func (PubInfo) TableName() string {
    return "pub_info"
}

这里并没有逻辑代码,但对于 gorm 的 tag 设置,在实际应用中有几点踩坑经历:

一是 gorm 对于 default 值的处理方式,如上面代码所示,Env 字段设置了默认值 dev,当调用插入接口时,如该值为空则会填写默认值,但这种情况只适用于所有数据库的默认值设置都相同的情况。如果想要在插入数据时使用数据库设置的默认值,需得在 tag 中设置 default:(-) ,如上述 SType字段,否则 gorm 会在插入时为其设置默认的零值(更加具体的解释可参考这篇文章

二是 gorm 可以声明默认的 update_time 和 create_time 字段,在 tag 中设置 autoUpdateTime 即可,在记录创建和更新的时候,gorm 会完成这两个字段的更新。但如果项目中有自己的设置规则,记得取消这两个字段的设置

代码语言:txt
复制
  CreateTime time.Time `gorm:"column:createtime;type:datetime(0);autoUpdateTime" json:"createtime"`
  UpdateTime time.Time `gorm:"column:updatetime;type:datetime(0);autoUpdateTime" json:"updatetime"`

三是 gorm 的表名默认使用结构体名的 蛇形命名 作为表名。对于结构体 PubInfo ,根据约定,其表名为 pubinfos,在 model 层可以通过重新设定表名来替换,实现 Tabler 接口来更改默认表名,例如:

代码语言:txt
复制
Copytype Tabler interface {
    TableName() string
}

// TableName 会将 PubInfo 的表名重写为 `pub_info`
func (PubInfo) TableName() string {
    return "pub_info"
}
2. repository层

repository 层包括对数据库的基础增删查改操作,不涉及任何业务逻辑,对于官方文档里有的介绍笔者就不进行搬运了, 用户也可以直接通过 GEN 工具进行生成,下面是笔者在实践过程中碰到的一些坑

  • 错误处理 由于是链式API,gorm的错误处理方式一般如下所示,通过设置 Error 字段来保存执行过程中的错误
代码语言:txt
复制
if err := db.Where("name = ?", "jinzhu").First(user).Error; err != nil {  
	// 处理错误
}

这一点本身并不迷惑,但在查询数据库中的对应记录时,如果使用 FirstTakeLast 方法从数据库中检索单个对象,当没有找到记录时,它会返回 ErrRecordNotFound 错误;但如果使用 Find 方法查询多条数据,如果没有命中纪录,其并不会返回 ErrRecordNotFound 错误,而这一点与老项目中数据库的处理逻辑是不兼容的,因此需要特别注意

  • 更新0值/非0值的字段:
代码语言:txt
复制
//当通过 struct 更新时,GORM 只会更新非零字段。 如果您想确保指定字段被更新,你应该使用 Select 更新选定字段,或使用 map 来完成更新操作
func UpdateSelective(user model.User) (effected int64, err error) {
	tx := db.Model(user).Updates(model.User{
	Id:    user.Id,
	Name:  user.Name,
	Age:   user.Age,
	Sex:   user.Sex,
	Phone: user.Phone,
	})
}

如果你想更新0值的字段,那么可以使用 Select 函数先选择指定的列名,或者使用 map 来完成:

代码语言:txt
复制
tx = constants.GVA_DB.Model(user).Updates(map[string]interface{}{
  "Id":    user.Id,
  "Name":  user.Name,
  "Age":   user.Age,
  "Sex":   user.Sex,
  "Phone": user.Phone,
})

gorm 中一定要严格注意零值和非零值的判断,否则可能出现意想不到的结果

3. usecase层

这一层主要是业务逻辑,业务逻辑相关代码都应该在这一层写,当然有时候代码可能就只是保存一下数据,直接封装调用一下 repository 层接口即可

因为是对原始代码进行重构,这一层需要注意的是多使用 Select(column1, column2) 方法指定需要查询、更新的字段,防止改变之前的业务逻辑

相对于项目自己封装数据库操作接口,gorm 在带来便利的同时也提供了一些不确定性,主要体现在 gorm 自身的一些设置或者特性所致,需要在测试阶段仔细检查数据库操作相关的业务逻辑表现是否一致


原创不易,转载请注明出处

我正在参与2023腾讯技术创作特训营第二期有奖征文,瓜分万元奖池和键盘手表

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、背景介绍
  • 二、框架选择
  • 三、ORM层目录结构
  • 四、重构过程
    • 1. model层
      • 2. repository层
        • 3. usecase层
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档