Go第三方框架--gorm框架(一)

前言

orm模型简介

orm模型全称是Object-Relational Mapping,即对象关系映射。其实就是在原生sql基础之上进行更高程度的封装。方便程序员采用面向对象的方式来操作数据库,将表映射成对象。

这种映射带来几个好处:

  1. 代码简洁:不用手动编写sql
  2. 安全性提高了:orm框架会自动处理参数化查询,可以有效减少sql注入的风险
  3. 可读性提高了:orm更接近自然语言。
  4. 可移植性提高,切换不同数据库时,不用重新编写sql,只需要更改连接字符串。
gorm模型简介

gorm顾名思义是采用go语言实现的orm框架,由jinzhu开发后开源,又融合了很多大神的思路。gorm主要是在go原生数据库包database/sql 的基础上引入orm思想来编写。所以其底层仍会调用database/sql的相关增删改查操作等dml操作和表结构修改等ddl操作。

以原生database/sql 查询为例:

要实现如下sql(gorm可以操作多种据库,我们本篇只以mysql为例来介绍)

sql 复制代码
SELECT * FROM userinfos WHERE name = "lisan" ORDER BY id ASC LIMIT 1;

则 database/sql 代码如下

go 复制代码
	dbsql, err := sql.Open("mysql", "root:root@tcp(127.0.0.1:3306)/world?charset=utf8mb4&parseTime=True&loc=Local")
	if err != nil {
		log.Fatal(err)
	}
	defer dbsql.Close()
	query := "SELECT * FROM userinfos WHERE name = ? ORDER BY id ASC LIMIT 1"

	// 执行查询
	_, err = dbsql.Query(query, "lisan")
	if err != nil {
		log.Fatal(err)
	}

debug可以看到其调用的是database/sql的QueryContext(...)函数。

如果用gorm来实现相同功能sql,则代码如下:

go 复制代码
	//gorm
	dsn := "root:root@tcp(127.0.0.1:3306)/world?charset=utf8mb4&parseTime=True&loc=Local"
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})  // db是初始化的db cloen=1 或 2 时,会将初始db复制一份,避免操作污染。
	if err != nil {
		panic(err)
	}
	var userinfo Userinfo
	// 查询单条db.Table(...) 中由于 db.clone=1 所以复制一份 新的db.clone=0 所以后续的链式调用 (各种. . .)的状态都会累加到 db.Table()生成的新db上,
	// 如此保证了 原始db的纯净 和 链式调用(拼装一条完整sql)的累加。(后续会有详细讲解)
	if err = db.Table("userinfos").Where("name = ?", "lisan").First(&userinfo).Error; err != nil {
 		return
	}

debug可以看到 其在First(...)函数内部也会调用 database/sql的QueryContext(...)函数

其调用链是

go 复制代码
First(...)---->Execute(tx)----->f(db)----> Query(db *gorm.DB)---->db.Statement.ConnPool.QueryContext(....)---->QueryContext(....)

我们介绍gorm也是从具体的例子开始,到调用具体的database/sql函数为止。其实gorm可以看做将原生database/sql语句可配置化,自动对应表结构和sql语句进行封装。

至于database/sql调用的具体细节,后续会单独编写文章。好了明确了边界,现在我们来看下orm的几个重要结构体。

几种结构体

DB

db是gorm的核心结构体,所有操作都由它承载。

go 复制代码
type DB struct {
	*Config     // 存放一些初始化的配制信息,包括连接池和执行dml的回调函数等
	Error        error // 执行操作的错误信息
	RowsAffected int64
	Statement    *Statement //  执行增删改查的 状态信息 ,是sql语句累加的地方。
	clone        int        // db克隆次数 用来克隆db实例 避免多个查询时 互相影响 保证每个操作都会复制一份初始化的db 
							// clone值 0:表示不复制实例 每个操作都会累计, 一般在相同sql语句中使用,例如 几个 where().where()链式调用需要进行sql状态累计。
							//   1:不同sql语句的增删改查等操作用,会复制一份新db,避免相互之间影响。一般只有连接池,配置等初始参数会复用,操作相关参数会初始化。相同语句链式调用会操作同一份Statement,不同的sql语句执行不同的Statement
							//   2: 开启事务时使用,配置直接复用,Statement会复制一份。 todo
}

Config结构体如下

Config

Config有初始化db需要的一些配置和初始化后的一些属性,比如连接池和回调函数等。

