GORM、Kitex、Hertz三件套 | 青训营笔记

103 阅读6分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 6 天

三件套的使用

建议查文档,这里只是短暂记录下

Gorm

基本使用

Gorm Model的约定:

  • Gorm使用名为ID的字段为主键
  • 使用结构体的蛇形负数的表名
  • 字段名的蛇形为列名
  • 使用CreateAt、UpdateAt字段作为创建、更新时间

定义Gorm Model

type Product struct {
    Code  string
    Price uint
}
​
func (p Product) TableName() string {
    return "product"
}

连接数据库:

db, err := gorm.Open(
        mysql.Open("user:password@tcp(127.0.0.1:3306)/dbname?charset=utf8&mb4&parseTime=True&loc=Local"),
        &gorm.Config{})
​
    if err != nil {
        panic("faild to connect database")
    }
​

增删改查:

db.Create(&Product{Code: "D42", Price: 100})
​
    var product Product
    db.First(&product, 1)
    db.First(&product, "code = ?", "D42")
​
    db.Model(&product).Update("Price", 200)
​
    db.Model(&product).Updates(Product{Price: 200, Code: "F42"})
    db.Model(&product).Updates(map[string]interface{}{"Price": 200, "Code": "F42"})
​
    db.Delete(&product, 1)

支持的数据库

Gorm目前支持MySQL、SQLServer、PostgreSQL、SQLite

创建数据

type Product struct {
    ID    uint   `gorm: "primarykey"`
    Code  string `gorm: "column": "code`
    Price uint   `gorm:"column": "user_id"`
}
​
func main() {
    // 连接数据库
    db, err := gorm.Open(mysql.Open("username:password@tcp(localhost:9910)/database?charset=utf8"),
        &gorm.Config{})
    if err != nil {
        panic("faild to connect database")
    }
    
    p := &Product{Code: "D42"}
    // 添加单条数据
    res := db.Create(p)
    fmt.Println(res.Error)
    fmt.Println(p.ID)
    
    // 添加多条数据
    products := []*Product{{Code: "D41"}, {Code: "D42"}, {Code: "D43"}}
    res = db.Create(products)
    fmt.Println(res.Error)
    for _, p := range products {
        fmt.Println(p.ID)
    }
}

如果产生数据冲突:使用clause.OnConflict处理数据冲突

p := &product{Code: "D42", ID: 1}
db.Clauses(clause.OnConflict{DoNoting: true}).Create(&p)

使用默认值:通过使用default标签为字段定义默认值

type User struct {
    ID   int64
    Name string `gorm: "default:galeone"`
    Age  int64  `gorm: default:18`
}

查询数据

使用First注意:

  • 查询不到数据会返回ErrRecordNotFound
  • 使用Find查询多条数据,查询不到数据不会返回错误

使用结构体作为查询条件:

GORM只会查询非0值字段:这意为着如果字段值为“0”、“false”或者其他0值,该字段不会用于构建查询条件,使用Map来构建查询条件

    // 获取第一条记录
    u := &User{}
    db.First(u)
​
    users := make([]*User, 0)
    // SELECT * FROM users WHERE age > 10
    result := db.Where("age > 10").Find(&users)
    fmt.Println(result.RowsAffected) // 返回查找到的记录数
    fmt.Println(result.Error)
​
    // SELECT * FROM users WHERE name IN ('jinzhu', 'jinzhu2')
    db.Where("name IN ?", []string{"jinzhu", "jinzhu2"}).Find(&users)
​
    // SELECT * FROM users WHERE name LIKE '%jin%'
    db.Where("name LIKE ?", "%jin%").Find(&users)
​
    // SELECT * FROM users WHERE name = jinzhu AND age >= 22
    db.Where("name = ? AND age >= ?", "jinzhu", "22").Find(&users)
​
    // SELECT * FROM users WHERE name = "jinzhu"
    db.Where(&User{Name: "jinzhu", Age: 0}).Find(&users)
​
    // SELECT * FROM users WHERE name = "jinzhu" AND age = 0
    db.Where(map[string]interface{}{"Name": "jinzhu", "Age": 0}).Find(&users)

更新数据

使用Struct更新时,只会更新非0值,如果需要更新零值可以使用Map更新或使用Select字段

// 条件更新单个列
    // UPDATE users SET name = "hello", updated_at = 'xxxx-xx-xx xx:xx:xx' WHERE age > 10
    db.Model(&User{ID: 111}).Where("age > ?", 18).Update("name", "hello")
​
    // 更新多个列。根据struct更新属性,只会更新非零值的字段
    // UPDATE users SET name = "hello", age = 18, updateed_at 'xxx' WHERE id = 111
    db.Model(&User{ID: 111}).Updates(User{Name: "hello", Age: 18})
