GORM最佳实践笔记 | 青训营笔记

1,569 阅读6分钟

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

今天和大家分享一下GORM的最佳实践笔记。

最佳实践

数据序列化与SQL表达式

当我们使用不同的数据库时,难免会遇到一些复杂的数据结构序列化放入、查询数据库的情况。

对于创建和查询来说,GORM 允许使用 SQL 表达式插入数据,有两种方法实现这个目标。根据 map[string]interface{}自定义数据类型 创建或查询:

// 通过 map 创建记录
db.Model(User{}).Create(map[string]interface{}{
  "Name": "jinzhu",
  "Location": clause.Expr{SQL: "ST_PointFromText(?)", Vars: []interface{}{"POINT(100 100)"}},
})
// INSERT INTO `users` (`name`,`location`) VALUES ("jinzhu",ST_PointFromText("POINT(100 100)"));

自定义数据类型要求我们去自定义结构和实现 GORMValuer 方法:

type Location struct {
    X, Y int
}
​
func (loc Location) GormValue(ctx context.Context, db *gorm.DB) clause.Expr {
  return clause.Expr{
    SQL:  "ST_PointFromText(?)",
    Vars: []interface{}{fmt.Sprintf("POINT(%d %d)", loc.X, loc.Y)},
  }
}
​
db.Create(&User{
  Name:     "jinzhu",
  Location: Location{X: 100, Y: 100},
})
db.Model(&user).Update("Location", Location{X: 100, Y: 100})                

查询方式,我们也可以使用类似的方法:

// 方法1:使用gorm.Expr
tx := db.Where("location = ?", gorm.Expr("ST_PointFromText(?)", "POINT(100 100)")).First(&user)
// 方法2:GORMValuer
func (loc Location) GormValue(ctx context.Context, db *gorm.DB) clause.Expr {
    return gorm.Expr("ST_PointFromText(?)", fmt.Sprintf("POINT(100 100)", loc.X, loc.Y))
}
tx := db.Where("location = ?", Location{X: 100, Y: 100})
// 方法3:自定义查询 SQL 实现接口 clause.Expression 例子:https://github.com/go-gorm/gorm/blob/master/clause/where.go
// 方法3可以让我们更加灵活的书写sql,这种写法可以让我们在不同的数据库使用不同的sql查询方式

上面是对于一些数据库独有数据结构如何去存储,对于业务层面的复杂结构,我们也会需要去进行序列化操作。

常见的序列化问题包括:

  • 一个复杂的结构体,将其转为json保存在数据库中
  • 将int作为日期时间保存在数据库
  • 自定义需求,如将密码保存在数据库时,在最后序列化阶段完成加密操作

这时我们可以使用到 GORM 的 Serializer,它允许自定义如何使用数据库对数据进行序列化和反序列化

GORM 提供了一些默认的序列化器:json、gob、unixtime,这里有一个如何使用它的快速示例: 序列化 | GORM

如果需要自定义序列化器,我们需要去实现以下接口:

import "gorm.io/gorm/schema"type SerializerInterface interface {
    Scan(ctx context.Context, field *schema.Field, dst reflect.Value, dbValue interface{}) error
    SerializerValuerInterface
}
​
type SerializerValuerInterface interface {
    Value(ctx context.Context, field *schema.Field, dst reflect.Value, fieldValue interface{}) (interface{}, error)
}

批量数据创建/查询

批量创建,我们可以使用到CreateCreateInBatches

创建 | GORM - The fantastic ORM library for Golang, aims to be developer friendly.

批量查询,我们可以使用 Rows 方法。该方法会返回一个迭代器:

rows, err := db.Model(&User{}).Where("role = ?", "admin").Rows()
for rows.Next() {
    rows.Scan(&name, &age, &email) //或 db.ScanRows(rows, &user)
}

或者也可以使用 FindInBatches 方法批量查询(适用于有很多条,防止查询出错时)。

tips:

