likes
comments
collection
share

使用 Hertz 和 Gorm 快速搭建 Web 服务

作者站长头像
站长
· 阅读数 9

简介

在之前的文章中我们使用 Hertz 编写了一个简单的 demo 帮助快速上手 Hertz 这款 Golang HTTP 框架,本节我们将加入 Gorm 框架和 Hertz 框架一起学习一个简单的官方 demo,同时带你了解 Hertz 一些有意思的特性。

如果你还不知道 Hertz 是什么,那么可以查看我之前的文章快速上手。

我们将着重学习以下特性:

  • 使用 thrift IDL 定义 HTTP 接口
  • 使用 hz 生成脚手架代码
  • 使用 Hertz 的参数绑定校验
  • 使用 GormMySQL 编写持久层

获取 demo

执行以下命令获取这个 demo:

 git clone https://github.com/cloudwego/hertz-examples.git
 cd bizdemo/hertz_gorm

项目结构

 hertz_gorm
 ├── biz
 |   ├── dal             // Logic code that interacts with the database
 │   ├── handler         // Main logical code that handles HTTP requests                  
 │   ├── hertz_gen       // Scaffolding generated by hertz from idl files
 |   ├── model           // Go struct corresponding to the database table
 |   ├── pack            // Transformation between database model and response model
 |   ├── router          // Middleware and mapping of routes to handlers
 ├── go.mod              // go.mod                 
 ├── idl                 // thift idl                  
 ├── main.go             // Initialize and start the server              
 ├── router.go           // Sample route registration
 ├── router_gen.go       // Route registration    
 ├── docker-compose.yml  // docker-compose.yml
 ├── Makefile            // Makefile

这是整个项目的基本结构,非常的简洁,hz 为我们生成了大量的脚手架代码。

定义 IDL

hz是由Hertz框架提供的用于生成代码的工具。目前,hz可以基于 thrift 和 protobuf IDL为 Hertz 项目生成脚手架。

一个优秀的 IDL 文件的定义在使用 Hertz 开发中起着重要的作用,我们将使用thrift IDL作为这个项目的示例。

我们可以使用 api 注解让 hz 帮助我们进行参数绑定和验证,路由注册代码生成等。

hz 将根据以下api 注解生成 go tag,以便 Hertz 使用反射检索这些值并解析它们。

字段注解

Hertz 使用 go-tagexpr 开源库用于参数绑定和字段注释的校验,如下面的CreateUserRequest示例所示:

 // api.thrift
 struct CreateUserRequest{
     1: string name      (api.body="name", api.form="name",api.vd="(len($) > 0 && len($) < 100)")
     2: Gender gender    (api.body="gender", api.form="gender",api.vd="($ == 1||$ == 2)")
     3: i64    age       (api.body="age", api.form="age",api.vd="$>0")
     4: string introduce (api.body="introduce", api.form="introduce",api.vd="(len($) > 0 && len($) < 1000)")
 }

form 注解允许 hz 自动为我们以 HTTP 请求体的形式绑定参数,省去了我们使用 PostForm 等方法手动绑定参数的麻烦。

vd 注解允许参数验证。例如,CreateUserRequest 使用 vd 注解来确保 gender 字段只有 1 或 2。

你可以参考 这里 了解更多关于参数验证语法的信息。

方法注解

方法注解可用于生成路由注册的代码。

 // api.thrift
 service UserService {
    UpdateUserResponse UpdateUser(1:UpdateUserRequest req)(api.post="/v1/user/update/:user_id")
    DeleteUserResponse DeleteUser(1:DeleteUserRequest req)(api.post="/v1/user/delete/:user_id")
    QueryUserResponse  QueryUser(1: QueryUserRequest req)(api.post="/v1/user/query/")
    CreateUserResponse CreateUser(1:CreateUserRequest req)(api.post="/v1/user/create/")
 }

我们使用 POST 注解定义了 POST 方法和路由,hz 将根据定义的路由生成对应的路由组,Handler 框架以及中间件框架等。如biz/router/user_gorm/api.go 所示,以及 biz/handler/user_gorm/user_service.go

我们也可以在idl文件中定义业务错误代码:

 // api.thrift
 enum Code {
      Success         = 1
      ParamInvalid    = 2
      DBErr           = 3
 }

hz 将基于这些为我们生成常量和相关方法:

 // biz/hertz_gen/user_gorm/api.go
 type Code int64
 ​
 const (
     Code_Success      Code = 1
     Code_ParamInvalid Code = 2
     Code_DBErr        Code = 3
 )

使用 hz 生成代码

在我们完成 IDL 的编写后,我们可以使用 hz 为我们生成脚手架代码。