​
    // 根据map更新属性
    // UPDATE users SET name = 'hello', age = '18', actived = false, updated_at = 'xxx' WHERE id = 111
    db.Model(&User{ID: 111}).Updates(map[string]interface{}{"name": "hello", "age": 18, "actived": false})
    
    // 更新选定字段
    // UPDATE users SET name = 'hello' WHERE id = 111
    db.Model(&User{ID: 111}).Select("name").Updates(map[string]interface{}{"name": "hello", "age": 18, "actived": false})
    
    // SQL表达式更新
    // UPDATE users SET "age" = age * 2 + 100, "updated_at" = xxx WHERE id = 111
    db.Model(&User{ID: 111}).Update("age", gorm.Expr("age * ? + ?", 2, 100))

删除数据

物理删除

// DELETE FROM users WHERE id = 10  
db.Delete(&User{}, 10)                           
// DELETE FROM users WHERE id = 10  
db.Delete(&User{}, "10")                           
 // DELETE FROM users WHERE id IN (1, 2, 3) 
db.Delete(&User{}, []int{1, 2, 3})                
// DELETE from users where name LIKE "%jinzhu%"
db.Where("name LIKE ?", "%jinzhu%").Delete(User{}) 
// DELETE from users WHERE name LIKE "%jinzhu%"
db.Delete(User{}, "name LIKE ?", "%jinzhu%")       

软删除

Gorm提供了gorm.DeleteAt用于帮助用户实现软删除

拥有软删除能力的Model调用Delete时,记录不会被数据真正删除,但Gorm会将DeleteAt置为当前时间,冰倩不能再通过正常的查询方法找到该记录

使用UnScoped可以查询到软删除的数据

db.UnScoped().Where("age = 20").Find(&users)

事务

Gorm提供了Begin、Commit、Rollback方法用于使用事务

 tx := db.Begin()   // 开启事务
 
 // 执行db操作
 if err = tx.Create(&User{Name: "name"}).Error; err != nil {
    tx.Rollback()
    return
 }
 
 if err = tx.Create(&User{Name: "name1"}).Error; err != nil {
    tx.Rollback()
    return
 }
 // 提交事务
 tx.Commit()

另外还提供了Transaction方法用于自动提交事务

image-20230120212744757

GORM HOOK

HOOK是在创建、查询、更新、删除等操作之前后,自动调用的函数。

如果任何Hook返回错误,Gorm将停止后续的操作并回滚事务

type User struct {
    ID   int64
    Name string `gorm: "default:galeone"`
    Age  int64  `gorm: default:18`
}
func (u *User) BeforeCreate(tx *gorm.db) (err Error) {
    if u.Age < 0 {
        return errors.New("Xxxx")
    }
    return
}
​
func (u *User) AfterCreate(tx * gorm.db) (err Error) {
    xxxx
}

Gorm性能提高

对于写操作,为了保证数据的完整性,GORM会将它们风状态事务内运行,但这会降低性能,可以使用SkipDefaultTransaction关闭默认事务

使用PrepareStmt缓存预编译语句可以提高后续调用的速度

db, err := gorm.Open(mysql.Open("username:password@tcp(localhost:9910)/database?charset=utf8"),
        &gorm.Config{
        SkipDefaultTransaction: true,
        PrepareStmt:            true},
)

更多

image-20230120213550558

Kitex

使用IDL定义接口与服务

namespace go api
​
struct Request {
    1: string message
}
​
struct Response {
    1: string message
}
​
service Echo {
    Response echo(1 :Request req)
}

使用kitex生成代码

kitex -module example -service example note.thrift

image-20230120222225687

目录介绍:

  • build.sh:构建脚本
  • kitex_gen:IDL内容相关的生成代码,主要是最基础的Server/Client代码
  • main.go:程序入口
  • handler.go:用户在该文件里实现IDL
  • server:定义的方法

基本使用

服务默认监听8888端口

package main
​
import (
    "context"
    api "example/kitex_gen/api"
)
​
// EchoImpl implements the last service interface defined in the IDL.
type EchoImpl struct{}
​
// Echo implements the EchoImpl interface.
func (s *EchoImpl) Echo(ctx context.Context, req *api.Request) (resp *api.Response, err error) {
    // TODO: Your code here...
    return
}
​

Kitex Client发起请求

创建client目录,在里面创建main.go

package main
​
import (
    "context"
    "example/kitex_gen/api"
    "example/kitex_gen/api/echo"
    "log"
    "time""github.com/cloudwego/kitex/client"
)
​
func main() {
    // 创建客户端
    c, err := echo.Client("example", client.WithHostPorts("0.0.0.0:8888"))
    if err != nil {
        log.Fatal(err)
    }
    
    // 发起请求
    req := &api.Request{Message: "my request"}
    resp, err := c.Echo(context.Background(), req, callopt.WithHostPorts(3*time.Second))
​
    if err != nil {
        log.Fatal(err)
    }
​
    log.Fatal(resp)
}

Kitex服务的注册与发现

目前Kitex的服务注册与发现已经对接了的主流的服务注册与发现中心,如ETCD,Nacos等

