Datebase/SQL和GORM解读 | 青训营笔记

143 阅读4分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 16 天

今天和大家分享SQL和GORM解读笔记。

前传:database/sql

基础概述

database/sql包为go实现了一套统一的抽象的接口,用来连接数据库和类数据库

由于他只是提供了一套统一的抽象接口,具体实现还是要引用到各个数据库实现的driver来实际实现连接数据库。

例如,我们如果要连接mysql数据库:

import (
    "database/sql"
    "time"
​
    _ "github.com/go-sql-driver/mysql"
)
​
// ...
// 连接数据库
db, err := sql.Open("mysql", "user:password@/dbname")
if err != nil {
    panic(err)
}
​
// ...
// 查询数据库,最终释放链接
rows, err := sql.Query("select id, name from users where id = ?", 1) // rows为得到的记录游标
defer rows.Close()
​
// 得到所有符合查询语句的记录, 这里我们一直去向后查询游标,直到没有记录了  tips: 这里没有处理错误,正式使用中一定要及时处理
for rows.Next() {
    var user User
    _ := rows.Scan(&user.ID, &use.Name)
    users = append(users, user)
}

设计原理

database/sql 作为一个连接上层应用程序和底层的数据库之间的框架,它对上实现了一个操作接口,让我们可以对不同的sql库使用一套操作接口;对下它实现了连接接口和操作接口,让不同的数据库实现自己的 driver 来适配 database/sql。它本身则实现了一个连接池,用来适应大连接量操作。

GORM

基础概念

GORM 是一个设计简介,功能强大,自由扩展的全功能 ORM。GORM 是为了避免 database/sql 需要写大量的sql语句,通过封装 database/sql 并且加入了很多实用性扩展实现的一个框架。

GORM 指南中对GORM的功能介绍和基本使用都已经非常完善了,这里不再赘述。

GORM 设计原理

sql 是怎么生成的?

对于一般的 SQL 语句,以查询语句为例,可能会具有 SELECT Clause,FROM Clause,WHERE Clause,ORDER BY Clause,LIMIT Clause,FOR Clause :

SELECT `name` FROM `USER` WHERE age > 35 AND role <> `manager` ORDER BY age DESC LIMIT 10 OFFSET 10 FOR UPDATE

对应我们的 gorm 代码:

db.Where("role <> ?", "manager").Where("age > ?", 35).Limit(100).Order("age desc").Find(&user)

其中,Where 函数,Limit 函数和 Order 函数对应的都是 Chain Method,最终的 Find 函数则是一个 Finisher Method。

Chain Method 的作用在于给 gorm 添加子句,而最终的 Finisher Method 的作用则是指定类型和执行。

例如,这里我们的 Finisher Method 是 Find 方法,那么 gorm 在生成 SQL 时,会首先添加进来 SELECT FROM 指令。而我后面的Chain Method 则是对应添加 WHERE Clause,ORDER BY Clause,LIMIT Clause 等子句。

以 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
}

可以比很清楚的看到,其中的 AddClause 方法将我们的给出的条件都作为一个个的子句中的表达式添加到了 WHERE Clause 中。

而对于 Finisher Method 而言,我们以 First 为例(此处默认第二个参数条件没有填,下面代码也会省略相应部分):

func (db *DB) First(dest interface{}, conds ...interface{}) (tx *DB) {
    tx = db.getInstance() // 这里后期有改动,但核心逻辑还是创建了一个数据库实例
    // ...
    tx.Statement.Dest = dest 
    return tx.callbacks.Query().Execute(tx)
}

在 gorm 中,我们也可以显式地去调用 Claus() 去拓展 各部分的 SQL 子句:高级查询 | GORM - The fantastic ORM library for Golang, aims to be developer friendly.

插件系统

当我们的 Finisher Method 执行之后,gorm 的插件系统开始执行一系列动作:首先分析 Finisher Method,得到 Statement 类型,例如 SELECT,UPDATE 等,并且根据其类型,执行对应的回调方法,生成 SQL 语句并执行。

例如,对于一个 Create 方法,我们分析处他的 Statement 类型后去执行对应 Callback:

// 对应的db.Create()
func (db *DB) Create(value interface{}) (tx *DB) {
    return tx.callbacks.Create().Execute(tx) // 核心代码 这里去找 callbacks.Create() 的相关方法去执行
}
// 对应callbacks.Create()
func (cs *callbacks) Create() *processor {
    return cs.processors["create"] // 返回记录下来的所有相关 callback,这里使用map保存
}
// Execute() 执行callback
func (p *processor) Execute(db *DB) *DB {
    // ...
    for _, f := range p.fns {
        f(db)
    }
}

当然,我们也可以去扩展插件来让每次执行 Create,Update 等方法时,可以工作我们自己的插件:

// 注册新的插件
db.Callback().Create().Register("mypligin", func(*gorm.DB) {})
// 删除
db.Callback().Create().Remove("myplugin")
// 替换
db.MysqlDB.Callback().Create().Replace("myplugin", func(db *gorm.DB) {})
// 指定顺序
db.Callback().Create().Before("gorm:create").After("gorm:after_create").Register("myplugin", func(db *gorm.DB) {})
// 注册到最前面
db.Callback().Create().Before("*").Register("myplugin", func(db *gorm.DB) {})

更多内容:编写插件 | GORM - The fantastic ORM library for Golang, aims to be developer friendly.

gorm 插件可以实现多种功能,例如多数据库的读写分离:

DBResolver | GORM - The fantastic ORM library for Golang, aims to be developer friendly.

ConnPool

ConnPool 是 gorm 定义的一个接口层,并且使用 database/sql 包去实现这些接口从而使用 ConnPool 作为新的连接池层。

这样的设计可以使我们实际和数据库进行交互的连接池层和 DB Connect 层解耦,在需要使用不同的数据库存储数据库或者需要进行读写分离时可以更加方便。