Go框架三件套详解之Gorm | 青训营

1,900 阅读6分钟

Gorm框架的使用入门

Gorm是个已经迭代了10年+的功能强大的ORM框架,在字节内部被广泛使用并且拥有非常丰富的开源扩展。 本文是基于字节课程《Go 框架三件套详解(Web/RPC/ORM)》的实践记录之一,下面将根据课程内容,并结合实际例子进行Gorm框架使用的介绍。

基础用法

// gorm.Model 的定义 
type Product struct {
  Code  string
  Price unit
}

func main() {
  // 连接到数据库
  dsn := "user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
  db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
}
  // 创建
  db.Create(&Product{Code: "D1", Price: 1})
  // 查询
  var product Product
  db.First(&product, 1)                // 根据整形主键查找
  db.First(&product, "code = ?", "D1") // 查找code字段值为D1的记录
  // 更新
  db.Model(&product).Update("Price", 2)
  // 更新多个字段
  db.Model(&product).Updates(Product{Price: 2, Code: "F2"})
  // 删除
  db.Delete(&product, 1)
}

Gorm支持的数据库

Gorm目前支持MySQL、SQLServer、PostgreSQL、SQLite。 Gorm通过驱动来连接数据库,如果需要连接其他类型的数据库,可以复用/自行开发驱动。 对于我来说,平时写的项目一般用MySQL或者SQLite就能满足需求了。如果你的项目需要使用ElasticSearch之类的数据库的话,可以考虑Elastic Search APM或go-ElasticSearch,但是笔者没有了解使用过,有兴趣的同学可以自行搜索,这里就不展开叙述了。

Gorm项目实践

通过项目实践是掌握一门新技术、新框架的最快方法,下面是我在项目实践中的一些记录,包括在Gorm中进行增删改查的操作、需要注意的常见问题以及一些常用技巧,也包括我的一些感想和体会。

项目内容为简易版抖音服务端,同时这也是青训营的后端大作业内容。这个项目涵盖了后端中最核心的内容,如Golang语言编程,常用框架、数据库、对象存储等内容。其接口模块可以主要分为Feed、User、Publish、Favorite、Comment、Relation和Message这七个,下面我将通过Relation模块的部分实现来记录Gorm在项目中的具体实践。

前置条件

在项目中,我们先通过编写Protobuf3语言的IDL文件,并使用RPC框架Kitex生成了部分代码,包括Relation模块的各个接口的RPC代码,因此下面的代码将在kitex框架生成代码的基础上进行编写。

设计Gorm Model

首先,我们根据Relation服务的特点,设计了一个FollowRelation Model用以表示一个用户对另一个用户的follow 关系,在FollowRelation中,我们用一个Id字段作为主键,UserId字段表示当前用户的Id,ToUserId表示指向用户的Id,IsFollow用以表示是否关注(这个字段设置的目的是为了防止因频繁的取消关注导致数据库存储结构的频繁变化从而影响服务端和存储端性能等),CreatedAt和UpdatedAt字段分别表示该条记录的创建时间和更新时间。最后,创建的Model如下:

type FollowRelation struct {
   Id        int64     `gorm:"primary_key"`
   UserId    int64     `gorm:"not null"`
   ToUserId  int64     `gorm:"not null"`
   IsFollow  bool      `gorm:"not null"`
   CreatedAt time.Time `gorm:"not null"`
   UpdatedAt time.Time `gorm:"not null"`
}

关注接口

关注接口我们将进行判断。如果数据库中不存在UserId对ToUserId的关系对应的记录,那么就将创建一条,设置UserId,ToUserId,并设置IsFollow为True,将CreatedAt和UpdatedAt都设置为现在的时间。如果数据库中已经存在了UserId对ToUserId的关系对应的记录,那么就进行数据的更新即可,将IsFollow更改为true,并且设置UpdatedAt更新时间为当前时间。在Gorm中,我们可以通过Upsert来实现,通过clause.OnConflict来处理冲突以实现。代码也很简洁,在Created()前加上.Clauses(clasuse.OnConflict{选项})即可。 比如官方文档中给的例子:

db.Clauses(clause.OnConflict{
  Columns:   []clause.Column{{Name: "id"}},
  DoUpdates: clause.Assignments(map[string]interface{}{"role": "user"}),
}).Create(&users)

这段代码在MySQL中其实就等价于

INSERT INTO `users` *** ON DUPLICATE KEY UPDATE ***;

