GORM补充

DBResolver

DBResolver 为 GORM 提供了多个数据库支持,支持以下功能:

  • 支持多个 sources、replicas
  • 读写分离
  • 根据工作表、struct 自动切换连接
  • 手动切换连接
  • Sources/Replicas 负载均衡
  • 适用于原生 SQL
  • Transaction

https://github.com/go-gorm/dbresolver

用法

import (
  "gorm.io/gorm"
  "gorm.io/plugin/dbresolver"
  "gorm.io/driver/mysql"
)

db, err := gorm.Open(mysql.Open("db1_dsn"), &gorm.Config{})

db.Use(dbresolver.Register(dbresolver.Config{
  // `db2` 作为 sources,`db3`、`db4` 作为 replicas
  Sources:  []gorm.Dialector{mysql.Open("db2_dsn")},
  Replicas: []gorm.Dialector{mysql.Open("db3_dsn"), mysql.Open("db4_dsn")},
  // sources/replicas 负载均衡策略
  Policy: dbresolver.RandomPolicy{},
}).Register(dbresolver.Config{
  // `db1` 作为 sources(DB 的默认连接),对于 `User`、`Address` 使用 `db5` 作为 replicas
  Replicas: []gorm.Dialector{mysql.Open("db5_dsn")},
}, &User{}, &Address{}).Register(dbresolver.Config{
  // `db6`、`db7` 作为 sources,对于 `orders`、`Product` 使用 `db8` 作为 replicas
  Sources:  []gorm.Dialector{mysql.Open("db6_dsn"), mysql.Open("db7_dsn")},
  Replicas: []gorm.Dialector{mysql.Open("db8_dsn")},
}, "orders", &Product{}, "secondary"))

Automatic connection switching

DBResolver will automatically switch connection based on the working table/struct

For RAW SQL, DBResolver will extract the table name from the SQL to match the resolver, and will use sources unless the SQL begins with SELECT (excepts SELECT... FOR UPDATE), for example:

// `User` Resolver 示例
db.Table("users").Rows() // replicas `db5`
db.Model(&User{}).Find(&AdvancedUser{}) // replicas `db5`
db.Exec("update users set name = ?", "jinzhu") // sources `db1`
db.Raw("select name from users").Row().Scan(&name) // replicas `db5`
db.Create(&user) // sources `db1`
db.Delete(&User{}, "name = ?", "jinzhu") // sources `db1`
db.Table("users").Update("name", "jinzhu") // sources `db1`

// Global Resolver 示例
db.Find(&Pet{}) // replicas `db3`/`db4`
db.Save(&Pet{}) // sources `db2`

// Orders Resolver 示例
db.Find(&Order{}) // replicas `db8`
db.Table("orders").Find(&Report{}) // replicas `db8`

Read/Write Splitting

Read/Write splitting with DBResolver based on the current used GORM callbacks.

For Query, Row callback, will use replicas unless Write mode specified For Raw callback, statements are considered read-only and will use replicas if the SQL starts with SELECT

主库sources 负责写

从库replicas 负责读

实现读写分离

Manual connection switching

// 使用 Write 模式:从 sources db `db1` 读取 user
db.Clauses(dbresolver.Write).First(&user)

// 指定 Resolver:从 `secondary` 的 replicas db `db8` 读取 user
db.Clauses(dbresolver.Use("secondary")).First(&user)

// 指定 Resolver 和 Write 模式:从 `secondary` 的 sources db `db6` 或 `db7` 读取 user
db.Clauses(dbresolver.Use("secondary"), dbresolver.Write).First(&user)

Transaction

When using transaction, DBResolver will keep using the transaction and won’t switch to sources/replicas based on configuration

But you can specifies which DB to use before starting a transaction, for example:

// Start transaction based on default replicas db
tx := DB.Clauses(dbresolver.Read).Begin()

// Start transaction based on default sources db
tx := DB.Clauses(dbresolver.Write).Begin()

// Start transaction based on `secondary`'s sources
tx := DB.Clauses(dbresolver.Use("secondary"), dbresolver.Write).Begin()

负载均衡

GORM supports load balancing sources/replicas based on policy, the policy should be a struct implements following interface:

type Policy interface {
    Resolve([]gorm.ConnPool) gorm.ConnPool
}

Currently only the RandomPolicy implemented and it is the default option if no other policy specified.

连接池

