GORM gorm.DB 对象剖析

文章目录

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

  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 结构体。

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。

  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 对象,其中包含了前一个对象的设置和新的设置。

// 链式调用构建查询
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 框架原理&源码解析 - 小徐先生

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/198760.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

基于c 实现 FIFO

功能&#xff1a; 1、读和写长度不限制 2、数据操作 和 指针操作分开&#xff08;如先操作数据&#xff0c;再操作指针&#xff09; 适用场景&#xff1a; 单向通信模式&#xff0c;一方写、一方读&#xff0c;可用于任务间单向通信&#xff08;无需锁&#xff09; 如&…

7-HDFS的文件管理

单选题 题目1&#xff1a;下列哪个属性是hdfs-site.xml中的配置&#xff1f; 选项: A fs.defaultFS B dfs.replication C mapreduce.framework.name D yarn.resourcemanager.address 答案&#xff1a;B ------------------------------ 题目2&#xff1a;HDFS默认备份数量&…

fastboot常用命令

fastboot常用命令 显示fastboot设备&#xff1a;fastboot devices 获取手机相关信息&#xff1a;fastboot getvar all 重启手机&#xff1a;fastboot reboot 重启到bootloader&#xff1a;fastboot reboot-bootloader 擦除分区&#xff1a;fastboot erase (分区名) 例&…

代码随想录算法训练营第四十三天 _ 动态规划_1049.最后一块石头的重量II、494.目标和、474.一和零。

学习目标&#xff1a; 动态规划五部曲&#xff1a; ① 确定dp[i]的含义 ② 求递推公式 ③ dp数组如何初始化 ④ 确定遍历顺序 ⑤ 打印递归数组 ---- 调试 引用自代码随想录&#xff01; 60天训练营打卡计划&#xff01; 学习内容&#xff1a; 1049.最后一块石头的重量II 该题…

360公司-2019校招笔试-Windows开发工程师客观题合集解析

360公司-2019校招笔试-Windows开发工程师客观题合集 API无法实现进程间数据的相互传递是PostMessage2.以下代码执行后,it的数据为(异常) std::list<int> temp; std::list<int>::iterator it = temp.begin(); it = --it; 3.API在失败时的返回值跟其他不一样是 …

微信小程序自定义tabBar简易实现

文章目录 1.app.json设置custom为true开启自定义2.根目录创建自定义的tab文件3.app.js全局封装一个设置tabbar选中的方法4.在onshow中使用选中方法最终效果预览 1.app.json设置custom为true开启自定义 2.根目录创建自定义的tab文件 index.wxml <view class"tab-bar&quo…

自动升降压稳压电源模块输入3v~24V输出3.3/4.2/5/9/12V芯片

自动升降压稳压电源模块是一种高效、高稳定性的电源解决方案&#xff0c;广泛应用于各种需要稳定电压输出的场合。该模块采用宽电压低功耗方案&#xff0c;能够自动升降压&#xff0c;适应不同的输入电压范围&#xff0c;同时具有关断功能&#xff0c;确保设备的安全运行。 该电…

Vue 报错error:0308010C:digital envelope routines::unsupported

因为 node.js V17版本中最近发布的OpenSSL3.0, 而OpenSSL3.0对允许算法和密钥大小增加了严格的限制 方法一 windows终端输入 set NODE_OPTIONS--openssl-legacy-provider 方法二 降低node版本&#xff0c;比如16. 方法三 package.json增加如下配置 "scripts":…

想要更高效的文件传输?这些aspera替代方案可以助你一臂之力

随着数字化时代的不断推进&#xff0c;数据传输已成为各行各业、各类企业所必需的核心能力。而在文件传输方面&#xff0c;传统的方式往往面临着诸多问题&#xff0c;例如文件大小限制、传输速度过慢、不稳定、不安全等问题。为此&#xff0c;许多企业开始寻找更可靠、更高效的…

Java大数据开发入门教程:使用Hadoop处理海量数据