go 复制代码
type Config struct {
	// GORM perform single create, update, delete operations in transactions by default to ensure database data  
	// ... 省略一些本篇不用的属性
	// ClauseBuilders clause builder
	ClauseBuilders map[string]clause.ClauseBuilder
	// ConnPool db conn pool  // 连接池 对应database/sql 中的sql.DB 存放连接状态等信息 防止频繁创建连接
	ConnPool ConnPool
	// Dialector database dialector
	Dialector //  mysql/sqlite等数据的操作接口
	// Plugins registered plugins
	Plugins map[string]Plugin

	callbacks  *callbacks // 增删改查等 gorm高度封装回调database/sql函数在这里注册
	cacheStore *sync.Map
}

其中 Dialector主要实现各种数据库的连接等操作,其结构如下:

go 复制代码
type Dialector interface {
	Name() string
	Initialize(*DB) error      // 数据库连接池等的初始化 databae/sql.db的初始化 和 注册回调的原生database/sql dml 封装函数等
	Migrator(db *DB) Migrator  // 装载操作 也就是承接 ddl的一些功能
	DataTypeOf(*schema.Field) string
	DefaultValueOf(*schema.Field) clause.Expression
	BindVarTo(writer clause.Writer, stmt *Statement, v interface{})
	QuoteTo(clause.Writer, string)
	Explain(sql string, vars ...interface{}) string
}

其中 ConnPool 是Config中对接各数据库操作的地方,其函数内部直接调用了database/sql的相关函数,结构体如下:

go 复制代码
type ConnPool interface {
	PrepareContext(ctx context.Context, query string) (*sql.Stmt, error) 
	ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) // 执行
	QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error)  // 查询
	QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row 
}

目前 gorm集合了 mysql、sqlite等数据库的实现。

Config 中 callbacks属性 其结构体如下:

go 复制代码
type callbacks struct {
	processors map[string]*processor
}

type processor struct {
	db        *DB
	Clauses   []string
	fns       []func(*DB) // 回调函数 query 等
	callbacks []*callback
}
Statement

Statement是状态,也就是拼接完整sql和执行sql时需要的信息。

go 复制代码
type Statement struct {
	*DB
	TableExpr            *clause.Expr
	Table                string         // 操作的表名
	Model                interface{}   //  结构体,跟表对应 用来承接结果
	Unscoped             bool
	Dest                 interface{}   //  结构体,跟表对应
	ReflectValue         reflect.Value
	Clauses              map[string]clause.Clause // gorm执行语句 存储map 例如 Where("id=?",1) 则key:Where,value :包含 id和1,用来拼接最终sql; 累计各个dml的操作
	BuildClauses         []string // 某dml操作可能需要的操作关键字, SELECT ,FROM,FOR等在sql中出现的先后顺序排列。 先出现的先用来组合SQL,这样保证sql语句的合法性
	Distinct             bool
	Selects              []string // selected columns
	Omits                []string // omit columns
	Joins                []join
	Preloads             map[string][]interface{}
	Settings             sync.Map
	ConnPool             ConnPool      // 连接池
	Schema               *schema.Schema //  要执行操作的表对象的一些信息;比如:这张表属性列表、主键信息、表名;结构体和表名对应表。
	Context              context.Context
	RaiseErrorOnNotFound bool
	SkipHooks            bool
	SQL                  strings.Builder // 拼接后的最终 sql语句 入参用占位符代替
	Vars                 []interface{}   // SQL 属性的入参
	CurDestIndex         int
	attrs                []interface{}
	assigns              []interface{}
	scopes               []func(*DB) *DB
}

现在只是大概梳理下其结构体,有疑惑很正常,接下来我们开始进入内部了解下其原理。共分为四大部分:

  1. 初始化:介绍gorm.db初始化的一些操作,包括初始化db.ConnPool(也就是database/sql db的初始化),注册回调的原生database/sql dml 封装函数等。
  2. 自动装载:自动装载主要介绍表的自动创建,属性的增加等ddl操作。
  3. 增删改查:这块主要讲解 gorm 如何将封装程序的可装配函数(例如where链式调用)转化为复杂的sql语句,然后通过回调函数实现原生 database/sql 的dml操作。
  4. 事务: todo

初始化

初始化主要是初始化一些必要的参数,我们重点关注初始化对应数据库的连接池和注册dml操作的回调函数

