这是我参与「第五届青训营 」伴学笔记创作活动的第 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。如图所示:
不处理冲突:
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里拼接查询条件,示例如下:
// 获取第一条匹配的记录
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。
事务例程二:
当然,也可以像这张图的代码把 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:
如果不用写操作关联或者hook前后关联时,一般关闭默认事务以提高性能:
如图,对于写操作(创建、更新、删除),为了确保数据的完整性,GORM 会将它们自动创建默认事务,封装在事务内运行,但这会降低性能。如果不用写操作关联(几步写操作关联在一起)或者hook前后关联时,一般关闭默认事务以提高性能。可以使用 SkipDefaultTransaction 关闭默认事务。如上图所示。
如图,使用 PrepareStmt 缓存预编译语句可以提高后续调用的速度,本机测试提高大约 35 %左右。
多表连接查询
for循环分次查询再拼接可以,for循环写操作不行。数据量百万级别用读写分离/分表,几百万用分片库方案,例如go sharding。部分go生态工具如下:
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))