golang为什么会有Gorm?特性有哪些?

2023年 7月 11日 27.7k 0

为什么会有Gorm

GORM的出现主要是为了解决Go的原生SQL库在使用上的不便利和不足。原生SQL库需要手动拼接SQL语句和参数,这样容易出错且不便于维护。此外,原生库对于一些高级功能(例如连接池管理、自动Migrate、Hooks等)的支持也比较有限,需要用户自己实现。

  • 原生SQL库需要手动拼接SQL语句和参数,容易出错且不便于维护。

举个例子,如果我们要查询所有年龄在18岁以下的用户,可以使用如下的SQL语句:

SELECT * FROM users WHERE age < 18;

使用原生SQL库实现时,我们需要手动拼接SQL语句和参数,如下所示:

age := 18
query := fmt.Sprintf("SELECT * FROM users WHERE age < %d", age)
rows, err := db.Query(query)

如果我们同时需要查询年龄在18岁以下且性别为男性的用户,那么我们需要修改SQL语句和参数,如下所示:

age := 18
gender := "male"
query := fmt.Sprintf("SELECT * FROM users WHERE age < %d AND gender = '%s'", age, gender)
rows, err := db.Query(query)

这样的代码容易出错,而且不容易维护。相比之下,使用GORM实现可以更加方便:

var users []User
db.Where("age < ?", 18).Find(&users)

通过GORM,我们只需要调用db.Where("age < ?", 18)就可以方便地实现查询年龄在18岁以下的用户了。如果我们需要查询年龄在18岁以下且性别为男性的用户,只需要稍微修改一下就可以了:

var users []User
db.Where("age < ? AND gender = ?", 18, "male").Find(&users)

这样的代码更加直观和易于维护。

  • GORM体现了ORM(对象关系映射)的思想。

ORM是一种将关系型数据库中的表和行映射到编程语言中的对象和属性的技术。它能够将数据库操作转化为面向对象的操作,大大提高了开发效率和代码可维护性。在这套思想下,数据库的每一行数据其实对映射成编程语言中的Model对象,视为一个整体,每一次查询更新删除都是对整个Model对象的操作,而原生的SQL库通过RawSQL的查询,分割出每一行的值,在查询时只是拿出了某个特定条件的值。

  • GORM提供了更多高级的功能

GORM提供了更便捷的API和更多的高级功能,例如自动生成SQL语句、连接池管理、Hooks、事务支持等。这些功能大大提高了开发效率和代码可维护性,使得开发者可以更专注于业务逻辑的实现。

GORM的特性

连接池管理

其实GORM并没有单独的实现db的连接池,

使用 database/sql 维护连接池,复用了sql.DB 连接,下面简单讲解一下database/sql中的源码

// SetMaxIdleConns 设置空闲连接池中连接的最大数量 sqlDB.SetMaxIdleConns(10)

maxIdle := db.maxIdleConnsLocked()
 if idleCount > maxIdle {
  closing = db.freeConn[maxIdle:]
  db.freeConn = db.freeConn[:maxIdle]
 }
 db.maxIdleClosed += int64(len(closing))
 db.mu.Unlock()
 for _, c := range closing {
  c.Close()
 }

maxIdleConnsLocked就是拿到SetMaxIdleConns里设置的最大数量maxIdle,如果超过了就把超出的部分关掉。

SQL生成

SQL的生成其实跟分词后对句子做语法分析差不多,但其实更容易些

golang为什么会有Gorm?特性有哪些?SQL生成

db.Where("age < ? AND gender = ?", 18, "male").Find(&users)

像上面这个例子,其实遇到了Where,就可以用把Where这个Clause加进Statement中

// Where add conditions
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
}

Clause中最关键的其实就是Expression的基本单元,分别实现了In,Gte,Eq,Lt,Neq,Lte,Gt和Like,BuildCondition 就是对Where中的语句分词后封装成了不同的Expression,再解析到底是大于还是小于,再分出Column和Value。像上面的例子就是,Column=”age”,Value=”18“,Expression=”Lt“。

plugin机制

GORM提供了plugin机制,可以通过编写插件的方式对GORM的行为进行调整或扩展。比如,我们可以通过编写一个插件来实现对所有GORM操作的日志记录,或者实现对GORM的自定义数据缓存逻辑。

插件是通过实现gorm.Plugin接口来实现的。gorm.Plugin接口只有一个方法Apply(*gorm.DB),该方法在每次gorm.Open()或gorm.DB()方法调用时都会被调用。在Apply()方法中,我们可以通过gorm.DB对象对GORM的行为进行调整或扩展。比如,我们可以通过db.Callback()方法注册回调函数,在GORM执行某些操作时执行我们的自定义逻辑。

实现起来其实很简单,提供特定的接口,实现之后注册进去,再每次调用Create或者Update前可以调用之前已经注册好的插件的方法。

GORM的plugin机制有以下的回调:

  • BeforeCreate
  • BeforeSave
  • BeforeUpdate
  • BeforeDelete
  • AfterCreate
  • AfterSave
  • AfterUpdate
  • AfterDelete

我们可以根据这些回调来实现不同的扩展,例如在BeforeSave时自动填充某些字段,或者在AfterDelete时触发一些异步任务等。

如果我们要实现一个Update之后打印出来RowsAffect的行数时,我们就可以注册一个插件。这里是一个示例:

type RowsAffectedPlugin struct{}

func (p *RowsAffectedPlugin) Name() string {
 return "RowsAffectedPlugin"
}

func (p *RowsAffectedPlugin) Initialize(db *gorm.DB) (err error) {
 db.Callback().Update().After("gorm:after_update").Register("RowsAffectedPlugin:after_update", func(db *gorm.DB) {
  affected := db.RowsAffected
  fmt.Printf("Affected rows: %d\n", affected)
 })
 return
}

我们定义了一个叫做RowsAffectedPlugin的插件,它会在每次更新操作之后打印出受影响的行数。在Initialize()方法中,我们通过db.Callback().Update().After("gorm:after_update")来定义了执行回调函数的顺序,然后在回调函数中打印出受影响的行数。最后,我们需要在使用GORM的代码中注册该插件:

db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
 // 注册插件
 Plugins: []gorm.Plugin{
  &AffectedRowsPlugin{},
 },
})

这样,在每次更新操作之后,就会自动打印出受影响的行数了。

Callback

Plugin机制提供了一个Initialize的方法即可实现插件化,这也是基于Callback机制的基础上的,回调机制是什么呢?

每一个 DB 对象对应一个 callbacks对象,一个callbacks对象中包含了多个processor(其实只有六个create,query,update,delete,row,raw),每个 processor 又对应了一组 callback 对象,每个callback对象中又包含了多个processor,这些processor一般是before, after等回调操作。

func initializeCallbacks(db *DB) *callbacks {
 return &callbacks{
  processors: map[string]*processor{
   "create": {db: db},
   "query":  {db: db},
   "update": {db: db},
   "delete": {db: db},
   "row":    {db: db},
   "raw":    {db: db},
  },
 }
}

golang为什么会有Gorm?特性有哪些?callback

以Take函数为例:

// Take return a record that match given conditions, the order will depend on the database implementation
func (db *DB) Take(dest interface{}, conds ...interface{}) (tx *DB) {
 tx = db.Limit(1)
 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 = true
 tx.Statement.Dest = dest
 return tx.callbacks.Query().Execute(tx)
}

Take这个终结方法,会先把Condition像上面小节阐述的一样生成不同的clause并对Expression进行解析,然后生成SQL语句,最后会落到Query这个callback上执行Execute

type processor struct {
 db        *DB
 Clauses   []string
 fns       []func(*DB)
 callbacks []*callback
}

processor 的 Execute 方法本质上就干了三件事:

  • 解析 model。我们需要定义一个 gorm 的 struct,对应到表结构,这里就是去解析传入的对象,拿到字段类型,解析 tag;
  • 根据传入的 Dest 去给 model 赋值,还是依靠反射完成的;
  • 获取 processor 中注册的 fns 函数数组,依次 call 每个函数。
  • golang为什么会有Gorm?特性有哪些?Execute流程图

    其实fns就是多个callback函数,实际去 Register 一个回调函数时,是直接 append 进数组,同时触发一个 compile 操作。这个 compile 就是会遍历整个数组,根据 before, after 那些属性,理清楚各个 callback 的最终执行顺序,赋值给 fns 变量。

    链式方法

    链式方法是将 Clauses 修改或添加到当前 Statement 的方法,例如:

    Where, Select, Omit, Joins, Scopes, Preload, Raw

    终结(方法) 是会立即执行注册回调的方法,然后生成并执行 SQL,比如这些方法:

    Create, First, Find, Take, Save, Update, Delete, Scan, Row, Rows

    链式方法, 终结方法之后, GORM 返回一个初始化的 *gorm.DB 实例,实例不能安全地重复使用,并且新生成的 SQL 可能会被先前的条件污染,例如:

    queryDB := DB.Where("name = ?", "jinzhu")
    
    queryDB.Where("age > ?", 10).First(&user)
    // SELECT * FROM users WHERE name = "jinzhu" AND age > 10
    
    queryDB.Where("age > ?", 20).First(&user2)
    // SELECT * FROM users WHERE name = "jinzhu" AND age > 10 AND age > 20
    

    一些源码阅读后的使用技巧

  • 在业务场景中,经常会使用到查一批数据,但是查一批数据有时候数据量太多,不好全放到内存中,我们希望一批一批的查出。使用FindInBatches实现批量查库,可以参照如下实现:
  • batchResults每次都把results放到slice中,最后batchResults就是长度为3的[2]User。这里需要注意的是results只是最后一个batch的结果。

    func TestFindInBatches(t *testing.T) {
     users := []User{
      *GetUser("find_in_batches", Config{Friends: 1}),
      *GetUser("find_in_batches", Config{Friends: 2}),
      *GetUser("find_in_batches", Config{Friends: 3}),
      *GetUser("find_in_batches", Config{Friends: 4}),
      *GetUser("find_in_batches", Config{Friends: 5}),
     }
    
     DB.Create(&users)
    
     var (
      batchResults [][]User
      results    []User
      totalBatch int
     )
     if result := DB.Where("name = ?", users[0].Name).FindInBatches(&results, 2, func(tx *gorm.DB, batch int) error {
      totalBatch += batch
      batchResults = append(batchResults, results)
      return nil
     }); result.Error != nil {
      t.Errorf("Failed to batch find, got error %v, rows affected: %v", result.Error, result.RowsAffected)
     }
    }

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论