db.Use(
  dbresolver.Register(dbresolver.Config{ /* xxx */ }).
  SetConnMaxIdleTime(time.Hour).
  SetConnMaxLifetime(24 * time.Hour).
  SetMaxIdleConns(100).
  SetMaxOpenConns(200)
)

Sharding

Sharding 是一个高性能的 Gorm 分表中间件。它基于 Conn 层做 SQL 拦截、AST 解析、分表路由、自增主键填充,带来的额外开销极小。对开发者友好、透明,使用上与普通 SQL、Gorm 查询无差别,只需要额外注意一下分表键条件。 为您提供高性能的数据库访问。

https://github.com/go-gorm/sharding

功能特点

  • 非侵入式设计, 加载插件,指定配置,既可实现分表。
  • 轻快, 非基于网络层的中间件,像 Go 一样快
  • 支持多种数据库。 PostgreSQL 已通过测试,MySQL 和 SQLite 也在路上。
  • 多种主键生成方式支持(Snowflake, PostgreSQL Sequence, 以及自定义支持)Snowflake 支持从主键中确定分表键。

使用说明

配置 Sharding 中间件,为需要分表的业务表定义他们分表的规则。 查看 Godoc 获取配置详情。

import (
    "fmt"

    "gorm.io/driver/postgres"
    "gorm.io/gorm"
    "gorm.io/sharding"
)

dsn := "postgres://localhost:5432/sharding-db?sslmode=disable"
db, err := gorm.Open(postgres.New(postgres.Config{DSN: dsn}))

db.Use(sharding.Register(sharding.Config{
    ShardingKey:         "user_id",
    NumberOfShards:      64,
    PrimaryKeyGenerator: sharding.PKSnowflake,
}, "orders").Register(sharding.Config{
    ShardingKey:         "user_id",
    NumberOfShards:      256,
    PrimaryKeyGenerator: sharding.PKSnowflake,
    // This case for show up give notifications, audit_logs table use same sharding rule.
}, Notification{}, AuditLog{}))

依然保持原来的方式使用 db 来查询数据库。 你只需要注意在 CURD 动作的时候,明确知道 Sharding Key 对应的分表,查询条件带 Sharding Key,以确保 Sharding 能理解数据需要对应到哪一个子表。

// GORM 创建示例,这会插入到 orders_02 表
db.Create(&Order{UserID: 2})
// sql: INSERT INTO orders_2 ...

// 原生 SQL 插入示例,这会插入到 orders_03 表
db.Exec("INSERT INTO orders(user_id) VALUES(?)", int64(3))

// 这会抛出 ErrMissingShardingKey 错误,因此此处没有提供 sharding key
db.Create(&Order{Amount: 10, ProductID: 100})
fmt.Println(err)

// Find 方法,这会检索 order_02 表
var orders []Order
db.Model(&Order{}).Where("user_id", int64(2)).Find(&orders)
fmt.Printf("%#v\n", orders)

// 原生 SQL 也是支持的
db.Raw("SELECT * FROM orders WHERE user_id = ?", int64(3)).Scan(&orders)
fmt.Printf("%#v\n", orders)

// 这会抛出 ErrMissingShardingKey 错误,因为 WHERE 条件没有包含 sharding key
err = db.Model(&Order{}).Where("product_id", "1").Find(&orders).Error
fmt.Println(err)

// Update 和 Delete 方法与创建、查询类似
db.Exec("UPDATE orders SET product_id = ? WHERE user_id = ?", 2, int64(3))
err = db.Exec("DELETE FROM orders WHERE product_id = 3").Error
fmt.Println(err) // ErrMissingShardingKey

完整示例演示 Example

数据库索引

GORM 允许通过 indexuniqueIndex 标签创建索引,这些索引将在使用 GORM 进行AutoMigrate 或 Createtable 时创建

索引标签

GORM 可以接受很多索引设置,例如classtypewherecommentexpressionsortcollateoption

下面的示例演示了如何使用它:

type User struct {
    Name  string `gorm:"index"`
    Name2 string `gorm:"index:idx_name,unique"`
    Name3 string `gorm:"index:,sort:desc,collate:utf8,type:btree,length:10,where:name3 != 'jinzhu'"`
    Name4 string `gorm:"uniqueIndex"`
    Age   int64  `gorm:"index:,class:FULLTEXT,comment:hello \\, world,where:age > 10"`
    Age2  int64  `gorm:"index:,expression:ABS(age)"`
}

