这是我参与「第五届青训营 」伴学笔记创作活动的第 8 天,本文介绍了三个 Go 开发框架 GORM,Kitex,Hertz之一GORM 的基本使用方法.
一.介绍
1. Gorm
Gorm是一个迭代了十年的功能强大的Go语言编写的ORM框架.
- 全功能 ORM
- 关联 (Has One,Has Many,Belongs To,Many To Many,多态,单表继承)
- Create,Save,Update,Delete,Find 中钩子方法
- 支持 Preload、Joins 的预加载
- 事务,嵌套事务,Save Point,Rollback To Saved Point
- Context,预编译模式,DryRun 模式
- 批量插入,FindInBatches,Find/Create with Map,使用 SQL 表达式、Context Valuer 进行 CRUD
- SQL 构建器,Upsert,数据库锁,Optimizer/Index/Comment Hint,命名参数,子查询
- 复合主键,索引,约束
- Auto Migration
- 自定义 Logger
- 灵活的可扩展插件 API:Database Resolver(多数据库,读写分离)、Prometheus…
- 每个特性都经过了测试的重重考验
- 开发者友好
2.Kitex
Kitex是字节内部的Golang微服务RPC框架,具有高性能,高可扩展的主要特点,支持多协议且拥有丰富的开源拓展
-
高性能
使用自研的高性能网络库 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 以及脚手架代码。
3.Hertz
Hertz是一个 Golang 微服务 HTTP 框架,在设计之初参考了其他开源框架 fasthttp、gin、echo 的优势, 并结合字节跳动内部的需求,使其具有高易用性、高性能、高扩展性等特点.
-
高易用性
容易上手,开箱即用,且不缺乏灵活性
-
高性能
Hertz 默认使用自研的高性能网络库 Netpoll,在一些特殊场景相较于 go net,Hertz 在 QPS、时延上均具有一定优势。关于性能数据,可参考下图 Echo 数据。
-
高扩展性
Hertz 采用了分层设计,提供了较多的接口以及默认的扩展实现,用户也可以自行扩展。
-
多协议支持
Hertz 框架原生提供 HTTP1.1、ALPN 协议支持。除此之外,由于分层设计,Hertz 甚至支持自定义构建协议解析逻辑,以满足协议层扩展的任意需求。
-
网络层切换能力
ertz 实现了 Netpoll 和 Golang 原生网络库 间按需切换能力,用户可以针对不同的场景选择合适的网络库,同时也支持以插件的方式为 Hertz 扩展网络库实现。
4.ORM
ORM全称是:Object Relational Mapping(对象关系映射),其主要作用是在编程中,把面向对象的概念跟数据库中表的概念对应起来。一个对象就对应一张表,使用ORM,我们可以在不直接使用SQl语句的情况下,使用数据库.
5.RPC
RPC(Remote Procedure Call)远程过程调用协议,是调用另一个地址的存储过程或者函数,而不用显式编码这次远程调用的协议.RPC一般是网络从远程计算机上请求服务,它假定某些协议的存在,例如TPC/UDP等,为通信程序之间携带信息数据。
它是可以像调用本地程序一样调用远程服务,隐藏了底层网络技术的协议,可以显著提高吞出量而代价不高.
在OSI网络七层模型中,RPC跨越了传输层和应用层,RPC使得开发,包括网络分布式多程序在内的应用程序更加容易。
二. GORM
1.Gorm的基础操作
阅读 gorm.cn/docs/#Insta…
代码相关来源gorm.cn/zh_CN/docs/…
使用之前,因为Grom是第三方库,所以先安装 GORM 及需要连接对应数据库的驱动。
我们使用mysql连接作为展示.
mysql需要导入"gorm.io/driver/mysql"和"gorm.io/gorm"
安装
go get -u gorm.io/gorm
go get -u gorm.io/driver/mysql
快速入门
import (
"fmt"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
type Product struct { //定义gorm model
gorm.Model
Code string
Price uint
}
func (p Product) TableName() string { //为model定义表名
return "product"
}
func main() {
log := User + ":" + Pass + "@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(log), &gorm.Config{}) //连接数据库
if err != nil {
panic("failed to connect database" + fmt.Sprintf("%s", err))
}
// 迁移 schema
db.AutoMigrate(&Product{})
// Create(创建数据)
db.Create(&Product{Code: "D42", Price: 100}) //创建多条数据传入切片
// Read(查询数据)
var product Product
db.First(&product, 1) // 根据整型主键查找
//传入指针,因为Grom要将查到的字段反写入结构体中(存在问题后面会介绍)
db.First(&product, "code = ?", "D42") // 查找 code 字段值为 D42 的记录
//(更新数据)
// Update - 将 product 的 price 更新为 200
//或者使用db.Table()
db.Model(&product).Update("Price", 200) //model()定义TableName()已经确定表名
// Update - 更新多个字段
db.Model(&product).Updates(Product{Price: 200, Code: "F42"}) // 仅更新非零值字段
db.Model(&product).Updates(map[string]interface{}{"Price": 200, "Code": "F42"}) //更新零值
//(删除数据)
// Delete - 删除 product
db.Delete(&product, 1)
db.Delete(&product, "code = ?", "D42")
}
2.GORM支持的数据库
Gorm 官方支持的数据库类型有:MySQL(MariaDB), PostgreSQL, SQlite, SQL Server。
import (
"gorm.io/driver/sqlserver"
"gorm.io/gorm"
)
//连接SQLServer数据库
dsn := "sqlserver://gorm:LoremIpsum86@localhost:9930?database=gorm"
db,err := gorm.Open(sqlserver.Open(dsn),&gorm.Config{})
GORM 通过驱动来连接数据库,如果需要连接其他类型的数据库,可以复用/自行开发驱动.
什么是DSN
Data Source Name (DSN)的PDO命名惯例为:PDO驱动程序的名称,后面为一个冒号,再后面是可选的驱动程序连接数据库变量信息,如主机名、端口和数据库名。
下面以MySQL为例:mysql:host=localhost;dbname=testdb。
- DSN 前缀 : 前缀是mysql0
- host : 主机上的数据库服务器
- port : 主机上数据库服务器监听的端口号。
- dbname : 数据库的名称。
- unix_socket : MySQL的UNIX套接字(不应该被用于主机或端口)。
- charset : 字符集设置,请参考字符集设置.
3. GORM创建数据
import (
"fmt"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
type Product struct { //定义gorm model
ID uint `gorm:"primarykey"`
Code string `gorm:"column:code"`
Price uint `gorm:"column:user_id"`
}
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{}) //连接数据库
if err != nil {
panic("failed to connect database" + fmt.Sprintf("%s", err))
}
p := &Product{
Code: "D42",
}
//db.Clauses(clause.OnConflict{DoNothing: true}).Create(&p)
res := db.Create(p)
fmt.Println(res.Error) //获取err
fmt.Println(p.ID) //返回插入数据的主键
Products := []*Product{{Code: "D41"}, {Code: "D42"}, {Code: "D43"}}
res = db.Create(Products)
fmt.Println(res.Error) //获取err
for _, p := range Products {
fmt.Println(p)
}
}
1.表的名字
- 结构体的名字为表的名字,如果为单词,则结构体名字的复数为表的名字
- 修改表名字,使用下面的函数,继承接口就能修改表名
func (p Product) TableName() string {
return "user"
}
2.db.create(val interface{}) *gorm.DB
接收一个结构体指针,将结构体中的数据插入表中,并将表的主键反写回结构体中,返回一个gorm.
gorm常用两个成员:
- gorm.DB.Error // 返回 error
- gorm.DB.RowsAffected // 返回插入记录的条数
3.如何使用Upset
//以不冲突为例,创建一条数据
//使用clause.OnConflict处理数据冲突
p := &Product{
Code: "D42",
ID: 1,
}
db.Clauses(clause.OnConflict{DoNothing: true}).Create(&p)
DoNothing : true: 遇到冲突时什么都不做- create()等执行sql的函数后面调用create()等函数,后面的函数不会生效,因为第一个create()时sql已经执行了
4, 如何使用默认值
type Product struct {
ID uint
Code string `gorm:"default:galeone"` // 不要添加多余的空格
Price uint `gorm:"default:18"`
}
5.选定字段的来创建
- 选定字段创建
db.Select("Name", "Age", "CreatedAt").Create(&user)
// INSERT INTO `users` (`name`,`age`) VALUES ("jinzhu", 18)
- 排除选定字段
db.Omit("Age").Create(&user)
// INSERT INTO `users` (`id`,`Name`) VALUES (1,"jinzhu")
4.GORM查询数据
import (
"fmt"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
type User struct {
ID int64
Name string `gorm:"default:galeone"`
Age int64 `gorm:"default:18"`
}
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{}) //连接数据库
if err != nil {
panic("failed to connect database" + fmt.Sprintf("%s", err))
}
//获取第一条记录(主键升序),查询不到数据则返回 ErrRecordNotFound
u := &User{}
db.First(u) // select * from users ORDER BY id LIMIT 1
//查询多条数据
users := make([]*User, 0)
result := db.Where("age > 10").Find(&users) // select * from users where age > 10;
fmt.Println(result.RowsAffected) //返回找到的记录数,相当于len(users)
fmt.Println(result.Error) //returns error
//IN : SELECT * FROM user WHERE name IN ('jinzhu','jinzhu 2');
db.Where("name IN ?", []string{"jinzhu", "jinzhu 2"}).Find(&users)
//LIKE : SELECT * FROM users WHERE name LIKE '%jin%';
db.Where("name LIKE ?", "%jin%").Find(&users)
//AND : 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)
}
1.First(),Find(),Take(),Last()
First()获取第一条记录(主键升序)Find()获取符合条件的记录(可以是多条)Take()获取一条记录,没有指定排序字段Last()获取最后一条记录(主键降序)- 使用First时,需要注意查询不到数据则返回
ErrRecordNotFound - 检查 ErrRecordNotFound 错误 :
errors.Is(result.Error, gorm.ErrRecordNotFound) - 使用Find查询多条数据,查询不到数据不会返回错误
u := &User{}
db.First(u)
users := make([]*User, 0)
result := db.Where("age > 10").Find(&users)
- 使用Find可以使用
db.Where()设置查询条件,将返回的结果保存起来,再Find(),或者直接链式调用db.Where().Find() db.Find(u)效果等于db.Take(u)
2.Find()的返回值result
- 类型为
*gorm.DB - result.RowsAffected //返回找到的记录数,相当于
len(users) - result.Error //返回错误
3.Find()复杂查询条件
db.Where("name = ? AND age >= ?", "jinzhu", "22").Find(&users)
db.Where(map[string]interface{}{"Name": "jinzhu", "Age": 0}).Find(&users)
result := map[string]interface{}{}
tx = db.Model(&User{}).First(&result)
- where()的参数是可变长的
- 将需要查询的值用?代替,并在后面按照顺序插入字符串替代?的位置
- 可以使用map来传入查询条件
- 也可以使用result(定义为 map[string]interface{}{})来接受结果,先使用model(&User{}),告诉Gorm result以User{}的形式接收数据,再调用First(&result)接收数据.
- 如果model 类型没有定义主键,则按第一个字段排序
- model()会在后面更新数据中讲
4.使用结构体为查询条件
当使用结构体作为条件查询时,GORM只会查询非零值字段.这意味着如果您的字段值为0,"",false或者其他零值,该字段不会被用于构建查询条件,建议使用Map来构建查询条件或者使用db.Select()的api.
- GORM默认关闭全局更新与全局删除
数据库中存在的数据:
//错误:只有name条件生效,age不被作为查询的依据
tx = db.Where(&User{Name: "jinzhu", Age: 0}).Find(&users)
fmt.Println(tx.RowsAffected)
for _, x := range users {
fmt.Println(x)
}
//正确:
tx = db.Where("name = ? AND age = ?", "jinzhu", "0").Find(&users)
fmt.Println(tx.RowsAffected)
for _, x := range users {
fmt.Println(x)
}
//正确:
tx = db.Where(map[string]interface{}{"Name": "jinzhu", "Age": 0}).Find(&users)
fmt.Println(tx.RowsAffected)
for _, x := range users {
fmt.Println(x)
}
查询结果:
当使用结构体时,也可以使用select()函数来挑选字段使用零值的查询条件.
select()
- select()在查询、创建、更新时选择指定你想要的字段
- 当您只需要字段的子集时使用选择。 默认情况下,GORM 将选择所有字段。 Select 接受字符串参数和数组。
// Select name and age of user using multiple arguments
db.Select("name", "age").Find(&users)
// Select name and age of user using an array
db.Select([]string{"name", "age"}).Find(&users)
- 使用select()也可以挑选字段用来作为查询条件
tx = db.Select(&User{Name: "jinzhu", Age: 0}).Find(&users)
for _, x := range users {
fmt.Println(x)
}
查询结果:
5.GORM更新数据
type User struct {
ID int64
Name string `gorm:"default:galeone"`
Age int64 `gorm:"default:18"`
actived bool
}
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{}) //连接数据库
if err != nil {
panic("failed to connect database" + fmt.Sprintf("%s", err))
}
//条件更新单个列
//UPDATE users SET name='hello' WHERE age > 18;
db.Model(&User{ID: 111}).Where("age > ?", 18).Update("name", "hello")
// 更新多个列
// 根据'struct' 更新属性,只会更新非零值的字段
// UPDATE users SET name='hello',age = 18 where id = 111;
db.Model(&User{ID: 111}).Updates(User{Name: "hello", Age: 10})
// 根据map更新属性
// UPDATE users SET name='hello',age = 18,actived:false 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 where id = 111;
db.Model(&User{ID: 111}).Update("age", gorm.Expr("age * ? + ?", 2, 100))
}
- 如果User结构体实现了TableName接口,就选tableName实现的表名,否则就选蛇形复数
- Model()指定您要运行数据库操作的模型
// 将所有用户的名字更新为 `hello`
db.Model(&User{}).Update("name", "hello")
// 将ID为111的名字更新为 `hello`
db.Model(&User{ID: 111}).Update("name", "hello")
// 如果用户的主键是非空的,将使用它作为条件,然后只会将该用户的名字更新为 `hello`
db.Model(&user).Update("name", "hello")
-
update()更新单个列,不能传递单个列,必须使用Model()或者TableName()设置表名,否则会报错
-
updates()可以传入结构体修改列,因此Model()不是必须的,但是没有过滤条件,可以使用Model(&user)或者whele()添加过滤条件
-
updates()也可以使用map或者select()规避零值更新的问题,使用结构体更新会忽略零值
db.Model(&User{ID: 111}).Updates(map[string]interface{}{"name": "hello", "age": 18, "actived": false}) -
SQL 表达式更新
db.Model(&User{ID: 111}).Update("age", gorm.Expr("age * ? + ?", 2, 100))使用gorm.Expr()将参数传入clause.Expr{}结构体,用来以表达式的形式更新,等效于先查询要修改的值age再进行
db.Model(&User{ID: 111}).Update("age", age * 2 + 100)修改
6.GORM更删除数据
Delete() 删除符合给定条件的值。如果值包含主键,则它包含在条件中。如果值包含 deleted_at 字段,则 Delete 执行软删除,而不是通过将 deleted_at 设置为 null(如果为 null)来执行软删除。
1.物理删除
- 使用物理删除,user结构体需要注销字段:gorm.DeletedAt
- 此外软删除必须使用结构体的指针或者slice,否则会报错,物理删除不必
func delete1(db *gorm.DB) {
db.Delete(User{}, 10) // DELETE FROM users Where id = 10;
db.Delete(User{}, "10") // DELETE FROM users Where id = 10;
db.Delete(User{}, []int{1, 2, 3}) // DELETE FROM users WHERE id IN (1,2,3)
db.Where("name LIKE ?", "%jinzhu%").Delete(User{}) // DELETE FROM users WHERE name LIKE "%jinzhu%";
db.Delete(User{}, "name LIKE ?", "%jinzhu%") // DELETE FROM users WHERE name LIKE "%jinzhu%";
}
直接删除数据库中的内容,一般情况下应使用软删除.
2.软删除
- GORM提供了gorm.DeleteAt用于帮助用户实现软删除
- 拥有软删除能力的Model调用Delete时,记录不会从被数据库中真正的删除.但GORM会将DeleteAt置为当前时间,并且你不能再通过正常的查询方法找到该记录.
- 使用Unscoped可以查询到被软删的数据
import (
"fmt"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
type User struct {
ID int64
Name string `gorm:"default:galeone"`
Age int64 `gorm:"default:18"`
actived bool
Deleted gorm.DeletedAt `gorm:"column:deleted_at"` //软删除必须
}
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{}) //连接数据库
if err != nil {
panic("failed to connect database" + fmt.Sprintf("%s", err))
}
//软删除
//delete1(db)
u := &User{ID: 111} // u的id是111
db.Delete(&u) // DELETE FROM users Where id = 111;
//批量删除
tx := db.Where("age = ?", 20).Delete(&User{}) // DELETE FROM users Where age = 20;
fmt.Println(tx.RowsAffected)
users := make([]*User, 0)
//在查询时会忽略被软删除的记录
tx = db.Where("age = 20").Find(&users) // select * from users WHERE age = 20 and deleted_at IS NULL;
for _, x := range users {
fmt.Println(x) //不会输出
}
fmt.Println()
// 在查询时不会忽略被软删除的记录
tx = db.Unscoped().Where("age = 20").Find(&users) // select * from users WHERE age = 20;
for _, x := range users {
fmt.Println(x) //输出信息
}
}
软删除前:
软删除后:
6. GORM事务
- 事务 : 由一次或者多次基本操作构成,或者说,事务由一条或者多条 SQL 语句构成。 事务包含的所有 SQL 语句作为一个整体向数据库提交,只有所有的 SQL 语句都执行完成,整个事务才算成功,一旦某个 SQL 语句执行失败,整个事务就失败了。 事务失败后需要回滚所有的 SQL 语句。
- Gorm提供了Begin,Commit,Rollback方法用于使用事务
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{}) //连接数据库
if err != nil {
panic("failed to connect database" + fmt.Sprintf("%s", err))
}
tx := db.Begin() // 开始事务
// 在事务中执行一些db操作(从这里开始,您应该使用'tx'而不是'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()
}
-
db.begin()执行了两步操作:
- 固化连接,保证使用同一个连接,底层数据库操作使用的是连接池
- 执行开启sql的sql语句
-
Gorm提供了Tansaction方法用于自动提交事务,避免用户漏写Commit,Rollback
func TransactionTest(db gorm.DB) {
if err := db.Transaction(func(tx *gorm.DB) error {
if err := tx.Create(&User{Name: "name"}).Error; err != nil {
return err
}
if err := tx.Create(&User{Name: "name1"}).Error; err != nil {
tx.Rollback()
return err
}
return nil
}); err != nil {
return
}
}
- 返回err或者panic的时候会自动进行tx.Rollback(),返回nil的时候自动Commit().
- Tansaction方法的实现就是使用defer进行err和panic的拦截
7. GORM Hook
type User struct {
ID int64
Name string `gorm:"defer:goleone"`
Age int64 `gorm:"defer:18"`
}
type Email struct {
ID int64
Name string
Email string
}
func (u *User) BeforeCreate(tx *gorm.DB) (err error) {
if u.Age < 0 {
return errors.New("can't save invalid data")
}
return
}
func (u *User) AfterCreate(tx *gorm.DB) (err error) {
return tx.Create(&Email{
ID: u.ID,
Name: u.Name,
Email: u.Name + "@***,com",
}).Error
}
- GORM默认开了一个事务,Hook操作也在默认操作里,可以保证一致性,因此性能会有影响
- GORM在提供了CURD的Hook能力.
- Hook是在创建,查询,更新,删除等操作之前.之后自动调用的函数.
- 如果任何Hook返回错误,GORM将停止后续的操作并回滚事务
8. GORM性能提高
dsn := Const.User + ":" + Const.Pass + "@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
SkipDefaultTransaction: true, //关闭默认事务
PrepareStmt: true, //缓存预编译语句
}) //连接数据库
if err != nil {
panic("failed to connect database" + fmt.Sprintf("%s", err))
}
- 对于写操作(创建,更新,删除),为了确保数据的完整性,GORM会将它们封装在事务内运行.但这会降低性能,你可以用SkipDefaultTransaction关闭默认事务.
- 使用PrepareStmt缓存预编译语句可以提高后续调用的速度.
9.GORM生态
更多GROM用法请查看GORM的中文文档