GROM 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.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

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

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

相关文章

社区工作者在哪里啊?真救命了

社区工作者&#xff0c;每天各种写材料啊啊啊&#xff01;&#xff01; 有时候真的写麻掉了啊&#xff0c;家人们&#xff0c;太难了&#xff0c;谁懂啊&#xff01;&#xff01;&#xff01;&#xff01;&#xff01; 这个好东西真的救我大命啊&#xff0c;只要输入关键词和…

linux基础五:linux 系统(进程状态2:)

linux 系统 一.进程状态&#xff1a;1.睡眠状态(sleep)&#xff1a;2.磁盘休眠状态(disk sleep)&#xff1a;3.停止状态(stoped --- T)&#xff1a;4.死亡状态&#xff1a;5.控制状态&#xff08;t&#xff09; 二.僵尸进程和孤儿进程&#xff1a;1.僵尸状态&#xff1a;2.孤儿…

Redis:缓存问题及解决——缓存穿透、缓存击穿、缓存雪崩

缓存穿透 问题描述 当系统中引入redis缓存后&#xff0c;一个请求进来后&#xff0c;会先从redis缓存中查询&#xff0c;缓存有就直接返回&#xff0c;缓存中没有就去db中查询&#xff0c;db中如果有就会将其丢到缓存中&#xff0c;但是有些key对应数据在db中并不存在&#x…

管理员配置Jupterhub

在Ubuntu上演示如何部署R语言环境&#xff0c;包括 posit的已经编译了一些R的安装 系统已经装了R&#xff0c;我额外编译的R如何让大家都能用到 如何配置RStudio jupyterhub的配置和使用 管理员如何配置jupyterhub 用户如何配置自己的jupyter环境 Ubuntu上的R部署 这一次…

后端防止重复提交相同数据处理方式(Redis)

使用AOP注解处理接口幂等性&#xff0c;默认禁止同一用户在上次提交未果后10秒内又重复提交 在原先的sameUrlData的注解上进行了copy新建优化&#xff0c;使用redis去setnx的参数视项目使用点而调整&#xff0c;不一定是每个项目都适合这种取参形式。 源码如下: package com…

Mendix UI页面布局以案说法

一、前言 试着回想最近一次与公司网站交互的情况&#xff0c;访问了多个页面&#xff0c;并且可能使用了某些功能。有可能基于这种互动&#xff0c;可以向某人介绍公司的一些主要功能。其中一些可能是更肤浅的东西&#xff0c;比如他们的标志是什么样子或他们的主要配色方案是…

笔记二十四、剖析Redux的工作流程

24.1 定义 用做于状态管理的第三方 js 库react框架中使用&#xff0c;也可应用于其他的框架 使用场景 组件间需要共享状态和改变另一个组件的状态在react项目中可以不使用就尽量不用&#xff0c;复杂场景下才使用 24.2 原理图 24.3 代码 安装 yarn add reduxjs/toolkit rea…

GPIO的使用--操作PE02 PE03 PE04实现开关控制灯泡亮灭

效果&#xff1a; 开关控制灯的亮灭 目录 1.找到引脚组别(DEFG) led灯硬件结构 开关硬件结构 2.时钟使能 3.GPIO时钟控制 4.控制实现思路 5. 完整代码 6.视频演示 1.找到引脚组别(DEFG) 开关的引脚组别--E&#xff1b;LED灯的引脚组别--F led灯硬件结构 开关硬件结构…

【linux】基本指令(上篇)

1.快速认识5~6个指令 pwd指令 ls指令 touch指令 cd指令 clear指令 touch指令 详细讲解 首先有一个问题就是当我们创建一个文件&#xff0c;但是没有往里面写内容&#xff0c;那么磁盘上会有该文件吗&#xff1f; 磁盘上会保存&#xff0c;因为创建好的文件&#xff0c;没有…

Configure ssh-config简化ssh , scp命令;Screen 后台运行命令