// MySQL 选项
type User struct {
    Name string `gorm:"index:,class:FULLTEXT,option:WITH PARSER ngram INVISIBLE"`
}

// PostgreSQL 选项
type User struct {
    Name string `gorm:"index:,option:CONCURRENTLY"`
}

唯一索引

uniqueIndex` 标签的作用与 `index` 类似,它等效于 `index:,unique
type User struct {
    Name1 string `gorm:"uniqueIndex"`
    Name2 string `gorm:"uniqueIndex:idx_name,sort:desc"`
}

复合索引

两个字段使用同一个索引名将创建复合索引,例如:

// create composite index `idx_member` with columns `name`, `number`
type User struct {
    Name   string `gorm:"index:idx_member"`
    Number string `gorm:"index:idx_member"`
}

字段优先级

复合索引列的顺序会影响其性能,因此必须仔细考虑

您可以使用 priority 指定顺序,默认优先级值是 10,如果优先级值相同,则顺序取决于模型结构体字段的顺序

type User struct {
    Name   string `gorm:"index:idx_member"`
    Number string `gorm:"index:idx_member"`
}
// column order: name, number

type User struct {
    Name   string `gorm:"index:idx_member,priority:2"`
    Number string `gorm:"index:idx_member,priority:1"`
}
// column order: number, name

type User struct {
    Name   string `gorm:"index:idx_member,priority:12"`
    Number string `gorm:"index:idx_member"`
}
// column order: number, name

Shared composite indexes

If you are creating shared composite indexes with an embedding struct, you can’t specify the index name, as embedding the struct more than once results in the duplicated index name in db.

In this case, you can use index tag composite, it means the id of the composite index. All fields which have the same composite id of the struct are put together to the same index, just like the original rule. But the improvement is it lets the most derived/embedding struct generates the name of index by NamingStrategy. For example:

type Foo struct {
  IndexA int `gorm:"index:,unique,composite:myname"`
  IndexB int `gorm:"index:,unique,composite:myname"`
}

If the table Foo is created, the name of composite index will be idx_foo_myname.

type Bar0 struct {
  Foo
}

type Bar1 struct {
  Foo
}

Respectively, the name of composite index is idx_bar0_myname and idx_bar1_myname.

composite only works if not specify the name of index.

多索引

A field accepts multiple index, uniqueIndex tags that will create multiple indexes on a field

type UserIndex struct {
    OID          int64  `gorm:"index:idx_id;index:idx_oid,unique"`
    MemberNumber string `gorm:"index:idx_id"`
}

约束

GORM 允许通过标签创建数据库约束,约束会在通过 GORM 进行 AutoMigrate 或创建数据表时被创建。

检查约束

通过 check 标签创建检查约束

type UserIndex struct {
    Name  string `gorm:"check:name_checker,name <> 'jinzhu'"`
    Name2 string `gorm:"check:name <> 'jinzhu'"`
    Name3 string `gorm:"check:,name <> 'jinzhu'"`
}

索引约束

查看 数据库索引 获取详情

外键约束

GORM 会为关联创建外键约束,您可以在初始化过程中禁用此功能:

db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{
  DisableForeignKeyConstraintWhenMigrating: true,
})

GORM 允许您通过 constraint 标签的 OnDeleteOnUpdate 选项设置外键约束,例如:

type User struct {
  gorm.Model
  CompanyID  int
  Company    Company    `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"`
  CreditCard CreditCard `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"`
}

type CreditCard struct {
  gorm.Model
  Number string
  UserID uint
}

type Company struct {
  ID   int
  Name string
}

复合主键

通过将多个字段设为主键,以创建复合主键,例如:

type Product struct {
  ID           string `gorm:"primaryKey"`
  LanguageCode string `gorm:"primaryKey"`
  Code         string
  Name         string
}

**注意:**默认情况下,整型 PrioritizedPrimaryField 启用了 AutoIncrement,要禁用它,您需要为整型字段关闭 autoIncrement

type Product struct {
  CategoryID uint64 `gorm:"primaryKey;autoIncrement:false"`
  TypeID     uint64 `gorm:"primaryKey;autoIncrement:false"`
}

安全

GORM 使用 database/sql 的参数占位符来构造 SQL 语句,这可以自动转义参数,避免 SQL 注入数据

注意 Logger 打印的 SQL 并不像最终执行的 SQL 那样已经转义,复制和运行这些 SQL 时应当注意。

