gorm 创建/查询数据与冲突、拼接条件/链式调用、事务、hook、多表查询、hertz框架 | 青训营笔记

1,507 阅读7分钟

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

gorm 创建数据

创建记录

user := User{Name: "Jinzhu", Age: 18, Birthday: time.Now()}
result := db.Create(&user) // 通过数据的指针来创建

user.ID             // 返回插入数据的主键
result.Error        // 返回 error
result.RowsAffected // 返回插入记录的条数

选定字段创建--用选定字段的来创建

db.Select("Name", "Age", "CreatedAt").Create(&user)
// 等价于
//INSERT INTO `users` (`name`,`age`,`created_at`) VALUES ("jinzhu", 18, "2020-07-04 11:05:21.775")

创建时排除选定字段

db.Omit("Name", "Age", "CreatedAt").Create(&user)
// 等价于
// INSERT INTO `users` (`birthday`,`updated_at`) VALUES ("2020-01-01 00:00:00.000", "2020-07-04 11:05:21.775")

冲突处理

创建数据时,若不处理冲突,则需要在create操作前(任意写操作都要)加Clauses,把clause.OnConflict{DoNothing: true})属性标注为true。如图所示: image.png

不处理冲突:


import "gorm.io/gorm/clause"

// 不处理冲突
DB.Clauses(clause.OnConflict{DoNothing: true}).Create(&user)

处理冲突:

import "gorm.io/gorm/clause"
// `id` 冲突时,将字段值更新为默认值
DB.Clauses(clause.OnConflict{
  Columns:   []clause.Column{{Name: "id"}},
  DoUpdates: clause.Assignments(map[string]interface{}{"role": "user"}),
}).Create(&users)
// MERGE INTO "users" USING *** WHEN NOT MATCHED THEN INSERT *** WHEN MATCHED THEN UPDATE SET ***; SQL Server
// INSERT INTO `users` *** ON DUPLICATE KEY UPDATE ***; MySQL

// Update columns to new value on `id` conflict
DB.Clauses(clause.OnConflict{
  Columns:   []clause.Column{{Name: "id"}},
  DoUpdates: clause.AssignmentColumns([]string{"name", "age"}),
}).Create(&users)
// MERGE INTO "users" USING *** WHEN NOT MATCHED THEN INSERT *** WHEN MATCHED THEN UPDATE SET "name"="excluded"."name"; SQL Server
// INSERT INTO "users" *** ON CONFLICT ("id") DO UPDATE SET "name"="excluded"."name", "age"="excluded"."age"; PostgreSQL
// INSERT INTO `users` *** ON DUPLICATE KEY UPDATE `name`=VALUES(name),`age=VALUES(age); MySQL

GORM 支持根据 map[string]interface{} 和 []map[string]interface{}{} 创建记录

例如:

DB.Model(&User{}).Create(map[string]interface{}{
  "Name": "jinzhu", "Age": 18,
})

// 根据 `[]map[string]interface{}{}` 批量插入
DB.Model(&User{}).Create([]map[string]interface{}{
  {"Name": "jinzhu_1", "Age": 18},
  {"Name": "jinzhu_2", "Age": 20},
}) 

注意,使用 Struct 更新时,只会更新非零值,如果需要更新零值可以使用 Map 更新或使用Select 选择字段。

gorm查询数据

first方法,只返回一个

result := db.First(&user)
result.RowsAffected // 返回找到的记录数
result.Error        // returns error

db.where.find语句比较常用,where里拼接查询条件,示例如下:

image.png

// 获取第一条匹配的记录
db.Where("name = ?", "jinzhu").First(&user)
// SELECT * FROM users WHERE name = 'jinzhu' ORDER BY id LIMIT 1;

// 获取全部匹配的记录
db.Where("name <> ?", "jinzhu").Find(&users)
// SELECT * FROM users WHERE name <> 'jinzhu';

// IN,数组
db.Where("name IN ?", []string{"jinzhu", "jinzhu 2"}).Find(&users)
// SELECT * FROM users WHERE name IN ('jinzhu','jinzhu 2');

// LIKE
db.Where("name LIKE ?", "%jin%").Find(&users)
// SELECT * FROM users WHERE name LIKE '%jin%';

// AND
db.Where("name = ? AND age >= ?", "jinzhu", "22").Find(&users)
// SELECT * FROM users WHERE name = 'jinzhu' AND age >= 22;

// BETWEEN
db.Where("created_at BETWEEN ? AND ?", lastWeek, today).Find(&users)
// SELECT * FROM users WHERE created_at BETWEEN '2000-01-01 00:00:00' AND '2000-01-08 00:00:00';
 

注意: 链式调用时,.where拼sql语句的条件,直到.find/.first/.create/.update等执行语句就已经执行sql语句了,这时候后面再拼的条件会对此句无效,且此句的条件会清空对后面的sql语句也无效。

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;

// 主键切片条件
db.Where([]int64{20, 21, 22}).Find(&users)
// SELECT * FROM users WHERE id IN (20, 21, 22);

注意当使用结构Struct作为条件查询时,GORM 只会查询非零值字段。这意味着如果您的字段值为 0、false 或其他零值,该字段不会被用于构建查询条件,例如:

db.Where(&User{Name: "jinzhu", Age: 0}).Find(&users)
// SELECT * FROM users WHERE name = "jinzhu";

您可以使用 map 来构建查询条件,例如:

db.Where(map[string]interface{}{"Name": "jinzhu", "Age": 0}).Find(&users)
// SELECT * FROM users WHERE name = "jinzhu" AND age = 0; 