1. Configure .ssh/config简化ssh, scp命令 # Configure myServer Host myServerHostName <ServerIP>User ubuntuIdentityFile /home/ubuntu/.ssh/<myServerKey>.pemssh 命令简化为. ssh myServer ssh myServerscp 命令简化为 scp -rp test.txt myServer:~/Down…

IDEA安装python插件并配置

目录 一、Mac1. 安装插件2. 新建项目3. 下载第三方库4. 配置镜像源 一、Mac 1. 安装插件 在plugins中搜索python 2. 新建项目 使用本项目独享的虚拟环境&#xff0c;选择解释器 如果需要修改解释器可以进入project structure&#xff0c;在SDKs中点击&#xff0c;选择add py…

clip-path,css裁剪函数

https://www.cnblogs.com/dzyany/p/13985939.html clip-path - CSS&#xff1a;层叠样式表 | MDN 我们看下这个例子 polygon里有四个值分别代表这四个点相对于原图左上方的偏移量。 裁剪个五角星

解决vue3项目打包发布到服务器后访问页面显示空白问题

1.在 vite.config.ts 文件中 加入 base:./ 当你将 base 设置为 / 时&#xff0c;它表示你的应用程序将部署在服务器的根路径上&#xff0c;&#xff08;将 base 设置为 / 表示你的应用程序部署在服务器的根路径上&#xff0c;并且 Vite 会相应地处理资源和路由的路径…

【UE】制作一块布

效果 步骤 1. 新建一个空白模板工程&#xff0c;新建一个Basic关卡 2. 选项模式选择“建模” 3. 创建一个矩形 宽度、深度设为500&#xff0c;宽度细分和深度细分设置为100&#xff0c;然后点击接受 此时在浏览器中编辑器也帮我们创建了一个矩形的静态网格体&#xff0c;这里…

C/C++---------------LeetCode第27. 移除元素

移除元素 题目及要求双指针在main内使用 题目及要求 给你一个数组 nums 和一个值 val&#xff0c;你需要 原地 移除所有数值等于 val 的元素&#xff0c;并返回移除后数组的新长度。 不要使用额外的数组空间&#xff0c;你必须仅使用 O(1) 额外空间并 原地 修改输入数组。 元…

MySQL数据库SQLSTATE[22007]: Invalid datetime format 日期类型不能为空值的解决办法

如果你的数据库是mysql&#xff0c; 如果你创建表或插入数据时遇到的BUG–它长这样&#xff1a; Invalid datetime format: 1292 Incorrect datetime value: ‘’ for column ‘xxx’ at row 1 或 1067 - Invalid default value for ‘xx’ 那么我将赐予你 两套剑法: &#…

Peter算法小课堂—差分与前缀和

差分 Codeforces802 D2C C代码详解 差分_哔哩哔哩_bilibili 一维差分 差分与前缀和可以说成减法和加法的关系、除法和乘法的关系、积分和微分的关系&#xff08;听不懂吧&#xff09; 给定数组A&#xff0c;S为A的前缀和数组&#xff0c;则A为S的差分数组 差分数组构造 现…

MySQL远程连接数据库

【0】数据库的安装及配置 Windows安装MySQL - 知乎 (zhihu.com) 【1】mysql远程连接数据库 1. 首先打开打开cmd窗口&#xff1a;winr ---------- cmd 2.输入用户名和密码进入数据库&#xff1a;mysql -u root -p 3.打开use mysql表&#xff1a;use mysql 4.执行远程连接(授…

TCP/IP_整理起因

先分享一个初级的问题&#xff1b;有个客户现场&#xff0c;终端设备使用客户网络更新很慢&#xff0c;使用手机热点更新速度符合预期&#xff1b;网络部署情况如下&#xff1a; 前期花费了很大的精力进行问题排查对比&#xff0c;怀疑是客户网络问题&#xff08;其他的客户现…

微信开发者工具真机调试连接状态在正常和未连接之间反复横跳

开启局域网模式能解决这个问题&#xff0c;目前只找到这一个方法