查询条件

用户的输入只能作为参数,例如:

userInput := "jinzhu;drop table users;"

// 安全的,会被转义
db.Where("name = ?", userInput).First(&user)

// SQL 注入
db.Where(fmt.Sprintf("name = %v", userInput)).First(&user)

内联条件

// 会被转义
db.First(&user, "name = ?", userInput)

// SQL 注入
db.First(&user, fmt.Sprintf("name = %v", userInput))

当通过用户输入的整形主键检索记录时,你应该对变量进行类型检查。

userInputID := "1=1;drop table users;"
// 安全的,返回 err
id,err := strconv.Atoi(userInputID)
if err != nil {
    return error
}
db.First(&user, id)

// SQL 注入
db.First(&user, userInputID)
// SELECT * FROM users WHERE 1=1;drop table users;

SQL 注入方法

为了支持某些功能,一些输入不会被转义,调用方法时要小心用户输入的参数。

db.Select("name; drop table users;").First(&user)
db.Distinct("name; drop table users;").First(&user)

db.Model(&user).Pluck("name; drop table users;", &names)

db.Group("name; drop table users;").First(&user)

db.Group("name").Having("1 = 1;drop table users;").First(&user)

db.Raw("select name from users; drop table users;").First(&user)

db.Exec("select name from users; drop table users;")

db.Order("name; drop table users;").First(&user)

避免 SQL 注入的一般原则是,不信任用户提交的数据。您可以进行白名单验证来测试用户的输入是否为已知安全的、已批准、已定义的输入,并且在使用用户的输入时,仅将它们作为参数。

GORM 配置

GORM 提供的配置可以在初始化时使用

type Config struct {
  SkipDefaultTransaction   bool
  NamingStrategy           schema.Namer
  Logger                   logger.Interface
  NowFunc                  func() time.Time
  DryRun                   bool
  PrepareStmt              bool
  DisableNestedTransaction bool
  AllowGlobalUpdate        bool
  DisableAutomaticPing     bool
  DisableForeignKeyConstraintWhenMigrating bool
}

跳过默认事务

为了确保数据一致性,GORM 会在事务里执行写入操作(创建、更新、删除)。如果没有这方面的要求,您可以在初始化时禁用它。

db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{
  SkipDefaultTransaction: true,
})

命名策略

GORM 允许用户通过覆盖默认的NamingStrategy来更改命名约定,这需要实现接口 Namer

type Namer interface {
    TableName(table string) string
    SchemaName(table string) string
    ColumnName(table, column string) string
    JoinTableName(table string) string
    RelationshipFKName(Relationship) string
    CheckerName(table, column string) string
    IndexName(table, column string) string
}

默认 NamingStrategy 也提供了几个选项,如:

db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{
  NamingStrategy: schema.NamingStrategy{
    TablePrefix: "t_",   // table name prefix, table for `User` would be `t_users`
    SingularTable: true, // use singular table name, table for `User` would be `user` with this option enabled
    NoLowerCase: true, // skip the snake_casing of names
    NameReplacer: strings.NewReplacer("CID", "Cid"), // use name replacer to change struct/field name before convert it to db name
  },
})

Logger

允许通过覆盖此选项更改 GORM 的默认 logger,参考 Logger 获取详情

NowFunc

更改创建时间使用的函数

db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{
  NowFunc: func() time.Time {
    return time.Now().Local()
  },
})

DryRun

生成 SQL 但不执行,可以用于准备或测试生成的 SQL,参考 会话 获取详情

db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{
  DryRun: false,
})

PrepareStmt

PreparedStmt 在执行任何 SQL 时都会创建一个 prepared statement 并将其缓存,以提高后续的效率,参考 会话 获取详情

db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{
  PrepareStmt: false,
})

禁用嵌套事务

在一个事务中使用 Transaction 方法,GORM 会使用 SavePoint(savedPointName)RollbackTo(savedPointName) 为你提供嵌套事务支持,你可以通过 DisableNestedTransaction 选项关闭它,查看 Session 获取详情

AllowGlobalUpdate

启用全局 update/delete,查看 Session 获取详情

DisableAutomaticPing

在完成初始化后,GORM 会自动 ping 数据库以检查数据库的可用性,若要禁用该特性,可将其设置为 true

db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{
  DisableAutomaticPing: true,
})

DisableForeignKeyConstraintWhenMigrating

