1.GORM 简介
GORM 是一个流行的 Golang ORM 库。
类似于 Java 生态里大家听到过的 Mybatis、Hibernate、SpringData 等。
GORM 由国人开发,中文文档齐全,对开发者友好,支持主流关系型数据库。
- MySQL
- SQL Server
- PostgreSQL
- SQLite
GORM 功能丰富齐全:
- 关联 (拥有一个,拥有多个,属于,多对多,多态,单表继承)
- 钩子(before/after create/save/update/delete/find)
- 支持 Preload、Joins 的预加载
- 事务,嵌套事务,保存点,回滚到保存点
- Context、预编译模式、DryRun 模式
- 批量插入,FindInBatches,使用 Map Find/Create,使用 SQL 表达式、Context Valuer 进行 CRUD
- SQL 构建器,Upsert,锁,Optimizer/Index/Comment Hint,命名参数,子查询
- 复合主键,索引,约束
- 自动迁移
- 自定义 Logger
- 灵活的可扩展插件 API:Database Resolver(多数据库,读写分离)、Prometheus…
- 每个特性都经过了测试的重重考验
- 开发者友好
GORM 官网地址点这里。
本文基于 GORM V2(版本号 v1.25.5)源码进行探究。
2.gorm.DB 简介
2.1 定义
gorm.DB 是 GORM 的核心类型,它代表了与数据库的连接和交互。所有与数据库的交互均需要通过 gorm.DB 对象来完成。
gorm.DB 的定义如下:
代码语言:javascript复制// DB GORM DB definition
type DB struct {
*Config
Error error
RowsAffected int64
Statement *Statement
clone int
}
- Config 是 GORM 的相关配置。
- Error 表示 SQL 的执行错误信息。
- RowsAffected 表示 SQL 影响的行数。
- Statement 表示 SQL 语句。
- clone 在初始化时会被置为 1,表示使用 gorm.DB 对象时需要克隆。后续所有 SQL 操作,都会基于全局 gorm.DB 对象克隆一个新的 gorm.DB 对象,进行链式操作。
2.2 初始化
初始化 gorm.DB 使用 gorm.Open 函数。
代码语言:javascript复制func Open(dialector Dialector, opts ...Option) (db *DB, err error)
其中 Dialector 是 GORM 抽象出来的数据库驱动接口,用于支持不同的数据库,每个数据库都有对应的实现。比如 MySQL 驱动是 gorm.io/driver/mysql。
以 MySQL 为例,指定 DSN(Data Source Name)初始化 MySQL 数据库实例。
代码语言:javascript复制import (
"fmt"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
// MySQLConn GORM MySQL 连接。
var MySQLConn *gorm.DB
// Init gorm mysql connnection based on the configuration.
func InitMySQLConn() error {
dsn := fmt.Sprintf("%v:%v@tcp(%v:%v)/%v?charset=utf8mb4&parseTime=True", Conf.Mysql.User, Conf.Mysql.Passwd, Conf.Mysql.IP, Conf.Mysql.Port, Conf.Mysql.Dbname)
var err error
MySQLConn, err = gorm.Open(mysql.Open(dsn), &gorm.Config{
Logger: logger.Default.LogMode(logger.Info),
})
return err
}
gorm.Open 函数的实现如下:
代码语言:javascript复制// Open initialize db session based on dialector
func Open(dialector Dialector, opts ...Option) (db *DB, err error) {
config := &Config{}
sort.Slice(opts, func(i, j int) bool {
_, isConfig := opts[i].(*Config)
_, isConfig2 := opts[j].(*Config)
return isConfig && !isConfig2
})
for _, opt := range opts {
if opt != nil {
if applyErr := opt.Apply(config); applyErr != nil {
return nil, applyErr
}
defer func(opt Option) {
if errr := opt.AfterInitialize(db); errr != nil {
err = errr
}
}(opt)
}
}
if d, ok := dialector.(interface{ Apply(*Config) error }); ok {
if err = d.Apply(config); err != nil {
return
}
}
if config.NamingStrategy == nil {
config.NamingStrategy = schema.NamingStrategy{IdentifierMaxLength: 64} // Default Identifier length is 64
}
if config.Logger == nil {
config.Logger = logger.Default
}
if config.NowFunc == nil {
config.NowFunc = func() time.Time { return time.Now().Local() }
}
if dialector != nil {
config.Dialector = dialector
}
if config.Plugins == nil {
config.Plugins = map[string]Plugin{}
}
if config.cacheStore == nil {
config.cacheStore = &sync.Map{}
}
db = &DB{Config: config, clone: 1}
db.callbacks = initializeCallbacks(db)
if config.ClauseBuilders == nil {
config.ClauseBuilders = map[string]clause.ClauseBuilder{}
}
if config.Dialector != nil {
err = config.Dialector.Initialize(db)
if err != nil {
if db, _ := db.DB(); db != nil {
_ = db.Close()
}
}
}
if config.PrepareStmt {
preparedStmt := NewPreparedStmtDB(db.ConnPool)
db.cacheStore.Store(preparedStmtDBKey, preparedStmt)
db.ConnPool = preparedStmt
}
db.Statement = &Statement{
DB: db,
ConnPool: db.ConnPool,
Context: context.Background(),
Clauses: map[string]clause.Clause{},
}
if err == nil && !config.DisableAutomaticPing {
if pinger, ok := db.ConnPool.(interface{ Ping() error }); ok {
err = pinger.Ping()
}
}
if err != nil {
config.Logger.Error(context.Background(), "failed to initialize database, got error %v", err)
}
return
}
gorm.Open 函数完成 gorm.DB 的初始化,主要分为如下几个部分:
- 根据选项 option 初始化配置。如果某些配置未被初始化,则被置为缺省的配置。
- 将私有属性 clone 置为 1 表示使用 gorm.DB 对象时需要克隆,全局 gorm.DB 对象可以安全地进行复用。
- initializeCallbacks 初始化回调。
- 通过各个数据库的 Dialector 的 Initialize 方法建立连接。
- 初始化 Statement。
2.3 查询方法
gorm.DB 提供了多种查询方法,如 Find、First、Where、Order 等,用于执行不同类型的数据库查询操作。这些方法负责构建 SQL 查询语句,并将查询结果映射到指定的 Go 结构体。
代码语言:javascript复制var user User
db.First(&user, 1) // 查询 ID 为 1 的用户
2.4 事务支持
gorm.DB 对象支持事务操作。你可以使用 Begin 方法启动一个事务,然后使用 Commit 提交事务或使用 Rollback 回滚事务。
代码语言:javascript复制tx := db.Begin()
// 在事务中执行操作
tx.Create(&User{Name: "John"})
tx.Commit() // 提交事务
2.5 模型关联
GORM 支持模型之间的关联,例如一对一、一对多、多对多等关系。你可以在 gorm.DB 对象上使用 Preload、Association 等方法来处理模型关联。
代码语言:javascript复制var user User
db.Preload("Orders").Find(&user) // 预加载用户的订单信息
2.6 钩子(Hooks)
gorm.DB 支持钩子,你可以在执行查询的不同阶段注册回调函数,以便在执行前或执行后执行一些操作。
代码语言:javascript复制db.Callback().Create().Before("gorm:before_create").Register("update_created_at", updateCreatedAt)
2.7 自定义数据类型
gorm.DB 允许你定义和使用自定义数据类型,以便更好地映射数据库中的数据。
代码语言:javascript复制type CustomType struct {
// 自定义类型的定义
}
var custom CustomType
db.Model(&User{}).Select("custom_field").Scan(&custom)
这些只是 gorm.DB 对象的一些基本特性,GORM 还提供了其他功能,如数据库迁移、复杂查询、日志记录等。详细了解 gorm.DB 的功能和用法,可以参考 GORM 的文档:GORM 文档。
3.为什么不同请求可以共用一个 gorm.DB 对象?
初始化 gorm.DB 后,不知道大家有没有一个疑问,所有的 SQL 请求均是通过一个 gorm.DB 对象完成的,gorm.DB 对象是怎么区分不同的 SQL 请求的呢?
比如下面是一个查询:
代码语言:javascript复制var goods []Good
db := Db.Model(&Good{})
db.Where("name LIKE ?", "%" name "%")
db.Where("price >= ?", price)
db.Find(&goods)
然后接着使用同一个全局 gorm.DB 对象再执行一个 SQL 查询。
代码语言:javascript复制var goods []Good
db := Db.Model(&Good{})
db.Where("name LIKE ?", "%" name "%")
db.Where("price < ?", price)
db.Find(&goods)
这两个查询是如何相互隔离、互不干扰的呢?
说到这里,那么就不得不提 GORM 的链式调用与方法了。
4.链式调用与方法
gorm.DB 的方法有三种:Chain Method、Finisher Method 和 New Session Method。
- Chain Method 可以用来将特定筛选条件增加到 gorm.DB 状态中,常见的有 db.Where,db.Select 等。
- Finisher Method 可以立即执行回调,生成并执行 SQL 语句。比如 db.Create,db.First,db.Find 等。
- New Session Method 用于新建会话,相当于创建了一个 gorm.DB 对象。
上面三种方法都会返回一个新的 gorm.DB 对象。
gorm.DB 支持链式调用,使得你可以通过一系列的方法调用来构建和修改查询。每个方法都会返回一个新的 gorm.DB 对象,其中包含了前一个对象的设置和新的设置。
代码语言:javascript复制// 链式调用构建查询
result := db.Where("name = ?", "John").Order("age DESC").Find(&users)
通过链式调用完成 SQL 的构建与执行,而不必一层套一层,类似 A(B(argb), arga) 这种调用。
我们可以看一下 db.Where 函数的源码。
代码语言:javascript复制func (db *DB) Where(query interface{}, args ...interface{}) (tx *DB) {
tx = db.getInstance()
if conds := tx.Statement.BuildCondition(query, args...); len(conds) > 0 {
tx.Statement.AddClause(clause.Where{Exprs: conds})
}
return
}
我们从这里能看到,Where 传入的所有参数,都不是直接作用于 db 身上的,而是通过 db.getinstance 获取一个新的 gorm.DB 实例,再对这个实例进行操作。
我们可以看一下私有方法 getInstance 的实现。
代码语言:javascript复制func (db *DB) getInstance() *DB {
if db.clone > 0 {
tx := &DB{Config: db.Config, Error: db.Error}
if db.clone == 1 {
// clone with new statement
tx.Statement = &Statement{
DB: tx,
ConnPool: db.Statement.ConnPool,
Context: db.Statement.Context,
Clauses: map[string]clause.Clause{},
Vars: make([]interface{}, 0, 8),
}
} else {
// with clone statement
tx.Statement = db.Statement.clone()
tx.Statement.DB = tx
}
return tx
}
return db
}
在这里看到了前文提到的 gorm.DB 对象私有属性 clone 的处理逻辑,该字段与 gorm.DB 对象的克隆有关系。
当 clone <= 0 时,db.getinstance 直接返回 db 本身;否则先生成一个与 db 共享一些属性的 tx 并返回。
当 clone == 1 时,tx 生成一个新的 statement,独立于原来 db 的 statement,这个新的 statement 的 clauses(相当于筛选条件,比如 where 中的语句就被这一变量处理)为空,并不含有之前的内容。
当 clone > 1 时,通过 db.Statement.clone() 函数将之前 db 的 Statement 中的 clause 全部复制到 tx 中,这相当于新的 DB 实例 tx 拥有之前所添加过的所有条件,并将 Statement 所属的 DB 调整为 tx。
前文说到,全局 gorm.DB 对象通过 gorm.Open 函数初始化时,clone 被置为 1,表示全局 gorm.DB 对象是可以复用的,但是复用时需要克隆。克隆的新的 gorm.DB 对象 clone 为 0,后续使用时将不会再被克隆。
Chain Method 的第一句都会调用 getInstance 克隆当前 gorm.DB 对象获取一个新的 gorm.DB 对象。执行不同 SQL 之所以能够相互隔离、互补干扰,因为使用调用 gorm.DB 方法时会,最终使用的是不同的 gorm.DB 对象。
5.小结
本文主要介绍了 GORM 核心结构 gorm.DB 的定义、作用和初始化。
执行不同的 SQL 可以复用同一个全局 gorm.DB 对象,其背后的原理是基于 gorm.DB 对象 clone 属性的值,如果为 1 则克隆。在 gorm.DB 对象的链式调用过程中,会基于全局 gorm.DB 对象克隆一个新的 gorm.DB 对象,使得每次执行不同的 SQL 相互隔离、互补干扰。
如果想快速上手 GORM 请参考我写的 GORM CRUD 10 分钟快速上手,全面了解莫过于 GORM 官方文档。
参考文献
gorm github
golang源码分析:gorm
gorm源码之db的克隆 - 稀土掘金
Gorm 的黑魔法- weirwei