初始化代码是示例的前几行,如下:

go 复制代码
dsn := "root:root@tcp(127.0.0.1:3306)/world?charset=utf8mb4&parseTime=True&loc=Local"
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})  // db是初始化的db cloen=1 时,会将初始db复制一份,避免操作污染。
	if err != nil {
		panic(err)
	}

其中mysql.Open(dsn)是按照mysql数据库的链接逻辑解析dsn,然后赋值给mysql的Dialector实现结构体,用来初始化mysql连接池用。

我们来看下gorm.Open(...)函数的实现:

go 复制代码
func Open(dialector Dialector, opts ...Option) (db *DB, err error) {
	// ...
	// clone=1时  会复制一份 db的核心属性 包括 statement、config等
	db = &DB{Config: config, clone: 1}

	// 回调函数初始化 
	db.callbacks = initializeCallbacks(db)

	if config.ClauseBuilders == nil {
		config.ClauseBuilders = map[string]clause.ClauseBuilder{}
	}

	// 这块 调用database/sql 根据不同的Dialector初始化不同数据库的ConnPool(就是database/db 的sql.db参数)等参数;并将dml回调函数填入callbacks
	if config.Dialector != nil {
		err = config.Dialector.Initialize(db)

		if err != nil {
			if db, _ := db.DB(); db != nil {
				_ = db.Close()
			}
		}
	}
 	// ...
	// 初始化Statement 
	db.Statement = &Statement{
		DB:       db,
		ConnPool: db.ConnPool,
		Context:  context.Background(),
		Clauses:  map[string]clause.Clause{},
	}

 // ...
}

其中核心逻辑在config.Dialector.Initialize(db)中,我们来看下:

go 复制代码
func (dialector Dialector) Initialize(db *gorm.DB) (err error) {
	// ...
	if dialector.Conn != nil {
		db.ConnPool = dialector.Conn
	} else {

		// 这边对接 database/sql 开始初始化连接池操作
		db.ConnPool, err = sql.Open(dialector.DriverName, dialector.DSN)
		if err != nil {
			return err
		}
	}

    // ...
	// register callbacks
	// 加载dml操作的关键字,用来定位sql关键字的先后顺序,以便生成合法的sql。
	callbackConfig := &callbacks.Config{
		CreateClauses: CreateClauses,
		QueryClauses:  QueryClauses,
		UpdateClauses: UpdateClauses,
		DeleteClauses: DeleteClauses,
	}
	
	// ...
	// dml的callbacks函数在这里执行
	callbacks.RegisterDefaultCallbacks(db, callbackConfig)

	for k, v := range dialector.ClauseBuilders() {
		db.ClauseBuilders[k] = v
	}
	return
}

我们看到这里主要完成了 mysql连接池的和dml回调函数的初始化。

其中 callbacks.RegisterDefaultCallbacks(...)函数如下:

go 复制代码
func RegisterDefaultCallbacks(db *gorm.DB, config *Config) {
 	// ...
	// 注册执行的回调函数会在 First()中 最后一行的Execute()执行函数的 fns列表调用 
	// Create() 返回create对应的 *processor指针 对此指针的修改会反映在 db.config.callbacks属性上
	createCallback := db.Callback().Create()
	// *processor.callbacks ([]*callback) 在这边初始化
	createCallback.Match(enableTransaction).Register("gorm:begin_transaction", BeginTransaction)
	createCallback.Register("gorm:before_create", BeforeCreate)
	createCallback.Register("gorm:save_before_associations", SaveBeforeAssociations(true))
	createCallback.Register("gorm:create", Create(config))
	createCallback.Register("gorm:save_after_associations", SaveAfterAssociations(true))
	createCallback.Register("gorm:after_create", AfterCreate)
	createCallback.Match(enableTransaction).Register("gorm:commit_or_rollback_transaction", CommitOrRollbackTransaction)
	createCallback.Clauses = config.CreateClauses
	
	// 查询回调函数注册 也就是后续DML章节讲解的 查询函数的注册
	queryCallback := db.Callback().Query()
	queryCallback.Register("gorm:query", Query)
 	// ...
 	
 	// 删除
	deleteCallback := db.Callback().Delete()
	deleteCallback.Match(enableTransaction).Register("gorm:begin_transaction", BeginTransaction)
 	// ...
	
	// 更新
	updateCallback := db.Callback().Update()
	updateCallback.Match(enableTransaction).Register("gorm:begin_transaction", BeginTransaction)
	updateCallback.Register("gorm:setup_reflect_value", SetupUpdateReflectValue)
 	// ...

	// gorm可以对增删改查进行 封装操作,使得操作更简单。也支持原生sql的查询
	rowCallback := db.Callback().Row() 
	rowCallback.Register("gorm:row", RowQuery)
	rowCallback.Clauses = config.QueryClauses
	// ...
}