AutoMigrateCreateTable 时,GORM 会自动创建外键约束,若要禁用该特性,可将其设置为 true,参考 迁移 获取详情。

db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{
  DisableForeignKeyConstraintWhenMigrating: true,
})

编写插件

Callbacks

GORM 自身也是基于 Callbacks 的,包括 CreateQueryUpdateDeleteRowRaw。此外,您也完全可以根据自己的意愿自定义 GORM

回调会注册到全局 *gorm.DB,而不是会话级别。如果您想要 *gorm.DB 具有不同的回调,您需要初始化另一个 *gorm.DB

注册 Callback

注册 callback 至 callbacks

func cropImage(db *gorm.DB) {
  if db.Statement.Schema != nil {
    // 裁剪图片字段并上传到CDN,dummy code
    for _, field := range db.Statement.Schema.Fields {
      switch db.Statement.ReflectValue.Kind() {
      case reflect.Slice, reflect.Array:
        for i := 0; i < db.Statement.ReflectValue.Len(); i++ {
          // 从字段中获取数值
          if fieldValue, isZero := field.ValueOf(db.Statement.ReflectValue.Index(i)); !isZero {
            if crop, ok := fieldValue.(CropInterface); ok {
              crop.Crop()
            }
          }
        }
      case reflect.Struct:
        // 从字段中获取数值
        if fieldValue, isZero := field.ValueOf(db.Statement.ReflectValue); !isZero {
          if crop, ok := fieldValue.(CropInterface); ok {
            crop.Crop()
          }
        }

        // 将值设置给字段
        err := field.Set(db.Statement.ReflectValue, "newValue")
      }
    }

    // 当前实体的所有字段
    db.Statement.Schema.Fields

    // 当前实体的所有主键字段
    db.Statement.Schema.PrimaryFields

    // 优先主键字段:带有数据库名称`id`或第一个定义的主键的字段。
    db.Statement.Schema.PrioritizedPrimaryField

    // 当前模型的所有关系
    db.Statement.Schema.Relationships

    // 使用字段名或数据库名查找字段
    field := db.Statement.Schema.LookUpField("Name")

    // processing
  }
}

db.Callback().Create().Register("crop_image", cropImage)
// 为Create注册一个回调

删除 Callback

从 callbacks 中删除回调

db.Callback().Create().Remove("gorm:create")
// 从 Create 的 callbacks 中删除 `gorm:create`

替换 Callback

用一个新的回调替换已有的同名回调

db.Callback().Create().Replace("gorm:create", newCreateFunction)
// 用新函数 `newCreateFunction` 替换 Create 流程目前的 `gorm:create`

注册带顺序的 Callback

注册带顺序的 Callback

// gorm:create 之前
db.Callback().Create().Before("gorm:create").Register("update_created_at", updateCreated)

// gorm:create 之后
db.Callback().Create().After("gorm:create").Register("update_created_at", updateCreated)

// gorm:query 之后
db.Callback().Query().After("gorm:query").Register("my_plugin:after_query", afterQuery)

// gorm:delete 之后
db.Callback().Delete().After("gorm:delete").Register("my_plugin:after_delete", afterDelete)

// gorm:update 之前
db.Callback().Update().Before("gorm:update").Register("my_plugin:before_update", beforeUpdate)

// 位于 gorm:before_create 之后 gorm:create 之前
db.Callback().Create().Before("gorm:create").After("gorm:before_create").Register("my_plugin:before_create", beforeCreate)

// 所有其它 callback 之前
db.Callback().Create().Before("*").Register("update_created_at", updateCreated)

// 所有其它 callback 之后
db.Callback().Create().After("*").Register("update_created_at", updateCreated)

预定义 Callback

GORM 已经定义了 一些 callback 来支持当前的 GORM 功能,在启动您的插件之前可以先看看这些 callback

插件

GORM 提供了 Use 方法来注册插件,插件需要实现 Plugin 接口

type Plugin interface {
  Name() string
  Initialize(*gorm.DB) error
}

当插件首次注册到 GORM 时将调用 Initialize 方法,且 GORM 会保存已注册的插件,你可以这样访问访问:

db.Config.Plugins[pluginName]

查看 Prometheus 的例子

end
  • 作者:AWhiteElephant(联系作者)
  • 发表时间:2022-06-14 19:51
  • 版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)
  • 转载声明:如果是转载栈主转载的文章,请附上原文链接
  • 评论