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

增删改查(dml操作)

查询操作

gorm查询主要执行了三种操作:

  1. 通过链式函数调用累计查询条件(在map[string]clause.Clause中累计)
  2. 将查询条件转换成sql(赋值给 Statement.SQL和Statement.Vals)
  3. 执行对应回调函数
    我们以下面简单例子来进行说明:
func TestGorm(t *testing.T) {//gormdsn := "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)}var userinfo Userinfo// 其中 操作1 主要涉及  db.Table("userinfos").Where("name = ?", "lisan").Where("hobby","reading");和 .First(&userinfo)(First里主要累计 order by 主键和 limit 1条件)// 操作2,3   在.First(&userinfo) 函数完成。if err = db.Table("userinfos").Where("name = ?", "lisan").Where("hobby = ?","reading").First(&userinfo).Error; err != nil {return}}

我们来逐步进行下讲解(dml操作基本都是这个流程)

累计查询条件

我们先来看下整个累计的效果吧,我们在First函数的 第一行加上断点,可以得到如下的sql累计效果。
在这里插入图片描述
我们在语句中看到了三种关键字 Where,limit,order by,则Stement的 Clauses 的key是这三个value 是对应的语句和值。我们来看下是不是
在这里插入图片描述
可以看到 绿色框里是key,红色框里ddl操作语句相关的数组,其是value的核心属性。
通过上面的debug,基本验证了我们的猜测。
接下来我们来从代码的角度梳理下,怎么一步步形成上面的map (map[string]clause.Clause)
首先是 db. db.Table(“userinfos”),我们来看下 源码:

// 指定需要操作的表 这里会复制一份 db ,不影响其他的dml操作
func (db *DB) Table(name string, args ...interface{}) (tx *DB) {tx = db.getInstance() // 这边会复制一份 db,后续链式调用的sql变量都会累积到这个db.Statement上 ,新的db的clone没有赋值为0, 再调用 getInstance时 就都是返回自身if strings.Contains(name, " ") || strings.Contains(name, "`") || len(args) > 0 {tx.Statement.TableExpr = &clause.Expr{SQL: name, Vars: args}if results := tableRegexp.FindStringSubmatch(name); len(results) == 3 {if results[1] != "" {tx.Statement.Table = results[1]} else {tx.Statement.Table = results[2]}}} else if tables := strings.Split(name, "."); len(tables) == 2 {tx.Statement.TableExpr = &clause.Expr{SQL: tx.Statement.Quote(name)}tx.Statement.Table = tables[1]} else if name != "" {tx.Statement.TableExpr = &clause.Expr{SQL: tx.Statement.Quote(name)}tx.Statement.Table = name} else {tx.Statement.TableExpr = niltx.Statement.Table = ""}return
}

其中 db.getInstance() 函数控制db实例的获取方式,如下:


// 获取一个db实例,0:原始db ; 1: 复制一份,不同ddl,dml操作使用 ,会复制连接池 注册的回调函数等; 2:开启事务时使用
func (db *DB) getInstance() *DB {if db.clone > 0 {tx := &DB{Config: db.Config, Error: db.Error}// 等于1 则 Statement 需要重新生成一份,避免不同dml/ddl操作互相影响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),SkipHooks: db.Statement.SkipHooks,}// 开启事务 todo} else {// with clone statementtx.Statement = db.Statement.clone()tx.Statement.DB = tx}return tx}// 等于0 直接返回db自身 return db
}

db.Table( “userinfos”)初始化了一个新的 db(含新的Statement),后续链式操作行为都在这个新db上累加,做到不同语句不互相影响;指定了需要查询的表。
继续执行后续语句: db.Table(“userinfos”).Where(“name = ?”, “lisan”)
我们看下源码:

func (db *DB) Where(query interface{}, args ...interface{}) (tx *DB) {tx = db.getInstance() //  db.clone是0 返回自身 用来累加sql// 将sql条件 转换成拼装结构体数组的参数 赋值给 []clause.Expression if conds := tx.Statement.BuildCondition(query, args...); len(conds) > 0 {// 将where 条件 添加到 Key 是where的map中(conds添加到数组中,数组用来累加后续 where查询条件) (这边实现了where条件的积累,不同的dml操作AddClause实现不一样,但都是向对应关键字key添加value)tx.Statement.AddClause(clause.Where{Exprs: conds})}return
}

db.Table(“userinfos”).Where(“name = ?”, “lisan”).Where(“hobby = ?”, “reading”)时,其结构如下图1所示:
在这里插入图片描述

继续执行到First(…) 函数第一行其结构如下图2:在这里插入图片描述在这里插入图片描述
我们看到 key是字符串形式的 value是一个接口,结构体如下:

type Interface interface {Name() string   // 获取关键字名字 用来生成keyBuild(Builder)  // todo MergeClause(*Clause) // 添加key对应的value(sql语句)
}

几乎所有dml关键字都实现了这个接口,我们来看下有哪些,如下图:
在这里插入图片描述
我们来看下Where的实现逻辑,源码如下(其他的dml操作可自行查看):

func (stmt *Statement) AddClause(v clause.Interface) {if optimizer, ok := v.(StatementModifier); ok {optimizer.ModifyStatement(stmt)} else {name := v.Name() // 获取关键字 wHEREc := stmt.Clauses[name] // 获取对应的结构体c.Name = name // 赋值v.MergeClause(&c) // 添加 本sql结构体 (v中包含sql)到对应v中的数组中stmt.Clauses[name] = c // 赋值map}
}

其中v.MergeClause(&c)代码如下:

// MergeClause merge where clauses
func (where Where) MergeClause(clause *Clause) {if w, ok := clause.Expression.(Where); ok {//  深copyexprs := make([]Expression, len(w.Exprs)+len(where.Exprs))copy(exprs, w.Exprs)copy(exprs[len(w.Exprs):], where.Exprs)where.Exprs = exprs // 将对应sql结构体 存入数组}// 赋值给 Exoression 到这里 map[string]clause.Clause key:Whereh 对应的 value就赋值完毕了clause.Expression = where
}

这样就形成了如结构图1所示的结构。其他的关键字累加逻辑基本一致,不在赘述,经过不断累加就能形成如结构图2所示结构。
总结:每个dml要想加入map基本都需要两步 1:sql语句处理 2:加入value对应的结构体参数中。

查询条件转sql

上面dml关键字和对应的sql value结构体组合成了map[string]clause.Clause,下面就是将 map[string]clause.Clause转换成sql。到这里可能会有疑问,map中就三个关键字啊,缺少关键的SELECT 和 FROM。剩下的两个关键字,由于是查询语句必备的,所以会在转换成sql的函数中给自动添加上。
查询条件map[string]clause.Clause 转sql的涉及的函数调用链如下:
在这里插入图片描述
// from 的value 中 tables为啥是 nil那 sql语句累计时 从 db.statement.Tables处获取值
添加上 SELECT 和 FROM 后的完整示意图如下:
在这里插入图片描述
完整的map组合完毕后,接下来就开始组装原生sql,我们都知道select语句的关键字出现的先后顺序是一定的,所以我们需要有一个关键字先后顺序列表来约束sql语句生成过程中出现的顺序,这就是 Statement的BuildClauses关键字,这个关键字在执行db.open(…)初始化注册回调函数时会初始化,各个dml都有一个特定的数组。如下:
在这里插入图片描述
讲完大致的执行流程我们来看下BuildQuerySQL(…)源码:

func BuildQuerySQL(db *gorm.DB) {// ... db.Statement.AddClause(fromClause)} else {//  map[string]clause.Clause中添加 FROM关键字db.Statement.AddClauseIfNotExists(clause.From{})}//  map[string]clause.Clause中添加 SELECT关键字db.Statement.AddClauseIfNotExists(clauseSelect)// BuildClauses 组合sql时 需要的关键字 按照先后顺序来组合 比如 先是   map[SELECT] 参与组合SQL语句db.Statement.Build(db.Statement.BuildClauses...)}
}

可以看到 在map中加入 SELECT和 FROM关键字后,开始执行Build(…)函数来构造sql语句,根据map[string]clause.Clause来构造,Statement.SQL和Statement.Vals两个参数,分别是sql语句和sql语句的入参,这两个作为入参来调用原生database/sql。我们来看下Build(…)函数。

func (stmt *Statement) Build(clauses ...string) {var firstClauseWritten bool// 通过BuildClauses关键字出现的先后顺序构造sql for _, name := range clauses {//获取不同DML构造结构体if c, ok := stmt.Clauses[name]; ok {if firstClauseWritten {stmt.WriteByte(' ')}firstClauseWritten = trueif b, ok := stmt.DB.ClauseBuilders[name]; ok {b(c, stmt)} else {// 调用相应dml的构造方法,构造sql,Statement.sql 采用strings.Builder函数来不断累加,当碰到语句中的占位符或者限制条件等时,// 就从map[string]clause.Clause 的value中取出值赋值给 vals。 这里是各个查询关键字实现累计sql的地方,不在详细说明,感兴趣的可以扒扒源码。c.Build(stmt)}}}
}

最后的执行流程应该是这样的。
在这里插入图片描述
这里就是gorm思想的核心了,先根据各个dml 操作的gorm链式语句,生成map[string]clause.Clause ,然后根据关键字的先后顺序BuildClauses数组,逐渐补充完整完整 Statement的SQL和Vals两个属性。gorm dml操作核心就是根据散装的map[string]clause.Clause生成Statement的SQL和Vals两个属性值。这两个值就是调用原生database/sql的入参。
接下来就是对原生sql的调用了。比较简单我们来梳理下。

回调函数调用原生database/sql 方法

其实在讲解sql语句的组合的时候已经说过了部分调用链,我们来看下完整的调用链:

在这里插入图片描述
我们通过代码再来走一遍上面的流程

First(…)函数的源码如下:

func (db *DB) First(dest interface{}, conds ...interface{}) (tx *DB) {// 由于是 First 所以返回按照主键排序的第一条 这边也加入到 sql组合数组中 链式累加tx = db.Limit(1).Order(clause.OrderByColumn{Column: clause.Column{Table: clause.CurrentTable, Name: clause.PrimaryKey},})if len(conds) > 0 {if exprs := tx.Statement.BuildCondition(conds[0], conds[1:]...); len(exprs) > 0 {tx.Statement.AddClause(clause.Where{Exprs: exprs})}}tx.Statement.RaiseErrorOnNotFound = truetx.Statement.Dest = dest// 开始执行Query的回调函数,然后执行Execute(...)函数,回调函数在这个函数里进行链式调用。(dml的其他操作也是这个调用逻辑)return tx.callbacks.Query().Execute(tx)

其中 Query就是返回承载有回调函数的processor结构体。Query代码如下:

func (cs *callbacks) Query() *processor {return cs.processors["query"]
}

各个dml操作返回各自的回调函数。其结构之间的关系可以看 初始化那章结构体关系图。
然后执行 processor 的Execute(…)函数 我们看下其源码:

func (p *processor) Execute(db *DB) *DB {// ...// call scopes// sql语句拼接在f(db)中完成 然后调用database/sql 执行查询// 这边调用的都是注册的Query相关的函数 ,主要是查询操作,包括Query、Preload等(其他的dml操作这边是注册的相应的操作函数)for _, f := range p.fns {f(db)}// ...return db
}

Execute(…)执行链式函数到注册的Query(),其源码如下:


func Query(db *gorm.DB) {if db.Error == nil {// 此函数会组装sql 和 提取出 占位符 作为 入参 调用database/sql的原生函数BuildQuerySQL(db)if !db.DryRun && db.Error == nil {// 调用database/sql 方法rows, err := db.Statement.ConnPool.QueryContext(db.Statement.Context, db.Statement.SQL.String(), db.Statement.Vars...)if err != nil {db.AddError(err)return}defer func() {db.AddError(rows.Close())}()gorm.Scan(rows, db, 0)}}
}

到这里整个调用联调就完成了,再次强调其他的dml操作的整个调用逻辑跟查询操作是一致的,只是有些调用细节不同,不在赘述。

事务

未完待续…

总结

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

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

相关文章

A Simple Semi-Supervised Learning Framework for Object Detection

1. Introduction SSL的成功主要有以下两个方面: (1)一致性正则化:如果对一个未标记的数据应用实际的扰动, 其预测结果不应该发生显著变化, 也就是输出具有一致性,通过在未标记数据上构造添加扰动后的预测结果 y~​ 与…

【51 Pandas+Pyecharts | 深圳市共享单车数据分析可视化】

文章目录 🏳️‍🌈 1. 导入模块🏳️‍🌈 2. Pandas数据处理2.1 读取数据2.2 查看数据信息2.3 处理起始时间、结束时间2.4 增加骑行时长区间列2.5 增加骑行里程区间列 🏳️‍🌈 3. Pyecharts数据可视化3.1 各…

webGlL变量的声明与使用

抢先观看&#xff1a; 变量的声明格式&#xff1a;<存储限定符><类型限定符><变量名> 存储限定符&#xff1a;const, attribute, uniform, varying, buffer。 类型限定符&#xff1a;void, bool, int, float, double, vec2, vec3, vec4, mat2, mat3, mat4, s…

基于SSM的成都市旅游信息管理系统-计算机毕业设计源码65815

SSM成都市旅游信息管理系统 摘 要 本论文主要论述了如何使用SSM框架开发一个旅游信息管理系统&#xff0c;严格按照软件开发流程进行各个阶段的工作&#xff0c;采用B/S架构JAVA技术&#xff0c;面向对象编程思想进行项目开发。在引言中&#xff0c;作者将论述旅游信息管理系…

91.【C语言】数据结构之单向链表的头删和尾删

目录 1.尾删函数SLTPopBack 代码示例(写入SList.c) 在SList.h中写入该函数的声明 main.c部分代码改为 ​编辑 分析 解决方法 方法1:双指针算法(快指针tail,慢指针pretail) 方法2 2.头删函数SLTPopFront 一个节点示意图 多个节点示意图 代码示例(写入SList.c) 在S…

DEVOPS: 集群伸缩原理

概述 阿里云 K8S 集群的一个重要特性&#xff0c;是集群的节点可以动态的增加或减少有了这个特性&#xff0c;集群才能在计算资源不足的情况下扩容新的节点&#xff0c;同时也可以在资源利用 率降低的时候&#xff0c;释放节点以节省费用理解实现原理&#xff0c;在遇到问题的…

华为OD机试 - 无向图染色(Java 2024 E卷 100分)

华为OD机试 2024E卷题库疯狂收录中&#xff0c;刷题点这里 专栏导读 本专栏收录于《华为OD机试&#xff08;JAVA&#xff09;真题&#xff08;E卷D卷A卷B卷C卷&#xff09;》。 刷的越多&#xff0c;抽中的概率越大&#xff0c;私信哪吒&#xff0c;备注华为OD&#xff0c;加…

云智慧完成华为原生鸿蒙系统的适配, 透视宝 APM 为用户体验保驾护航

2024 年 10 月 22 日&#xff0c;首个国产移动操作系统 —— 华为原生鸿蒙操作系统 HarmonyOS NEXT 正式面世&#xff0c;成为继 iOS 和 Android 后的全球第三大移动操作系统。HarmonyOS NEXT&#xff0c;从系统内核、数据库根基&#xff0c;到编程语言创新、AI&#xff08;人工…

无人机之任务分配算法篇

无人机的任务分配算法是无人机系统中的重要组成部分&#xff0c;它决定了无人机如何高效、合理地执行各种任务。以下是一些常见的无人机任务分配算法&#xff1a; 一、合同网协议&#xff08;Contract Net Protocol, CNP&#xff09; 基本概念&#xff1a;CNP算法是一种分布式…

【WRF数据处理】基于GIS4WRF插件将geotiff数据转为tiff(geogrid,WPS所需数据)

【WRF数据处理】基于GIS4WRF插件将geotiff数据转为tiff&#xff08;geogrid&#xff0c;WPS所需数据&#xff09; 数据准备&#xff1a;以叶面积指数LAI为例QGis实操&#xff1a;基于GIS4WRF插件将geotiff数据转为tiff警告&#xff1a;GIS4WRF: Input layer had an unexpected …

【MySQL基础】高级查询

文章目录 一、聚合函数&#xff1a;COUNT、SUM、AVG、MIN、MAX1. 统计总数&#xff1a;COUNT2. 计算总和&#xff1a;SUM3. 计算平均值&#xff1a;AVG4. 找最小值&#xff1a;MIN5. 找最大值&#xff1a;MAX 综合使用聚合函数的例子小结 二、分组查询——GROUP BY 和 HAVING1.…

ElasticSearch备考 -- Index shrink

一、题目 索引task包括5个分片一个副本&#xff0c;对索引执行shrink压缩操作&#xff0c;压缩后索引为1主分片&#xff0c;索引名称为task-new 二、思考 在执行shrink前必须满足三个前置条件 The index must be read-only.A copy of every shard in the index must reside o…

名词(术语)了解--CSSOM (CSS Object Model)

名词&#xff08;术语&#xff09;了解–CSSOM (CSS Object Model) CSSOM 概述 CSSOM 是一个与 DOM (Document Object Model) 相对应的、用于 CSS 的 API 集合。 它提供了一种程序化的方式来读取和修改文档的样式信息。 CSSOM 的主要组成部分 样式规则树 document └── …

智能化超声波影像分析,优化医疗决策的开源AI解决方案

思通数科的医疗信息精准抽取系统是一款基于人工智能的开源软件&#xff0c;旨在自动化处理医疗数据&#xff0c;特别是从超声波影像到诊断报告的信息提取。该系统集成了图像识别、自然语言处理和知识图谱等先进技术&#xff0c;能够从医疗影像中提取关键数据&#xff0c;并将这…

Objective-C 音频爬虫:实时接收数据的 didReceiveData_ 方法

在互联网技术领域&#xff0c;数据的获取和处理是至关重要的。尤其是对于音频内容的获取&#xff0c;实时性和效率是衡量一个爬虫性能的重要指标。本文将深入探讨在Objective-C中实现音频爬虫时&#xff0c;如何高效地使用didReceiveData:方法来实时接收数据&#xff0c;并通过…

Python轴承故障诊断 (15)基于CNN-Transformer的一维故障信号识别模型

往期精彩内容&#xff1a; Python-凯斯西储大学&#xff08;CWRU&#xff09;轴承数据解读与分类处理 Pytorch-LSTM轴承故障一维信号分类(一)-CSDN博客 Pytorch-CNN轴承故障一维信号分类(二)-CSDN博客 Pytorch-Transformer轴承故障一维信号分类(三)-CSDN博客 三十多个开源…

如何在 Elasticsearch Ruby 客户端中使用 ES|QL Helper

作者&#xff1a;来自 Elastic Fernando Briano 了解如何使用 Elasticsearch Ruby 客户端编写 ES|QL 查询并处理其结果。 简介 Elasticsearch Ruby 客户端可用于编写 EQ|QL 查询&#xff0c;使处理从 esql.query 返回的数据更加容易。ES|QL 允许开发人员通过查询过滤、转换和分…

【elkb】ELKB安装token过期

问题 elastic启动时候生成的token 有效期只有30分钟。 30分钟后提示&#xff1a; Couldnt configure Elastic Generate a new enrollment token or configure manually. 相关版本信息 elasticsearch&#xff1a;8.8.1kibana&#xff1a;8.8.1logstash&#xff1a;8.8.1file…

交易所开发:开启数字金融新时代

当今数字化高速发展的时代&#xff0c;交易所作为金融市场的核心枢纽&#xff0c;发挥着至关重要的作用。而随着区块链技术的兴起&#xff0c;数字货币交易所的开发更是为金融领域带来了全新的变革与机遇。 一、数字货币交易所的重要性 数字货币交易所是连接数字货币世界与传统…

企业内训|LLM大模型在服务器和IT网络运维中的应用-某日企IT运维部门

本课程是为某在华日资企业集团的IT运维部门专门定制开发的企业培训课程&#xff0c;本课程旨在深入探讨大型语言模型&#xff08;LLM&#xff09;在服务器及IT网络运维中的应用&#xff0c;结合当前技术趋势与行业需求&#xff0c;帮助学员掌握LLM如何为运维工作赋能。通过系统…