资料链接
快速开始 | CloudWeGo GORM - The fantastic ORM library for Golang, aims to be developer friendly.
1. GORM
1.1 GORM创建数据
创建单条记录
user := User{Name: "Jinzhu", Age: 18, Birthday: time.Now()}
result := db.Create(&user) // 通过数据的指针来创建
user.ID // 返回插入数据的主键
result.Error // 返回 error
result.RowsAffected // 返回插入记录的条数
创建多条记录
users := []*User{
User{Name: "Jinzhu", Age: 18, Birthday: time.Now()},
User{Name: "Jackson", Age: 19, Birthday: time.Now()},
}
result := db.Create(users) // pass a slice to insert multiple row
result.Error // returns error
result.RowsAffected // returns inserted records count
1.2 GORM查询数据
查询单挑数据
GORM 提供了 First、Take、Last 方法,以便从数据库中检索单个对象。当查询数据库时它添加了 LIMIT 1 条件,且没有找到记录时,它会返回 ErrRecordNotFound 错误
// 获取第一条记录(主键升序)
db.First(&user)
// SELECT * FROM users ORDER BY id LIMIT 1;
// 获取一条记录,没有指定排序字段
db.Take(&user)
// SELECT * FROM users LIMIT 1;
// 获取最后一条记录(主键降序)
db.Last(&user)
// SELECT * FROM users ORDER BY id DESC LIMIT 1;
result := db.First(&user)
result.RowsAffected // 返回找到的记录数
result.Error // returns error or nil
// 检查 ErrRecordNotFound 错误
errors.Is(result.Error, gorm.ErrRecordNotFound)
- 使用First的时候,查询不到会返回ErrRecordNotFound
- 使用Find查询多条数据的时候,查询不到数据不会返回错误
查询全部
// Get all records
result := db.Find(&users)
// SELECT * FROM users;
result.RowsAffected // returns found records count, equals `len(users)`
result.Error // returns error
使用Struct或Map条件查询
// Struct
db.Where(&User{Name: "jinzhu", Age: 20}).First(&user)
// SELECT * FROM users WHERE name = "jinzhu" AND age = 20 ORDER BY id LIMIT 1;
// Map
db.Where(map[string]interface{}{"name": "jinzhu", "age": 20}).Find(&users)
// SELECT * FROM users WHERE name = "jinzhu" AND age = 20;
// Slice of primary keys
db.Where([]int64{20, 21, 22}).Find(&users)
// SELECT * FROM users WHERE id IN (20, 21, 22);
当结构体作为查询条件时,仅会比较非零字段,如果要比较所有的字段,需要使用Map来构建查询条件
1.3 更新数据
更新所有字段
Save 会保存所有的字段,即使字段是零值
db.Save(&User{Name: "jinzhu", Age: 100})
// INSERT INTO `users` (`name`,`age`,`birthday`,`update_at`) VALUES ("jinzhu",100,"0000-00-00 00:00:00","0000-00-00 00:00:00")
db.Save(&User{ID: 1, Name: "jinzhu", Age: 100})
// UPDATE `users` SET `name`="jinzhu",`age`=100,`birthday`="0000-00-00 00:00:00",`upda
save会保存所有的字段,在有prime key的情况下会执行Update,否则直接执行Create
更新单个列
当使用Update更新单个列时,它需要有条件限制,否则会引发错误ErrMissingWhereClause。当使用Model方法并且它的值有一个主值时,主键将被用来构建条件
// Update with conditions
db.Model(&User{}).Where("active = ?", true).Update("name", "hello")
// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE active=true;
// User's ID is `111`:
db.Model(&user).Update("name", "hello")
// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE id=111;
// Update with conditions and model value
db.Model(&user).Where("active = ?", true).Update("name", "hello")
// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE id=111 AND active=true;
更新多个列
可以使用struct或者Map进行更新,其中前者只能更新非零字段,后者则是都可以
// Update attributes with `struct`, will only update non-zero fields
db.Model(&user).Updates(User{Name: "hello", Age: 18, Active: false})
// UPDATE users SET name='hello', age=18, updated_at = '2013-11-17 21:34:10' WHERE id = 111;
// Update attributes with `map`
db.Model(&user).Updates(map[string]interface{}{"name": "hello", "age": 18, "active": false})
// UPDATE users SET name='hello', age=18, active=false, updated_at='2013-11-17 21:34:10' WHERE id=111;
更新选定的字段
可以使用select或者omit选中需要进行更新的字段
// Select with Map
// User's ID is `111`:
db.Model(&user).Select("name").Updates(map[string]interface{}{"name": "hello", "age": 18, "active": false})
// UPDATE users SET name='hello' WHERE id=111;
db.Model(&user).Omit("name").Updates(map[string]interface{}{"name": "hello", "age": 18, "active": false})
// UPDATE users SET age=18, active=false, updated_at='2013-11-17 21:34:10' WHERE id=111;
// Select with Struct (select zero value fields)
db.Model(&user).Select("Name", "Age").Updates(User{Name: "new_name", Age: 0})
// UPDATE users SET name='new_name', age=0 WHERE id=111;
// Select all fields (select all fields include zero value fields)
db.Model(&user).Select("*").Updates(User{Name: "jinzhu", Role: "admin", Age: 0})
// Select all fields but omit Role (select all fields include zero value fields)
db.Model(&user).Select("*").Omit("Role").Updates(User{Name: "jinzhu", Role: "admin", Age: 0})
GORM删除
物理删除
// Email 的 ID 是 `10`
db.Delete(&email)
// DELETE from emails where id = 10;
// 带额外条件的删除
db.Where("name = ?", "jinzhu").Delete(&email)
// DELETE from emails where id = 10 AND name = "jinzhu";
db.Delete(&User{}, 10)
// DELETE FROM users WHERE id = 10;
db.Delete(&User{}, "10")
// DELETE FROM users WHERE id = 10;
db.Delete(&users, []int{1,2,3})
// DELETE FROM users WHERE id IN (1,2,3);
软删除
当数据具有DeletedAt字段的时候 gorm通过gorm.DeletedAt进行软删除,将数据的DeletedAt设置为当前的时间,使得不会在数据库中通过一般查询查询到该数据,但是可以通过unscoped方法查询到被软删除的数据
// user's ID is `111`
db.Delete(&user)
// UPDATE users SET deleted_at="2013-10-29 10:23" WHERE id = 111;
// Batch Delete
db.Where("age = ?", 20).Delete(&User{})
// UPDATE users SET deleted_at="2013-10-29 10:23" WHERE age = 20;
// Soft deleted records will be ignored when querying
db.Where("age = 20").Find(&user)
// SELECT * FROM users WHERE age = 20 AND deleted_at IS NULL;
GORM事务
gorm使用Begin,Commit,RollBack等方法完成事务的编写
func CreateAnimals(db *gorm.DB) error {
// 再唠叨一下,事务一旦开始,你就应该使用 tx 处理数据
tx := db.Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
if err := tx.Error; err != nil {
return err
}
if err := tx.Create(&Animal{Name: "Giraffe"}).Error; err != nil {
tx.Rollback()
return err
}
if err := tx.Create(&Animal{Name: "Lion"}).Error; err != nil {
tx.Rollback()
return err
}
return tx.Commit().Error
}
也可以直接使用Transaction方法
db.Transaction(func(tx *gorm.DB) error {
// 在事务中执行一些 db 操作(从这里开始,您应该使用 'tx' 而不是 'db')
if err := tx.Create(&Animal{Name: "Giraffe"}).Error; err != nil {
// 返回任何错误都会回滚事务
return err
}
if err := tx.Create(&Animal{Name: "Lion"}).Error; err != nil {
return err
}
// 返回 nil 提交事务
return nil
})
GORM性能优化
- 对于写操作(创建、更新、删除),为了确保数据的完整性,GORM会将它们封装在事务内运行。但这会降低性能,可以使用
SkipDefaultTransaction关闭默认事务。 - 使用PrepareStmt缓存预编译语句可以提高后续调用的速度,本机测试提高大约35%左右。
GORM生态
2. Kitex
2.1 Kitex简介
Kitex字节跳动内部的 Golang 微服务 RPC 框架,具有高性能、强可扩展的特点,在字节内部已广泛使用。如果对微服务性能有要求,又希望定制扩展融入自己的治理体系,Kitex 会是一个不错的选择。
框架特点
-
高性能
使用自研的高性能网络库 Netpoll,性能相较 go net 具有显著优势。
-
扩展性
提供了较多的扩展接口以及默认扩展实现,使用者也可以根据需要自行定制扩展,具体见下面的框架扩展。
-
多消息协议
RPC 消息协议默认支持 Thrift、Kitex Protobuf、gRPC。Thrift 支持 Buffered 和 Framed 二进制协议;Kitex Protobuf 是 Kitex 自定义的 Protobuf 消息协议,协议格式类似 Thrift;gRPC 是对 gRPC 消息协议的支持,可以与 gRPC 互通。除此之外,使用者也可以扩展自己的消息协议。
-
多传输协议
传输协议封装消息协议进行 RPC 互通,传输协议可以额外透传元信息,用于服务治理,Kitex 支持的传输协议有 TTHeader、HTTP2。TTHeader 可以和 Thrift、Kitex Protobuf 结合使用;HTTP2 目前主要是结合 gRPC 协议使用,后续也会支持 Thrift。
-
多种消息类型
支持 PingPong、Oneway、双向 Streaming。其中 Oneway 目前只对 Thrift 协议支持,双向 Streaming 只对 gRPC 支持,后续会考虑支持 Thrift 的双向 Streaming。
-
服务治理
支持服务注册/发现、负载均衡、熔断、限流、重试、监控、链路跟踪、日志、诊断等服务治理模块,大部分均已提供默认扩展,使用者可选择集成。
-
代码生成
Kitex 内置代码生成工具,可支持生成 Thrift、Protobuf 以及脚手架代码。
2.2 定义IDL
如果我们要进行 RPC,就需要知道对方的接口是什么,需要传什么参数,同时也需要知道返回值是什么样的,就好比两个人之间交流,需要保证在说的是同一个语言、同一件事。 这时候,就需要通过 IDL 来约定双方的协议,就像在写代码的时候需要调用某个函数,我们需要知道函数签名一样。
首先我们需要编写一个 IDL,这里以 thrift IDL 为例。
首先创建一个名为 echo.thrift 的 thrift IDL 文件。
然后在里面定义我们的服务
namespace go api
struct Request {
1: string message
}
struct Response {
1: string message
}
service Echo {
Response echo(1: Request req)
}
2.3 生成服务代码
有了 IDL 以后我们便可以通过 kitex 工具生成项目代码了,执行如下命令:
$ kitex -module example -service example-server echo.thrift
上述命令中:
-
-module表示生成的该项目的 go module 名;建议使用完整包名,例如github.com/Yourname/exampleserver -
-service表明我们要生成一个服务端项目,后面紧跟的example-server为该服务的名字 -
最后一个参数则为该服务的 IDL 文件
文件结构如下:
. |-- build.sh |-- echo.thrift |-- handler.go |-- kitex_gen | `-- api | |-- echo | | |-- client.go | | |-- echo.go | | |-- invoker.go | | `-- server.go | |-- echo.go | `-- k-echo.go |-- main.go `-- script `-- bootstrap.sh
2.4 编写服务逻辑
我们需要编写的服务端逻辑都在 handler.go 这个文件中,现在这个文件应该如下所示:
package main
import (
"context"
"example/kitex_gen/api" //如果修改了 -module 参数的值,这里的 'example' 也要相应替换
)
// 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
}
这里的 Echo 函数就对应了我们之前在 IDL 中定义的 echo 方法。
现在让我们修改一下服务端逻辑,让 Echo 服务名副其实。
修改 Echo 函数为下述代码:
func (s *EchoImpl) Echo(ctx context.Context, req *api.Request) (resp *api.Response, err error) {
return &api.Response{Message: req.Message}, nil
}
2.5 编译运行
编译:
$ sh build.sh
执行上述命令后,会生成一个 output 目录,里面含有我们的编译产物。
运行:
$ sh output/bootstrap.sh
执行上述命令后,Echo 服务就开始运行啦!
2.6 编写客户端
有了服务端后,接下来就让我们编写一个客户端用于调用刚刚运行起来的服务端。
首先,同样的,先创建一个目录用于存放我们的客户端代码:
$ mkdir client
进入目录:
$ cd client
然后用 kitex 命令生成客户端所需的相关代码(如果该目录放在前述 example-server 目录下,则可省略此步,因为 server 代码中包含了 client 所需代码):
$ kitex -module example echo.thrift
注:
- 客户端代码不需要指定 -service 参数,生成的代码在 kitex_gen 目录下;
- -module 参数 建议使用完整的包名,例如
github.com/Yourname/exampleclient
然后创建一个 main.go 文件,就开始编写客户端代码了
2.7 创建client
首先让我们创建一个调用所需的 client:
import "example/kitex_gen/api/echo" //如果修改了 -module 参数,这里要将 'example' 相应替换成相应的包名
import "github.com/cloudwego/kitex/client"
...
c, err := echo.NewClient("example-server", client.WithHostPorts("0.0.0.0:8888"))
if err != nil {
log.Fatal(err)
}
上述代码中,echo.NewClient 用于创建 client,其第一个参数为调用的 服务名,第二个参数为 options,用于传入参数, 此处的 client.WithHostPorts 用于指定服务端的地址,更多参数可参考基本特性一节。
2.8 发起调用
接下来让我们编写用于发起调用的代码:
import "example/kitex_gen/api"
...
req := &api.Request{Message: "my request"}
resp, err := c.Echo(context.Background(), req, callopt.WithRPCTimeout(3*time.Second))
if err != nil {
log.Fatal(err)
}
log.Println(resp)
上述代码中,我们首先创建了一个请求 req , 然后通过 c.Echo 发起了调用。
其第一个参数为 context.Context,通过通常用其传递信息或者控制本次调用的一些行为,你可以在后续章节中找到如何使用它。
其第二个参数为本次调用的请求。
其第三个参数为本次调用的 options ,Kitex 提供了一种 callopt 机制,顾名思义——调用参数 ,有别于创建 client 时传入的参数,这里传入的参数仅对此次生效。 此处的 callopt.WithRPCTimeout 用于指定此次调用的超时(通常不需要指定,此处仅作演示之用)同样的,你可以在基本特性一节中找到更多的参数。