我们来看下涉及的结构体之间的关系图:

到这里初始化我们需要关注的两个领域已将讲解完毕。

自动装载(ddl操作)

自动装载主要是用来实现ddl相关的此操作,比如表的创建,属性的增加,属性参数的修改,添加约束条件,添加索引等。其会动态感知结构体的字段变化,从而将其映射到表结构上。我们来看下其源码。先看下下面的例子:

go 复制代码
type Userinfo struct {
	Id     uint
	Name   string
	Gender string
	Hobby  string
	Addr   string
	Age    uint8
}

func TestGorm(t *testing.T) {
	//gorm
	dsn := "root:root@tcp(127.0.0.1:3306)/world?charset=utf8mb4&parseTime=True&loc=Local"
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	if err != nil {
		panic(err)
	}

	//自动迁移 ddl相关的操作 调用 excute函数执行操作,调用"row"或者"raw"对应的 processor 执行查询 执行原生操作
	err = db.AutoMigrate(&Userinfo{})
	if err != nil {
		return
	}
}

执行如上语句后会创建 表名为 userinfos的表

其代码比较简单,但内部有复杂的逻辑,我们来简单梳理下。

自动装载源码如下:

go 复制代码
// AutoMigrate run auto migration for given models
func (db *DB) AutoMigrate(dst ...interface{}) error {
	return db.Migrator().AutoMigrate(dst...)
}

其中 **Migrator()**方法返回值是Migrator 接口,其承载着自动装载需要的所有方法。todo

go 复制代码
func (db *DB) Migrator() Migrator {
	tx := db.getInstance()  

	// apply scopes to migrator
	for len(tx.Statement.scopes) > 0 {
		tx = tx.executeScopes()
	}
	 // 调用 Dialector 的 Migrator 方法,传入一个 Session 实例。Session 包含了当前的事务信息,用于执行迁移操作 
	return tx.Dialector.Migrator(tx.Session(&Session{}))
}

Migrator 接口如下:

go 复制代码
// ddl相关操作 可以增删 表 表的属性 视图 限制条件 索引 等;可以查看数据库
type Migrator interface {
	// AutoMigrate
	AutoMigrate(dst ...interface{}) error 

	// Database 数据库相关操作
	CurrentDatabase() string
	FullDataTypeOf(*schema.Field) clause.Expr
	GetTypeAliases(databaseTypeName string) []string

	// Tables 表相关操作
	CreateTable(dst ...interface{}) error
	DropTable(dst ...interface{}) error
	// ...

	// Columns 列相关操作
	AddColumn(dst interface{}, field string) error
	DropColumn(dst interface{}, field string) error
 	// ...

	// Views 视图相关操作
	CreateView(name string, option ViewOption) error
	DropView(name string) error

	// Constraints 限制条件相关操作
	CreateConstraint(dst interface{}, name string) error
	DropConstraint(dst interface{}, name string) error
	HasConstraint(dst interface{}, name string) bool

	// Indexes 索引相关操作
	CreateIndex(dst interface{}, name string) error
	DropIndex(dst interface{}, name string) error
 	// ...
}

AutoMigrate(value ...interface{})承载着,自动装载的核心逻辑,包括对表、属性、索引等的操作;源码如下:

go 复制代码
// AutoMigrate auto migrate values
func (m Migrator) AutoMigrate(values ...interface{}) error {
	for _, value := range m.ReorderModels(values, true) {
		queryTx, execTx := m.GetQueryAndExecTx()

		// 没有找到对应表 需要创建表
		if !queryTx.Migrator().HasTable(value) {
			// 创建表
			if err := execTx.Migrator().CreateTable(value); err != nil {
				return err
			}
		} else {
			// 将结构体名映射成表名 然后执行回调函数
			if err := m.RunWithValue(value, func(stmt *gorm.Statement) error {
				columnTypes, err := queryTx.Migrator().ColumnTypes(value)
				if err != nil {
					return err
				}
				var (
					parseIndexes          = stmt.Schema.ParseIndexes()
					parseCheckConstraints = stmt.Schema.ParseCheckConstraints()
				)

				// DBNames 结构体 属性 按照 规则转换成 表属性 格式
				for _, dbName := range stmt.Schema.DBNames {
					var foundColumn gorm.ColumnType
					// columnTypes: 从数据库获得 表的 属性名信息 columnTypes
					for _, columnType := range columnTypes {
						if columnType.Name() == dbName {
							foundColumn = columnType
							break
						}
					}

					// 表中没有对应列
					if foundColumn == nil {
						// not found, add column 创建列
						if err = execTx.Migrator().AddColumn(value, dbName); err != nil {
							return err
						}
					} else {
						// found, smartly migrate 找到了结构体属性对应列名  对属性的参数(类型,注释等)进行 更新
						field := stmt.Schema.FieldsByDBName[dbName]
						if err = execTx.Migrator().MigrateColumn(value, field, foundColumn); err != nil {
							return err
						}
					}
				}

				// 对表约束条件进行更新
				if !m.DB.DisableForeignKeyConstraintWhenMigrating && !m.DB.IgnoreRelationshipsWhenMigrating {
					// ...
				}

				// 对表索引进行更新
				for _, idx := range parseIndexes {
					if !queryTx.Migrator().HasIndex(value, idx.Name) {
						if err := execTx.Migrator().CreateIndex(value, idx.Name); err != nil {
							return err
						}
					}
				}

				return nil
			}); err != nil {
				return err
			}
		}
	}

	return nil
}

AutoMigrate 函数中需要的核心调用函数 都来自 Migrator 结构体 ,我们选择HasTable()函数来简单梳理下。

**HasTable(...)**用来判断是否存在特定表。其源码如下:

go 复制代码
func (m Migrator) HasTable(value interface{}) bool {
	var count int64

	m.RunWithValue(value, func(stmt *gorm.Statement) error {
		currentDatabase := m.DB.Migrator().CurrentDatabase()
		// 原生sql执行(所谓执行原生sql,就是直接写原生sql,来调用database/sql方法,不使用gorm来组装sql),执行逻辑在Row()中,这边会调用已经注册的回调函数
		return m.DB.Raw("SELECT count(*) FROM information_schema.tables WHERE table_schema = ? AND table_name = ? AND table_type = ?", currentDatabase, stmt.Table, "BASE TABLE").Row().Scan(&count)
	})

	return count > 0
}

自动装载中所有对sql的调用的都是原生sql,因为查表、添加属性这种ddl sql语句比较固定,所以没必要采用组装的形式;而增删改查等 复杂的dml可以采用gorm来组装(比如:where(...).where(...)这种gorm最终会组合成sql语句。对database/sql的调用可以看做是接口调用,自动装载中的dml操作就简单的处理下得到了入参 ),再啰嗦一句,增删改等复杂的dml操作为用户提供了链式组合的方式来编写复杂的sql,它将组合的sql语句链,经过一些列的操作转换成调用原生sql的入参(原生sql和sql语句入参 )。

自动装载函数AutoMigrate中还有好多值得深挖的点,由于篇幅原因不做介绍,感兴趣的大神可以深挖下。

相关推荐
学传打活2 天前
【边打字.边学昆仑正义文化】_进阶篇5 宇宙星系的运转(续1)
微信公众平台·1024程序员节·汉字·昆仑正义文化
fangzhanpeng16810 天前
快速掌握C#语言基础知识点(1.基础数据类型)
开发语言·c#·1024程序员节
无己心12 天前
面向多平台 AI 搜索引擎的适配层架构设计
前端·人工智能·搜索引擎·ai·1024程序员节
u01030552718 天前
AI驱动的安卓UI自动生成功能实现
人工智能·1024程序员节
u01030552719 天前
长株潭智能体赋能腾讯元器链接共享
1024程序员节
云贝贝贝20 天前
OceanBase认证体系介绍与备考指南
运维·服务器·oceanbase·1024程序员节
u01030552720 天前
Java图像处理实战指南
人工智能·1024程序员节
u01030552721 天前
Java AWT鼠标事件全解析
人工智能·1024程序员节
u01030552723 天前
Java实用类应用精讲
人工智能·1024程序员节
u01030552723 天前
Java I/O核心操作实战
人工智能·1024程序员节