gorm 事务

gorm事务得四项基本操作如下所示:

// 开始事务
tx := db.Begin()

// 在事务中执行一些 db 操作(从这里开始,您应该使用 'tx' 而不是 'db')
tx.Create(...)

// ...

// 遇到错误时回滚事务
tx.Rollback()

// 否则,提交事务
tx.Commit()

Gorm 提供了 Begin、Commit、Rollback 方法用于使用事务。db.Begin()包含固化数据库链接和开启sql语句两个操作,执行tx := db.Begin()之后,在事务中执行一些 db 操作应该使用 'tx' 而不是 'db'。但是此方法执行过于繁琐,每一次sql操作都要加入上述语句,故Gorm 提供了 Tansaction 方法用于自动提交事务,避免用户漏写 Commit、Rollbcak,事务模板示例如下

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
})

事务例程如下:

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 { //if条件里直接执行sql操作tx.Create(&Animal{Name: "Giraffe"}).Error,检测是否返回错误err,如果err非空则在if函数里回滚。
     tx.Rollback()//不用加这句,go会自动检测拦截err回滚。
     return err
  }

  if err := tx.Create(&Animal{Name: "Lion"}).Error; err != nil {
     tx.Rollback()//不用加这句,go会自动检测拦截err回滚。
     return err
  }

  return tx.Commit().Error
}

上面的代码在if条件里直接执行sql操作 tx.Create(&Animal{Name: "Giraffe"}).Error,检测是否返回错误err,如果err非空则在if函数里执行 tx.Rollback()回滚,不用加 tx.Rollback()这句也行,go会自动检测拦截err回滚。如果一直没有异常返回err,则一直到最后并提交commit。

事务例程二:

image.png 当然,也可以像这张图的代码把 Tansaction 方法写到if条件判断语句里。先在if条件语句定义并执行db.Tansaction函数,赋值到err,再在最后两行判断err非空,如果返回err,则会自动回滚。优雅地实现Tansaction。

Gorm hook

GORM 在 提供了 CURD 的 Hook 能力。Hook 是在创建、查询、更新、删除等操作之前、之后自动调用的函数。如果任何 Hook 返回错误,GORM 将停止后续的操作并回滚事务。如果您已经为模型定义了指定的方法,它会在创建、更新、查询、删除时自动被调用。

在 GORM 中保存、删除操作会默认运行在事务上, 因此在事务完成之前该事务中所作的更改是不可见的,如果您的钩子返回了任何错误,则修改将被回滚。如果任何回调返回错误,GORM 将停止后续的操作并回滚事务。

示例:

// 开始事务 
BeforeSave 
BeforeCreate 
// 关联前的 save 
// 插入记录至 db 
// 关联后的 save 
AfterCreate 
AfterSave 
// 提交或回滚事务 

例程1:

func (u *User) BeforeCreate(tx *gorm.DB) (err error) {
  u.UUID = uuid.New()

  if !u.IsValid() {
    err = errors.New("can't save invalid data")
  }
  return
}

func (u *User) AfterCreate(tx *gorm.DB) (err error) {
  if u.ID == 1 {
    tx.Model(u).Update("role", "admin")
  }
  return
}

示例2:

image.png

如果不用写操作关联或者hook前后关联时,一般关闭默认事务以提高性能:

image.png 如图,对于写操作(创建、更新、删除),为了确保数据的完整性,GORM 会将它们自动创建默认事务,封装在事务内运行,但这会降低性能。如果不用写操作关联(几步写操作关联在一起)或者hook前后关联时,一般关闭默认事务以提高性能。可以使用 SkipDefaultTransaction 关闭默认事务。如上图所示。

如图,使用 PrepareStmt 缓存预编译语句可以提高后续调用的速度,本机测试提高大约 35 %左右。

多表连接查询

image.png

for循环分次查询再拼接可以,for循环写操作不行。数据量百万级别用读写分离/分表,几百万用分片库方案,例如go sharding。部分go生态工具如下:

image.png

HTTP框架

中间件

中间件.use可以注册全局或路由组或指定路由。示例如下:

//指定路由
  r.POST("/login", authMiddleware.LoginHandler)
  r.NoRoute(authMiddleware.MiddlewareFunc(), func(c *gin.Context) {
    claims := jwt.ExtractClaims(c)
    log.Printf("NoRoute claims: %#v\n", claims)
    c.JSON(404, gin.H{"code": "PAGE_NOT_FOUND", "message": "Page not found"})
  })
  
//路由组
  auth := r.Group("/auth")
  auth.Use(authMiddleware.MiddlewareFunc())
  {
    auth.GET("/hello", helloHandler)
  }

模拟客户端请求

GET请求:

context.Background()用于传递上下文

dst:nil是保留字段,如果需要传递可复用的字节切片可以存在这里

c, err := client.NewClient()
if err != nil {
    return
}
// send http get request
status, body, _ := c.Get(context.Background(), dst:nil, "https://www.example.com")
fmt.Printf("status=%v body=%v\n", status, string(body))
// dst:nil是保留字段,如果需要传递可复用的字节切片可以存在这里

POST请求:

protocol.Args用于携带的参数,.set设置参数,.get取参数

c, err := client.NewClient()
if err != nil {
    return
}
// send http post request
var postArgs protocol.Args
postArgs.Set("arg","a") // Set post args
status, body, _ = c.Post(context.Background(), nil, "https://www.example.com", &postArgs)
fmt.Printf("status=%v body=%v\n", status, string(body))