GROM gorm.DB 对象剖析

2023-12-02 10:08:09 浏览数 (2)

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 的初始化,主要分为如下几个部分:

  1. 根据选项 option 初始化配置。如果某些配置未被初始化,则被置为缺省的配置。
  2. 将私有属性 clone 置为 1 表示使用 gorm.DB 对象时需要克隆,全局 gorm.DB 对象可以安全地进行复用。
  3. initializeCallbacks 初始化回调。
  4. 通过各个数据库的 Dialector 的 Initialize 方法建立连接。
  5. 初始化 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。

  1. Chain Method 可以用来将特定筛选条件增加到 gorm.DB 状态中,常见的有 db.Where,db.Select 等。
  2. Finisher Method 可以立即执行回调,生成并执行 SQL 语句。比如 db.Create,db.First,db.Find 等。
  3. 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

0 人点赞