如果发生了冲突,就进行数据的更新。

下面是在青训营项目中的具体实践,以Follow接口为例,代码如下:

func (s *RelationServiceImpl) Follow(ctx context.Context, req *relation.FollowReq) (resp *relation.FollowRes, err error) {

   followRelation := &model.FollowRelation{
      UserId:    req.UserId,
      ToUserId:  req.ToUserId,
      IsFollow:  true,
      CreatedAt: time.Now(),
      UpdatedAt: time.Now(),
   }

   err = s.db.Clauses(clause.OnConflict{
      Columns:   []clause.Column{{Name: "user_id"}, {Name: "to_user_id"}},
      DoUpdates: clause.Assignments(map[string]interface{}{"is_follow": true, "updated_at": time.Now()}),
   }).Create(followRelation).Error
   if err != nil {
      resp.Status = relation.Status_ERROR
      resp.ErrMsg = "Create failed"
      return resp, err
   }

   resp.Status = relation.Status_OK
   resp.ErrMsg = ""

   return resp, nil
}

取消关注接口

在取消关注接口中,我们将先进行判断。如果数据库中存在UserId对ToUserId的关系对应的记录,那么就将IsFollow更改为false(这里没有将记录直接删除,也是为了最大限度保留数据库的存储结构,从而提高服务端和数据库端的性能),并且设置UpdatedAt更新时间为当前时间。如果数据库中不存在UserId对ToUserId的关系对应的记录,那么就返回错误信息。在Gorm中,我们可以使用Where和Update方法来实现,代码如下:

func (s *RelationServiceImpl) Unfollow(ctx context.Context, req *relation.UnfollowReq) (resp *relation.UnfollowRes, err error) {

   err = s.db.Where("user_id = ? AND to_user_id = ?", req.UserId, req.ToUserId).Update("is_follow", false).Update("updated_at", time.Now()).Error
   if err != nil {
      resp.Status = relation.Status_ERROR
      resp.ErrMsg = "Update failed"
      return resp, err
   }

   resp.Status = relation.Status_OK
   resp.ErrMsg = ""

   return resp, nil
}

查询关注列表接口

在业务中,我们需要根据一个用户的Id去查找这个用户所关注的所有其他用户,在ListFollow接口中,我们将根据用户的需求,从数据库中查询出符合条件的数据,并返回给用户。我们可以在Gorm中,使用Find方法来实现这个功能,代码如下:

func (s *RelationServiceImpl) ListFollow(ctx context.Context, req *relation.ListFollowReq) (resp *relation.ListFollowRes, err error) {

   var followRelations []model.FollowRelation

   err = s.db.Where("user_id = ? AND is_follow = ?", req.UserId, true).Find(&followRelations).Error
   if err != nil {
      resp.Status = relation.Status_ERROR
      resp.ErrMsg = "Find failed"
      return resp, err
   }

   var users []*relation.UserInfo

   for _, followRelation := range followRelations {
      user := &model.User{
         Id: followRelation.ToUserId,
      }
      userInfo := &relation.UserInfo{
         Id: user.Id,
         Name: user.Name,
         Avatar: user.Avatar,
         // ... 其他字段
      }
      users = append(users, userInfo)
   }

   resp.Status = relation.Status_OK
   resp.ErrMsg = ""
   resp.Users = users

   return resp, nil
}

删除记录

在上面的例子中,所有的删除都是通过更新记录来实现的,通过设置IsFollow为false来表示一个用户取消关注了另一个用户。但我们如何使用Gorm提供的软删除功能呢?另外,在其他场景下,如果我们确实需要将记录从数据库中删除,又该怎么做呢?

根据Gorm的官方文档,如果在定义的模型中包含了 gorm.DeletedAt字段(该字段也被包含在gorm.Model中),那么该模型将会自动获得软删除的能力。

当调用Delete时,GORM并不会从数据库中删除该记录,而是将该记录的DeleteAt设置为当前时间,而后的一般查询方法将无法查找到此条记录。

假如上述的FollowRelation中设置了该字段,并且执行了db.Delete(&followRelation)语句,那么将自动将DeleteAt设置为当前时间,并且后面的一般查询方法将无法查到此条记录。如果没有设置该字段,运行这条语句能直接将其从数据库中删除。

参考文献

GORM 指南 | GORM - The fantastic ORM library for Golang, aims to be developer friendly.

Go 框架三件套详解(Web/RPC/ORM) - 掘金 (juejin.cn)