执行下面的命令来生成代码:

 hz new --model_dir biz/hertz_gen -mod github.com/cloudwego/hertz-examples/bizdemo/hertz_gorm -idl idl/api.thrift

如果在第一次生成之后编辑了IDL,执行下面的命令来更新代码:

 hz update --model_dir biz/hertz_gen -idl idl/api.thrift

当然,项目已经为您生成了代码,因此您不需要执行它。当你实际使用 Hertz 进行 web 开发时,我相信你会发现它是一个非常有效和有趣的工具。

中间件的使用

在这个项目中,我们配置根路由组为所有路由使用 gzip 中间件以提高性能。

 // biz/router/user_gorm/middleware.go
 func rootMw() []app.HandlerFunc {
     // your code...
     // use gzip middleware
     return []app.HandlerFunc{gzip.Gzip(gzip.DefaultCompression)}
 }

只需在生成的脚手架代码中添加一行代码,非常简单。你可以参考 hertz-contrib/gzip 以获取更多自定义配置的信息。

使用 Gorm 操作数据库

配置 Gorm

要在数据库中使用 GORM,你首先需要使用驱动程序连接数据库并配置 GORM,如 biz/dal/mysql/init.go 所示。

 // biz/dal/mysql/user.go
 package mysql
 ​
 import (
    "gorm.io/driver/mysql"
    "gorm.io/gorm"
    "gorm.io/gorm/logger"
 )
 ​
 var dsn = "gorm:gorm@tcp(localhost:9910)/gorm?charset=utf8&parseTime=True&loc=Local"
 ​
 var DB *gorm.DB
 ​
 func Init() {
    var err error
    DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{
       SkipDefaultTransaction: true,
       PrepareStmt:            true,
       Logger:                 logger.Default.LogMode(logger.Info),
    })
    if err != nil {
       panic(err)
    }
 }

这里我们通过 DSN 连接 MySQL 数据库,并维护一个全局数据库操作对象 DB

在GORM配置方面,由于本项目不涉及同时操作多张表,我们可以将 SkipDefaultTransaction 配置为 true 来跳过默认事务,并通过PrepareStmt 启用缓存以提高效率。

我们还使用了默认的 logger,以便我们可以清楚地看到 GORM 为我们生成的 SQL

操作 MySQL

GORM 通过拼接 SQL 语句的方式来执行CRUD操作,因此代码非常简洁且易于阅读,所有的数据库操作都在 biz/dal/mysql/user.go中。

我们还声明了一个对应于数据库表的模型,gorm.Modelgorm.Model包含一些公共字段,GORM 可以自动为我们填充这些字段,并支持软删除等操作。

 // biz/model/user.go
 type User struct {
    gorm.Model
    Name      string `json:"name" column:"name"`
    Gender    int64  `json:"gender" column:"gender"`
    Age       int64  `json:"age" column:"age"`
    Introduce string `json:"introduce" column:"introduce"`
 }

处理 HTTP 请求

在本节中,我们将探索处理程序(biz/handler/user_gorm/user_service.go),这是主要的业务逻辑代码。

CreateUser & DeleteUser & UpdateUser

CreateUser

由于我们在 thift IDL中使用了api 注解,BindAndValidate 将为我们完成参数绑定和验证。所有有效的参数都会被注入到CreateUserRequest中,这非常方便。

如果出现错误,我们可以使用 JSON 方法以 JSON 格式返回数据。无论是 CreateUserResponse 还是业务代码,我们都可以直接使用hz生成的代码。

之后,我们可以通过在 dal层调用CreateUser来插入一个新用户到 MySQL 中,传入封装好的参数。

如果出现错误,就像一开始那样,返回包含错误代码和信息的JSON。否则,返回正确的服务代码,表示成功创建了用户。

 // biz/handler/user_gorm/user_service.go
 // CreateUser .
 // @router /v1/user/create/ [POST]
 func CreateUser(ctx context.Context, c *app.RequestContext) {
    var err error
    var req user_gorm.CreateUserRequest
    err = c.BindAndValidate(&req)
    if err != nil {
       c.JSON(200, &user_gorm.CreateUserResponse{Code: user_gorm.Code_ParamInvalid, Msg: err.Error()})
       return
    }
    if err = mysql.CreateUser([]*model.User{
       {
          Name:      req.Name,
          Gender:    int64(req.Gender),
          Age:       req.Age,
          Introduce: req.Introduce,
       },
    }); err != nil {
       c.JSON(200, &user_gorm.CreateUserResponse{Code: user_gorm.Code_DBErr, Msg: err.Error()})
       return
    }
 ​
    resp := new(user_gorm.CreateUserResponse)
    resp.Code = user_gorm.Code_Success
    c.JSON(200, resp)
 }

DeleteUser