type HelloImpl struct{}
​
func (h *HelloImpl) Echo(ctx context.Context, req *api.Request) (resp *api.Response, err error) {
    resp = &api.Response{
        Message: req.Message
    }
    return
}
​
// 服务端
func main() {
    r, err := etcd.NewEtcdRegister([]string{"127.0.0.1:2379"})
    if err != nil{
        log.Fatal(err)
    }
    server := hello.NewServer(new(HelloImpl), server.WithRegistry(r), server.WithServerBasicInfo(&rpcinfo,EndpointBasicInfo {
        ServiceName: "Hello",
    }))
    err = server.Run()
    if err != nil {
        log.Fatal(err)
    }
}
​
​
// 客户端
func main() {
    r, err := etcd.NewEtcdResolver([]string{"127.0.0.1:2379"})
    if err != nil {
        log.Fatal(err)
    }
    client := hello.MustNewClient("hello", client.WithResolver(r))
    for {
        ctx, cancel := context.WithTimeout(context.Background(), time.Second * 3)
        resp, err := client.Echo(ctx, &api.Request{Message: "Hello"})
        cancel()
        if err != nil {
            log.Fatal(err)
        }
        log.Println(resp)
        time.Sleep(time.Second)
    }
}

Kitex生态

image-20230122233045543

Hertz

Hertz是一个HTTP框架

基本使用

package main
​
import (
    "context""github.com/cloudwego/hertz/pkg/app"
    "github.com/cloudwego/hertz/pkg/app/server"
    "github.com/cloudwego/hertz/pkg/common/utils"
    "github.com/cloudwego/hertz/pkg/protocol/consts"
)
​
func main() {
    h := server.Default()
​
    h.GET("/ping", func(c context.Context, ctx *app.RequestContext) {
        ctx.JSON(consts.StatusOK, utils.H{"message": "pong"})
    })
​
    h.Spin()
}
​

Hertx路由

路由:路由指导报文转发的路径信息,通过路由可以确认转发IP报文的路径。路由设备是依据路由转发报文到达目的网段的网络设备,最常见的路由设备:路由器。路由器维护着一张表,保存着路由信息。

image-20230123211854157

Hertz提供了GET、POST、DELETE、ANY等方法用于路由注册

image-20230123123240582

image-20230123123759324

package main
​
import (
    "context"
    "github.com/cloudwego/hertz/pkg/app"
    "github.com/cloudwego/hertz/pkg/app/server"
    "github.com/cloudwego/hertz/pkg/protocol/consts"
)
​
func main(){
    h := server.Default(server.WithHostPorts("127.0.0.1:8080"))
​
    h.StaticFS("/", &app.FS{Root: "./", GenerateIndexPages: true})
​
    h.GET("/get", func(ctx context.Context, c *app.RequestContext) {
        c.String(consts.StatusOK, "get")
    })
    h.POST("/post", func(ctx context.Context, c *app.RequestContext) {
        c.String(consts.StatusOK, "post")
    })
    h.PUT("/put", func(ctx context.Context, c *app.RequestContext) {
        c.String(consts.StatusOK, "put")
    })
    h.DELETE("/delete", func(ctx context.Context, c *app.RequestContext) {
        c.String(consts.StatusOK, "delete")
    })
    h.PATCH("/patch", func(ctx context.Context, c *app.RequestContext) {
        c.String(consts.StatusOK, "patch")
    })
    h.HEAD("/head", func(ctx context.Context, c *app.RequestContext) {
        c.String(consts.StatusOK, "head")
    })
    h.OPTIONS("/options", func(ctx context.Context, c *app.RequestContext) {
        c.String(consts.StatusOK, "options")
    })
    h.Any("/ping_any", func(ctx context.Context, c *app.RequestContext) {
        c.String(consts.StatusOK, "any")
    })
    h.Handle("LOAD","/load", func(ctx context.Context, c *app.RequestContext) {
        c.String(consts.StatusOK, "load")
    })
    h.Spin()
}

Hertz 提供了路由组( Group )的能力,用于支持路由分组的功能,同时中间件也可以注册到路由组上。

package main
​
import (
    "context"
    "github.com/cloudwego/hertz/pkg/app"
    "github.com/cloudwego/hertz/pkg/app/server"
    "github.com/cloudwego/hertz/pkg/protocol/consts"
)
​
func main(){
    h := server.Default(server.WithHostPorts("127.0.0.1:8080"))
    v1 := h.Group("/v1")
    v1.GET("/get", func(ctx context.Context, c *app.RequestContext) {
        c.String(consts.StatusOK, "get")
    })
    v1.POST("/post", func(ctx context.Context, c *app.RequestContext) {
        c.String(consts.StatusOK, "post")
    })
    v2 := h.Group("/v2")
    v2.PUT("/put", func(ctx context.Context, c *app.RequestContext) {
        c.String(consts.StatusOK, "put")
    })
    v2.DELETE("/delete", func(ctx context.Context, c *app.RequestContext) {
        c.String(consts.StatusOK, "delete")
    })
    h.Spin()
}

Hertz代码生成工具

提供代码生成工具Hz,也可以使用接口定义语言IDL生成对应的基础服务代码

文件的结构: