0


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为例来介绍)

SELECT*FROM userinfos WHERE name = "lisan” ORDERBY id ASCLIMIT1;

则 database/sql 代码如下

    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,则代码如下:

//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(…)函数
在这里插入图片描述
其调用链是

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的核心结构体,所有操作都由它承载。

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需要的一些配置和初始化后的一些属性,比如连接池和回调函数等。

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主要实现各种数据库的连接等操作,其结构如下:

type Dialector interface{Name()stringInitialize(*DB)error// 数据库连接池等的初始化 databae/sql.db的初始化 和 注册回调的原生database/sql dml 封装函数等Migrator(db *DB) Migrator  // 装载操作 也就是承接 ddl的一些功能DataTypeOf(*schema.Field)stringDefaultValueOf(*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的相关函数,结构体如下:

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属性 其结构体如下:

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

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

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操作的回调函数
初始化代码是示例的前几行,如下:

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(…)函数的实现:

funcOpen(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回调函数填入callbacksif 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)中,我们来看下:

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(…)函数如下:

funcRegisterDefaultCallbacks(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
    // ...}

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

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

自动装载(ddl操作)

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

type Userinfo struct{
    Id     uint
    Name   string
    Gender string
    Hobby  string
    Addr   string
    Age    uint8}funcTestGorm(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的表
在这里插入图片描述
其代码比较简单,但内部有复杂的逻辑,我们来简单梳理下。

自动装载源码如下:

// AutoMigrate run auto migration for given modelsfunc(db *DB)AutoMigrate(dst ...interface{})error{return db.Migrator().AutoMigrate(dst...)}

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

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

Migrator 接口如下:

// ddl相关操作 可以增删 表 表的属性 视图 限制条件 索引 等;可以查看数据库type Migrator interface{// AutoMigrateAutoMigrate(dst ...interface{})error// Database 数据库相关操作CurrentDatabase()stringFullDataTypeOf(*schema.Field) clause.Expr
    GetTypeAliases(databaseTypeName string)[]string// Tables 表相关操作CreateTable(dst ...interface{})errorDropTable(dst ...interface{})error// ...// Columns 列相关操作AddColumn(dst interface{}, field string)errorDropColumn(dst interface{}, field string)error// ...// Views 视图相关操作CreateView(name string, option ViewOption)errorDropView(name string)error// Constraints 限制条件相关操作CreateConstraint(dst interface{}, name string)errorDropConstraint(dst interface{}, name string)errorHasConstraint(dst interface{}, name string)bool// Indexes 索引相关操作CreateIndex(dst interface{}, name string)errorDropIndex(dst interface{}, name string)error// ...}

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

// AutoMigrate auto migrate valuesfunc(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: 从数据库获得 表的 属性名信息 columnTypesfor_, 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
                        }}}returnnil}); err !=nil{return err
            }}}returnnil}

AutoMigrate函数中需要的核心调用函数 都来自 Migrator 结构体 ,我们选择HasTable()函数来简单梳理下。
**HasTable(…)**用来判断是否存在特定表。其源码如下:

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中还有好多值得深挖的点,由于篇幅原因不做介绍,感兴趣的大神可以深挖下。

标签: 1024程序员节

本文转载自: https://blog.csdn.net/u013915286/article/details/142480079
版权归原作者 fengzhuzhigu 所有, 如有侵权,请联系我们删除。

“Go第三方框架--gorm框架(一)”的评论:

还没有评论