引言&#xff1a; 随着互联网的发展和智能设备的普及&#xff0c;数据量的爆炸式增长已成为现实。如何高效地处理和分析这些海量数据成为了当今技术领域的一个重要课题。在大数据领域&#xff0c;Hadoop作为一个开源的分布式计算框架&#xff0c;被广泛应用于海量数据的存储和处…

网工学习10-IP地址

一、IP地址概念 IP地址是一个32位的二进制数&#xff0c;它由网络ID和主机ID两部份组成&#xff0c;用来在网络中唯一的标识的一台计算机。网络ID用来标识计算机所处的网段&#xff1b;主机ID用来标识计算机在网段中的位置。IP地址通常用4组3位十进制数表示&#xff0c;中间用…

XHR 和 Fetch 的区别

网站开发普遍采用前后端分离的模式&#xff0c;数据交互成为了不可或缺的关键环节。在这个过程中&#xff0c;XHR 和 Fetch API 是两种最常见的方法&#xff0c;用于从 Web 服务器获取数据。XHR 是一种传统的数据请求方式&#xff0c;而 Fetch API 则代表了现代 Web 开发的新兴…

scipy笔记:scipy.interpolate.interp1d

1 主要使用方法 class scipy.interpolate.interp1d(x, y, kindlinear, axis-1, copyTrue, bounds_errorNone, fill_valuenan, assume_sortedFalse) 2 主要函数 x一维实数值数组&#xff0c;代表插值的自变量y N维实数值数组&#xff0c;其中沿着插值轴的 y 长度必须等于 x 的…

Linux:使用pv实现执行进度监控

pv全称&#xff1a;Pipe Viewer&#xff0c;通过管道显示数据处理进度的信息 安装 yum install pv -y示例 复制文件 # 显示进度 pv data.sql > ./data-new.sql330MiB 0:00:00 [1.32GiB/s] [>] 100%限制mysql数据导出速率 mysqldump | pv -L10m > data.sql# -L, -…

gitlab注册无中国区电话验证问题

众所周知gitlab对中国区不友好&#xff0c;无法直接注册&#xff0c;页面无法选择86的手机号进行验证码发送。 Google上众多的方案是修改dom&#xff0c;而且时间大约是21年以前。 修改dom&#xff0c;对于现在的VUE、React框架来说是没有用的&#xff0c;所以不用尝试。 直接看…

Docker 安装 Nacos

Docker 安装 Nacos tags: docker Nacos 文章目录 Docker 安装 Nacostags: docker Nacos 下载镜像docker 运行命令说明 Nacos 端口说明 下载镜像 下载镜像 # 搜索镜像 docker search nacos # 下载镜像 dockers pull nacos/nacos-server创建挂载文件夹 # 存放日志 mkdir naco…

Linux结束程序运行的命令

kill 通过进程 ID&#xff08;PID&#xff09;结束一个程序的运行。例如&#xff0c;要结束进程 ID 为 1234 的进程&#xff1a; kill 1234 pkill 通过进程名称结束一个程序的运行。例如&#xff0c;要结束名称为example_process的进程&#xff1a; pkill example_process …

postman参数为D:\\audio\\test.mp3请求报错

报错信息 报错 java.lang.IllegalArgumentException: Invalid character found in the request target [/v1/audio/transcriptions?audioPathD:\\audio\\test.mp3 ]. The valid characters are defined in RFC 7230 and RFC 3986 解决方式 yml文件上放行指定字符 relaxed-pa…

Mac电脑每次修改完java的版本后,没有成功

问题&#xff0c;本地有多个java版本8,11,15,17但是每次执行代码后&#xff0c;版本没有变化。在环境变量文件.bash_profile中设置无效 export JAVA_HOME/Library/Java/JavaVirtualMachines/<Java版本目录>/Contents/Home 这个问题通常是由于系统默认使用的Shell不同导…

安装获取mongodb

目录 本地安装 获取云上资源 获取Atlas免费数据库 本地连接数据库 在Atlas中连接数据库 本文适合初学者或mongodb感兴趣的同学来准备学习测试环境&#xff0c;或本地临时开发环境。mongodb是一个对用户非常友好的数据库。这种友好&#xff0c;不仅仅体现在灵活的数据结构和…