DeleteUserCreateUser 的逻辑几乎是相同的:绑定并验证参数,使用mysql.DeleteUser 删除用户,如果有错误则返回,否则返回成功。

 // biz/handler/user_gorm/user_service.go
 // DeleteUser .
 // @router /v1/user/delete/:user_id [POST]
 func DeleteUser(ctx context.Context, c *app.RequestContext) {
    var err error
    var req user_gorm.DeleteUserRequest
    err = c.BindAndValidate(&req)
    if err != nil {
       c.JSON(200, &user_gorm.DeleteUserResponse{Code: user_gorm.Code_ParamInvalid, Msg: err.Error()})
       return
    }
    if err = mysql.DeleteUser(req.UserID); err != nil {
       c.JSON(200, &user_gorm.DeleteUserResponse{Code: user_gorm.Code_DBErr, Msg: err.Error()})
       return
    }
 ​
    c.JSON(200, &user_gorm.DeleteUserResponse{Code: user_gorm.Code_Success})
 }

UpdateUser

UpdateUser 与此基本相同,只是进行了从接收 HTTP 请求参数的对象到对应于数据库表的数据访问对象的模型转换。

 // biz/handler/user_gorm/user_service.go
 // UpdateUser .
 // @router /v1/user/update/:user_id [POST]
 func UpdateUser(ctx context.Context, c *app.RequestContext) {
     var err error
     var req user_gorm.UpdateUserRequest
     err = c.BindAndValidate(&req)
     if err != nil {
             c.JSON(200, &user_gorm.UpdateUserResponse{Code: user_gorm.Code_ParamInvalid, Msg: err.Error()})
             return
     }
 ​
     u := &model.User{}
     u.ID = uint(req.UserID)
     u.Name = req.Name
     u.Gender = int64(req.Gender)
     u.Age = req.Age
     u.Introduce = req.Introduce
 ​
     if err = mysql.UpdateUser(u); err != nil {
             c.JSON(200, &user_gorm.UpdateUserResponse{Code: user_gorm.Code_DBErr, Msg: err.Error()})
             return
     }
 ​
     c.JSON(200, &user_gorm.UpdateUserResponse{Code: user_gorm.Code_Success})
 }

QueryUser

QueryUser中值得注意的是,我们进行了分页和从 model.Useruser_gorm.User 的转换。这与我们刚刚在 UpdateUser 中提到的操作相反。

使用简单的分页公式 startIndex = (currentPage - 1) * pageSize,我们可以在查询数据时对其进行分页

这次我们将转换模型包装在 biz/pack/user.go 中。

 // biz/pack/user.go
 // Users Convert model.User list to user_gorm.User list
 func Users(models []*model.User) []*user_gorm.User {
     users := make([]*user_gorm.User, 0, len(models))
     for _, m := range models {
             if u := User(m); u != nil {
                     users = append(users, u)
             }
     }
     return users
 }
 ​
 // User Convert model.User to user_gorm.User
 func User(model *model.User) *user_gorm.User {
     if model == nil {
             return nil
     }
     return &user_gorm.User{
             UserID:    int64(model.ID),
             Name:      model.Name,
             Gender:    user_gorm.Gender(model.Gender),
             Age:       model.Age,
             Introduce: model.Introduce,
     }
 }
 // biz/handler/user_gorm/user_service.go
 // QueryUser .
 // @router /v1/user/query/ [POST]
 func QueryUser(ctx context.Context, c *app.RequestContext) {
     var err error
     var req user_gorm.QueryUserRequest
     err = c.BindAndValidate(&req)
     if err != nil {
             c.JSON(200, &user_gorm.QueryUserResponse{Code: user_gorm.Code_ParamInvalid, Msg: err.Error()})
             return
     }
 ​
     users, total, err := mysql.QueryUser(req.Keyword, req.Page, req.PageSize)
     if err != nil {
             c.JSON(200, &user_gorm.QueryUserResponse{Code: user_gorm.Code_DBErr, Msg: err.Error()})
             return
     }
     c.JSON(200, &user_gorm.QueryUserResponse{Code: user_gorm.Code_Success, Users: pack.Users(users), Totoal: total})
 }

其余的业务逻辑与之前相同,至此我们完成了所有处理程序函数。

运行 demo

  • 运行 docker
 cd bizdemo/hertz_gorm && docker-compose up
  • 生成 MySQL 表

连接 MySQL 并执行 user.sql

  • 运行 demo
 cd bizdemo/hertz_gorm
 go build -o hertz_gorm && ./hertz_gorm

总结

这就是本文的全部内容。希望它能让你快速了解如何使用 HertzGORM 进行开发。它们都有很好的官方文档。如果哪块有问题或者疑问欢迎在评论区提出或者私信我,以上。

参考列表