批量查询时难免会遇到速度问题,我们可以使用以下几种方法加速:

  1. 从 整体级别 或 会话级别 关闭默认事务( SkipDefaultTransaction ),来提速批量新增和修改更加快速;

  2. 默认批量增加会调用 Hooks 方法,使用 SkipHooks 跳过(在进行批量导入数据时,某些钩子并不是很重要):

    DB.Session(&gorm.Session{SkipHooks: true}).CreateInBatches(users, 1000)
    
  3. 开启 sql 预编译缓存

  4. 混合使用:

    // 会话级别
    tx := db.Session(&gorm.Session{
        SkipHooks: true, SkipDefaultTransaction: true, PrepareStmt: true, CreateBatchSize: 1000,
    })
    tx.Create(&users) // 会自动每1000条一次创建
    

代码复用

gorm 支持将一些代码逻辑抽离出来,并使用 Scopes 方法进行代码复用。Scopes

分页逻复用使用实例:分页

在同一个连接下,我们有时会进行分库分表,scope 也可以实现分库分表(动态表):动态表

Sharding

Sharding 是一个高性能的 Gorm 分表中间件。它基于 Conn 层做 SQL 拦截、AST 解析、分表路由、自增主键填充,带来的额外开销极小。

例如上面的分库分表(同一个连接下),我们可以使用 Sharding 为其创建一些固定的逻辑和策略来支持动态表:Sharding

混沌工程/压测

为了测试系统抗压性和安全性,混沌工程和压力测试是很重要的。

混沌工程

实现混沌工程,我们可以在创建数据库连接时,通过编写插件的形式。通过提供一些插入,更新,查询等操作的错误更改,来查看系统本身是否可以监测并反制。

压测

gorm 提供了 withContext 来设置上下文,我们可以将一些关于数据库的配置放在上下文中,当我们进行压力测试时,根据上下文中的信息,及时切换测试数据库和正是数据库:Context

Logger

详情见:Logger

可以注意到的是,这里的Logger也是可以根据需要,切换成全局模式或者会话模式。

数据库迁移

gorm 可以使用结构体自动迁移数据库:

db.AutoMigrate(&User{})

如果我们需要更多地迁移数据库的配置,那么也可以使用一些数据库版本定制,迁移工具等,例如 gormigrate package - github.com/go-gormigrate/gormigrate/v2 - Go Packages

gorm 本身也提供了一个 Migrator 的 interface,可以利用该接口实现相关方法来自定义迁移。

另外还有 ColumnType 接口用来获取列类型来辅助管理数据库。

Row SQL (原生SQL)和 命名参数

我们可以利用 RowExec 方法来写原生 sql 语句。SQL 构建器

gorm 同时提供了命名参数来支持 sql.NamedArgmap[string]interface{}{} 或 struct 形式的命名参数: 命名参数

代码生成 Gen

对于上面的原生SQL书写,我们也可以将其抽离出来,包装成一个函数并生成自己的代码使用:

Gen Guides | GORM - The fantastic ORM library for Golang, aims to be developer friendly.

安全问题

gorm 默认使用参数的方式传入数据,这样的写法不会产生sql注入问题。

但是如果我们将用户的输入来直接拼接到查询中,这样可能会出现sql注入问题:

sql := fmt.Sprintf("name = %v", userInput)
db.where(sql).First(&user) // 可能产生注入

更高的性能

通过 GORM 会增加 ORM 语句解析成 sql 语句的消耗,可以通过开启 statement 预编译将使用过的解析语句缓存起来,加快执行速度。

另一方面,我们可以使用关闭默认事务配置 SkipDefaultTransaction: true 让 gorm 中的写操作跳过内置的默认封装事务过程来提高性能。

全局模式:
db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{
    PrepareStmt: true,
    SkipDefaultTransaction: true
})
会话模式:
tx := db.Session(&Session{PrepareStmt: true})

此外,还可以在 DSN 中加入 interpolateParams=false 这一设置,默认为true是为了防止传统字符集下 sql 注入的问题,但是这个设置会导致我们请求次数增加(一次请求变成了 执行前预编译 -> 调用预编译 -> 关闭预编译 三个请求),但现在使用 UTF-8编码 的情况下,已经不需要这一功能了,将其设为 false 性能更高。