文章目录
- 1.GORM 简介
- 2.gorm.DB 简介
- 2.1 定义
- 2.2 初始化
- 2.3 查询方法
- 2.4 事务支持
- 2.5 模型关联
- 2.6 钩子(Hooks)
- 2.7 自定义数据类型
- 3.为什么不同请求可以共用一个 gorm.DB 对象?
- 4.链式调用与方法
- 5.小结
- 参考文献
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 的定义如下:
// DB GORM DB definition
type DB struct {*ConfigError errorRowsAffected int64Statement *Statementclone int
}
- Config 是 GORM 的相关配置。
- Error 表示 SQL 的执行错误信息。
- RowsAffected 表示 SQL 影响的行数。
- Statement 表示 SQL 语句。
- clone 在初始化时会被置为 1,表示使用 gorm.DB 对象时需要克隆。后续所有 SQL 操作,都会基于全局 gorm.DB 对象克隆一个新的 gorm.DB 对象,进行链式操作。
2.2 初始化
初始化 gorm.DB 使用 gorm.Open 函数。
func Open(dialector Dialector, opts ...Option) (db *DB, err error)
其中 Dialector 是 GORM 抽象出来的数据库驱动接口,用于支持不同的数据库,每个数据库都有对应的实现。比如 MySQL 驱动是 gorm.io/driver/mysql。
以 MySQL 为例,指定 DSN(Data Source Name)初始化 MySQL 数据库实例。
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 errorMySQLConn, err = gorm.Open(mysql.Open(dsn), &gorm.Config{Logger: logger.Default.LogMode(logger.Info),})return err
}
gorm.Open 函数的实现如下:
// 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 结构体。
var user User
db.First(&user, 1) // 查询 ID 为 1 的用户
2.4 事务支持
gorm.DB 对象支持事务操作。你可以使用 Begin 方法启动一个事务,然后使用 Commit 提交事务或使用 Rollback 回滚事务。
tx := db.Begin()
// 在事务中执行操作
tx.Create(&User{Name: "John"})
tx.Commit() // 提交事务
2.5 模型关联
GORM 支持模型之间的关联,例如一对一、一对多、多对多等关系。你可以在 gorm.DB 对象上使用 Preload、Association 等方法来处理模型关联。
var user User
db.Preload("Orders").Find(&user) // 预加载用户的订单信息
2.6 钩子(Hooks)
gorm.DB 支持钩子,你可以在执行查询的不同阶段注册回调函数,以便在执行前或执行后执行一些操作。
db.Callback().Create().Before("gorm:before_create").Register("update_created_at", updateCreatedAt)
2.7 自定义数据类型
gorm.DB 允许你定义和使用自定义数据类型,以便更好地映射数据库中的数据。
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 请求的呢?
比如下面是一个查询:
var goods []Good
db := Db.Model(&Good{})
db.Where("name LIKE ?", "%"+name+"%")
db.Where("price >= ?", price)
db.Find(&goods)
然后接着使用同一个全局 gorm.DB 对象再执行一个 SQL 查询。
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 对象,其中包含了前一个对象的设置和新的设置。
// 链式调用构建查询
result := db.Where("name = ?", "John").Order("age DESC").Find(&users)
通过链式调用完成 SQL 的构建与执行,而不必一层套一层,类似 A(B(argb), arga) 这种调用。
我们可以看一下 db.Where 函数的源码。
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 的实现。
func (db *DB) getInstance() *DB {if db.clone > 0 {tx := &DB{Config: db.Config, Error: db.Error}if db.clone == 1 {// clone with new statementtx.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 statementtx.Statement = db.Statement.clone()tx.Statement.DB = tx}return tx}return db
}
在这里看到了前文提到的 gorm.DB 对象私有属性 clone 的处理逻辑,该字段与 gorm.DB 对象的克隆有关系。
- 当 clone <= 0 时,返回 db 本身。
- 当 clone == 1 时,返回一个新的 db(clone 变成 0),tx 的 Statement 连接池和上下文延用之前的 db,把条件和变量置为空。
- 当 clone > 1 时(目前 clone 只有 2),通过 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
gorm 框架原理&源码解析 - 小徐先生