|
|
|
|
# 25|GORM(上):数据库的使用必不可少
|
|
|
|
|
|
|
|
|
|
你好,我是轩脉刃。
|
|
|
|
|
|
|
|
|
|
一个 Web 应用,有很大部分功能是对数据库中数据的获取和加工。比如一个用户管理系统,我们在业务代码中需要频繁增加用户、删除用户、修改用户等,而用户的数据都存放在数据库中。所以对数据库的增删改查,是做 Web 应用必须实现的功能。而我们的 hade 框架如何更好地支持数据库操作呢?这两节课我们就要讨论这个内容。
|
|
|
|
|
|
|
|
|
|
## ORM
|
|
|
|
|
|
|
|
|
|
提到数据库,就不得不提ORM了,有的同学一接触 Web 开发,就上手使用 ORM 了,这里我们要明确一点:ORM 并不等同于数据库操作。
|
|
|
|
|
|
|
|
|
|
数据库操作,本质上是使用 SQL 语句对数据库发送命令来操作数据。而 ORM 是一种将数据库中的数据映射到代码中对象的技术,这个技术的需求出发点就是,**代码中有类,数据库中有数据表,我们可以将类和数据表进行映射,从而使得在代码中操作类就等同于操作数据库中的数据表了**。
|
|
|
|
|
|
|
|
|
|
ORM 这个概念出现的时间无从考究了,基本上从面向对象的编程思想出来的时候就有讨论了。但是到现在,是否要使用 ORM 的讨论也一直没有停止。
|
|
|
|
|
|
|
|
|
|
不支持使用 ORM 的阵营的观点基本上是使用 ORM 会影响性能,且会让使用者不了解底层的具体最终拼接出来的SQL,容易造成用不上索引或者最终拼接错误的情况。而支持使用 ORM 的阵营的观点主要是它能切切实实加速应用开发。
|
|
|
|
|
|
|
|
|
|
就我个人的观点和经验,我还是支持使用 ORM 的。我认为 ORM 不仅仅是一种映射技术,也是一种建模思想。**因为数据库是和业务紧密关联起来的,建立数据库表结构的时候,也是建立了一个业务模型**。使用代码中的类定义,比如定义了一个 User 类,基本上就定义了一个 User 表,这样也是一个建立业务模型的过程。
|
|
|
|
|
|
|
|
|
|
其实不论 ORM 的讨论如何激烈,基本上各个语言都已经有了 ORM 的实现,比如 Java 的 Hibernate、PHP 的 Doctrine、Ruby 的 ActiveRecord。而在 Golang 中,现在最流行的 ORM 库是国人的开源项目[Gorm](https://github.com/go-gorm/gorm) 。
|
|
|
|
|
|
|
|
|
|
Gorm 作者 Jinzhu 目前是字节跳动的员工,他在 GitHub 上开源共享了诸如 copier、configer 等开源项目,Gorm 目前 star 数有 26k 之多,使用 MIT 的许可证协议,项目启动于 2013 年,目前是 v2 版本。
|
|
|
|
|
|
|
|
|
|
这个 **v2 版本对应 Gorm GitHub 上 v1.20 以上的 tag**,这点我们要额外注意。因为网上的分析文章很多都是基于Gorm 的 v1 版本写的,但Gorm 的 v1 和 v2 版本相差比较大。所以在看 Gorm 文章的时候需要先明确下是什么版本。
|
|
|
|
|
|
|
|
|
|
我们的框架侧重于整合,站在巨人的肩膀上才更符合现代化框架的要求。基于此, hade 框架并不打算重新开发一套 ORM 框架,而是会直接融合 Gorm 框架成为我们容器中的一个服务 orm service。
|
|
|
|
|
|
|
|
|
|
版本选择的是 Gorm 截止 2021/10/23 日最新的 v1.21.16 的 tag。毕竟 Gorm 是个有一定体量的项目,而且理解它的重点部分源码的实现原理,对使用者来说非常重要,值得我们先花一章来学习理解。如何融合 Gorm,我们下节课继续学。
|
|
|
|
|
|
|
|
|
|
## Gorm
|
|
|
|
|
|
|
|
|
|
一个 ORM 库,最核心要了解两个部分。一个部分是数据库连接,它是怎么和数据库建立连接的,第二部分是数据库操作,即它是怎么操作数据库的。
|
|
|
|
|
|
|
|
|
|
我们看一个最精简的 Gorm 的使用例子:
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"gorm.io/gorm"
|
|
|
|
|
"gorm.io/driver/mysql"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// 定义一个 gorm 类
|
|
|
|
|
type User struct {
|
|
|
|
|
ID uint
|
|
|
|
|
Name string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func main() {
|
|
|
|
|
// 创建 mysql 连接
|
|
|
|
|
dsn := "xxxxxxx"
|
|
|
|
|
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
|
|
|
|
|
...
|
|
|
|
|
|
|
|
|
|
// 插入一条数据
|
|
|
|
|
db.Create(&User{Name: "jianfengye"})
|
|
|
|
|
|
|
|
|
|
...
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
main 函数,先创建一个 MySQL 连接,再插入一条数据,这个 User 数据是通过事先定义好的 User 结构来进行设置的。**其中的 gorm.Open 就是一个快速连接数据库的接口,而后续的 Create 是如何操作数据库的接口**。
|
|
|
|
|
|
|
|
|
|
我们今天的任务就是理解这几行代码的实现原理,后面会不断拿这个例子举例。
|
|
|
|
|
|
|
|
|
|
## 数据结构
|
|
|
|
|
|
|
|
|
|
先把重点放在理解这个 Open 函数上,因为这个函数包含了 Gorm 中关键的几个对象,把这些关键数据结构一一理解透,再跟踪具体的源码能事半功倍。另外也推荐你边看边自己画出这几个关键参数的关系,非常有助于理解和记忆,每个参数讲完之后我也会展示一下我画的分析图供你参考。
|
|
|
|
|
|
|
|
|
|
来看它的源码定义:
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
// 初始化数据库连接
|
|
|
|
|
func Open(dialector Dialector, opts ...Option) (db *DB, err error)
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
这个初始化数据库链接的Open函数有两个参数dialector、opts,和两个返回值gorm.DB、error。我们先理解下这几个参数的意义。
|
|
|
|
|
|
|
|
|
|
## Dialector
|
|
|
|
|
|
|
|
|
|
第一个参数是 Dialector,这是什么呢?它代表数据库连接器。这里也是一个面向接口编程的思想,连接器结构 Dialector 是一个接口,代表如果你要使用 Gorm 来连接你的数据库,那么,只需要实现这个接口定义的所有方法,就可以使用 Gorm 来操作你的数据库了。
|
|
|
|
|
|
|
|
|
|
所以,这个接口 Dialecotor 中定义的所有方法,都是在后续的查询、更新、数据库迁移等操作中会使用到的。具体每个方法在哪里使用到的,如果你感兴趣可以跟踪下去,如果你不感兴趣也无所谓,只需要记得在后续某个 gorm 接口的具体实现中会用到就行。
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
// Dialector GORM database dialector
|
|
|
|
|
type Dialector interface {
|
|
|
|
|
Name() string // 连接器名称
|
|
|
|
|
Initialize(*DB) error // 连接器初始化连接方法
|
|
|
|
|
Migrator(db *DB) Migrator // 数据库迁移方法
|
|
|
|
|
DataTypeOf(*schema.Field) string // 类中每个字段的类型对应到 sql 语句
|
|
|
|
|
DefaultValueOf(*schema.Field) clause.Expression // 每个字段的默认值对应到 sql 语句
|
|
|
|
|
BindVarTo(writer clause.Writer, stmt *Statement, v interface{}) // 使用预编译模式的时候使用
|
|
|
|
|
QuoteTo(clause.Writer, string) // 将类中的注释对应到 sql 语句中
|
|
|
|
|
Explain(sql string, vars ...interface{}) string // 将有占位符的 sql 解析为无占位符 sql,常用于日志打印等
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
不同的数据库有不同的 Dialector 实现,我们称之为“驱动”。每个数据库的驱动,都有一个 git 地址进行存放。目前 gorm 官方支持五种数据库驱动:
|
|
|
|
|
|
|
|
|
|
* MySQL 的 Gorm 驱动地址为 [](http://gorm.io/driver/mysql) gorm.io/driver/mysql
|
|
|
|
|
* Postgres 的 Gorm 驱动地址为 gorm.io/driver/postgres
|
|
|
|
|
* SQLite 的 gorm 驱动地址为 gorm.io/driver/sqlite
|
|
|
|
|
* SQL Server 的 gorm 驱动地址为 gorm.io/driver/sqlserver
|
|
|
|
|
* ClickHouse 的 gorm 驱动地址为 gorm.io/driver/clickhouse
|
|
|
|
|
|
|
|
|
|
**如果要创建对应数据库的连接,要先引入对应的驱动**。而在对应的驱动库中都有一个约定的 Open 方法,来创建一个新的数据库驱动。比如要创建 MySQL 的连接,使用下面这个例子:
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
import (
|
|
|
|
|
"gorm.io/driver/mysql"
|
|
|
|
|
"gorm.io/gorm"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// 创建连接
|
|
|
|
|
dsn := "gorm:gorm@tcp(localhost:9910)/gorm?charset=utf8&parseTime=True&loc=Local"
|
|
|
|
|
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
我们看到这里有个 mysql.Open,就是创建MySQL 的 Gorm 驱动用的。而这个 Open 函数只有一个字符串参数 DSN,这个参数可能有的同学还不是很了解,我们一起研究下。
|
|
|
|
|
|
|
|
|
|
#### DSN
|
|
|
|
|
|
|
|
|
|
DSN 全称叫 Data Source Name,数据库的源名称。
|
|
|
|
|
|
|
|
|
|
DSN 定义了一个数据库的连接方式及信息,包含用户名、密码、数据库 IP、数据库端口、数据库字符集、数据库时区等信息。可以说**一个 DSN 就是一个数据源的描述**。但是 DSN 并没有明确的官方文档要求其格式,每个语言、每个平台都可以自己定义 DSN 格式,只要定义和解析能对得上就行。
|
|
|
|
|
|
|
|
|
|
在社区中,大家普遍会按照以下这种格式来进行定义:
|
|
|
|
|
|
|
|
|
|
```plain
|
|
|
|
|
scheme://username:password@host:port/dbname?param1=value1¶m2=value2&...
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
比如通过 Unix 的 socket 句柄连接本机 MySQL:
|
|
|
|
|
|
|
|
|
|
```plain
|
|
|
|
|
mysql://user@unix(/path/to/socket)/dbname
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
通过 TCP 连接远端 postgres:
|
|
|
|
|
|
|
|
|
|
```plain
|
|
|
|
|
pgsql://user:pass@tcp(localhost:5555)/dbname
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
DSN在gorm中的使用就如下图所示,我们使用这个dsn结合具体的驱动来生成Open函数的第一个参数,数据库连接器。
|
|
|
|
|
![](https://static001.geekbang.org/resource/image/64/e0/6484f9f91a60120d485a0ea2020fb7e0.jpg?wh=2185x1256)
|
|
|
|
|
|
|
|
|
|
在具体使用中,我们当然可以直接执行字符串拼接,来拼接出一个 DSN,但是我们更希望能**通过定义一个 Golang 的数据结构自动拼接出一个 DSN,或者是从一个 DSN 字符串反序列化生成这个数据结构**。
|
|
|
|
|
|
|
|
|
|
在 Golang 中有一个第三方库 github.com/go-sql-driver/mysql 就提供了这样的功能。这个库用来对 Go 中的 SQL 提供 MySQL 驱动,其中定义了一个 Config 结构,能映射到 DSN 字符串。Config 结构中一些比较重要的字段说明,我写在注释中了:
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
type Config struct {
|
|
|
|
|
User string // 用户名
|
|
|
|
|
Passwd string // 密码 (requires User)
|
|
|
|
|
Net string // 网络类型
|
|
|
|
|
Addr string // 地址 (requires Net)
|
|
|
|
|
DBName string // 数据库名
|
|
|
|
|
Params map[string]string // 其他连接参数
|
|
|
|
|
Collation string // 字符集
|
|
|
|
|
Loc *time.Location // 时区
|
|
|
|
|
MaxAllowedPacket int // 最大包大小
|
|
|
|
|
ServerPubKey string // 连接公钥名称
|
|
|
|
|
pubKey *rsa.PublicKey // 连接公钥 key
|
|
|
|
|
TLSConfig string // TLS 的配置名称
|
|
|
|
|
tls *tls.Config // TLS 的配置项
|
|
|
|
|
Timeout time.Duration // 连接超时
|
|
|
|
|
ReadTimeout time.Duration // 读超时
|
|
|
|
|
WriteTimeout time.Duration // 写超时
|
|
|
|
|
|
|
|
|
|
...
|
|
|
|
|
CheckConnLiveness bool // 在使用连接前确认连接可用
|
|
|
|
|
...
|
|
|
|
|
ParseTime bool // 是否解析时间格式
|
|
|
|
|
...
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
从 DSN 到这个 Config 结构,我们使用 github.com/go-sql-driver/mysql 的 [ParseDSN](https://github.com/go-sql-driver/mysql/blob/master/dsn.go) ,而从 Config 结构到 DSN 我们使用 [FormatDSN](https://github.com/go-sql-driver/mysql/blob/master/dsn.go) 方法:
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
// 解析 dsn
|
|
|
|
|
func ParseDSN(dsn string) (cfg *Config, err error)
|
|
|
|
|
|
|
|
|
|
// 生成 dsn
|
|
|
|
|
func (cfg *Config) FormatDSN() string
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
这两个方法都先记下,下节课会用到。
|
|
|
|
|
|
|
|
|
|
## Option
|
|
|
|
|
|
|
|
|
|
第一个初始化数据库的参数 dialector 以及之前必要的驱动引入相关参数DSN,就讲解到这里。我们回头继续看 Open 函数:
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
// 初始化数据库连接
|
|
|
|
|
func Open(dialector Dialector, opts ...Option) (db *DB, err error)
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
第二个参数 opts是 Option 的可变参数,而这个 Option 是一个实现了 Apply 和 AfterInitialize 的接口:
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
// Option 接口
|
|
|
|
|
type Option interface {
|
|
|
|
|
Apply(*Config) error
|
|
|
|
|
AfterInitialize(*DB) error
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
这种可变参数如何使用呢?我们看下 Open 的源码:
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
// Open 初始化 DB 的 Session
|
|
|
|
|
func Open(dialector Dialector, opts ...Option) (db *DB, err error) {
|
|
|
|
|
config := &Config{}
|
|
|
|
|
|
|
|
|
|
...
|
|
|
|
|
|
|
|
|
|
for _, opt := range opts {
|
|
|
|
|
if opt != nil {
|
|
|
|
|
// 先调用 Apply 初始化 Config
|
|
|
|
|
if err := opt.Apply(config); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
// Open 最后结束后调用 AfterInitialize
|
|
|
|
|
defer func(opt Option) {
|
|
|
|
|
if errr := opt.AfterInitialize(db); errr != nil {
|
|
|
|
|
err = errr
|
|
|
|
|
}
|
|
|
|
|
}(opt)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
可以看到,对每一个option,我们直接调用它的Apply方法来对数据库的配置config进行修改。Option 的这种编程方式常用在初始化一个比较复杂的结构里面。
|
|
|
|
|
|
|
|
|
|
比如这里在 Gorm 中,要初始化一个 Gorm 的构造配置 gorm.Config,而这个 Config 结构有非常多的配置项,我们希望在创建初始化的时候,能对这个配置进行调整。所以就可以在 Option 方法中再定义一个 Apply 方法,它的参数是 gorm.Config 指针:
|
|
|
|
|
|
|
|
|
|
```plain
|
|
|
|
|
func (c *Config) Apply(config *Config) error
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
这样,可以遍历所有的 Option,挨个调用它们的 Apply 方法对 Config 进行设置,最终我们获取的就是经过所有 Option 处理后的 Config。
|
|
|
|
|
|
|
|
|
|
这种 Option 的编程方法在 Golang 中十分常用,要好好掌握,下一节课,我们也会用这种方式为 hade 的 ORM 服务来注册参数。
|
|
|
|
|
|
|
|
|
|
Gorm 这里还有一个比较巧妙的设计,Config 结构本身也实现了 Option 接口。按照这个设计实现之后,你会发现,Config 本身也可以作为一个 Option 在 Open 的第二个参数中出现。
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
func (c *Config) Apply(config *Config) error {
|
|
|
|
|
if config != c {
|
|
|
|
|
*config = *c
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (c *Config) AfterInitialize(db *DB) error {
|
|
|
|
|
if db != nil {
|
|
|
|
|
for _, plugin := range c.Plugins {
|
|
|
|
|
if err := plugin.Initialize(db); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
讲到这里相信你能画出第二个参数的要点了,Gorm实现的时候使用的是gorm.Config,之所以它可以匹配Open函数定义的Option的参数的原因是Config结构本身也实现了Option接口。
|
|
|
|
|
![](https://static001.geekbang.org/resource/image/de/76/deda009d08f4bf80e384f85dc0d9ce76.jpg?wh=2185x1256)
|
|
|
|
|
|
|
|
|
|
通过上图我们就理解了Open函数在使用的时候,第一个参数和第二个参数是如何对应函数定义两个参数的了。
|
|
|
|
|
|
|
|
|
|
所以 Gorm 官方连接[MySQL的示例](https://gorm.io/zh_CN/docs/connecting_to_the_database.html)就很好理解了,看注释:
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
import (
|
|
|
|
|
"gorm.io/driver/mysql"
|
|
|
|
|
"gorm.io/gorm"
|
|
|
|
|
)
|
|
|
|
|
func main() {
|
|
|
|
|
// 参考 https://github.com/go-sql-driver/mysql#dsn-data-source-name 获取详情
|
|
|
|
|
dsn := "user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
|
|
|
|
|
// 第一个参数是 dialector
|
|
|
|
|
// 第二个参数是 option,但是由于 gorm.Config 实现了 option,所以可以这么使用
|
|
|
|
|
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## gorm.DB
|
|
|
|
|
|
|
|
|
|
两个传入参数讲完了,我们继续看Open 的返回结构,除了常规的 error 外,还有一个 gorm.DB 的结构指针,定义如下:
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
type DB struct {
|
|
|
|
|
*Config
|
|
|
|
|
Error error
|
|
|
|
|
RowsAffected int64
|
|
|
|
|
Statement *Statement
|
|
|
|
|
// Has unexported fields.
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
它具有丰富的操作数据库的方法,比如增加数据的 Create 方法、更新数据的 Update 方法。
|
|
|
|
|
|
|
|
|
|
我们研究下 gorm.DB 的结构,它嵌套了一层 gorm.Config 结构,里面有几个关键字段:
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
// Config GORM config
|
|
|
|
|
type Config struct {
|
|
|
|
|
...
|
|
|
|
|
// gorm 的日志输出
|
|
|
|
|
Logger logger.Interface
|
|
|
|
|
...
|
|
|
|
|
|
|
|
|
|
// db 的具体连接
|
|
|
|
|
ConnPool ConnPool
|
|
|
|
|
// db 驱动器
|
|
|
|
|
Dialector
|
|
|
|
|
...
|
|
|
|
|
|
|
|
|
|
callbacks *callbacks // 回调方法
|
|
|
|
|
...
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
其中的 Logger 、 ConnPool 和 Callback字段,值得详细研究一下。
|
|
|
|
|
|
|
|
|
|
### Logger
|
|
|
|
|
|
|
|
|
|
我们从 Open 看到了,一个 gorm.DB 结构就代表一个数据库连接,而这个数据库连接的所有日志操作输出在哪里呢?就是通过这个 Logger 字段配置的。
|
|
|
|
|
|
|
|
|
|
Logger 字段是一个接口,表示如果有一个实现了 logger.Interface 接口的日志输出类,我就能让这个 DB 的所有数据库操作的日志,都输出到这个类中。
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
// Interface logger interface
|
|
|
|
|
type Interface interface {
|
|
|
|
|
LogMode(LogLevel) Interface
|
|
|
|
|
Info(context.Context, string, ...interface{})
|
|
|
|
|
Warn(context.Context, string, ...interface{})
|
|
|
|
|
Error(context.Context, string, ...interface{})
|
|
|
|
|
Trace(ctx context.Context, begin time.Time, fc func() (sql string, rowsAffected int64), err error)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Gorm 使用 Logger 接口的方法,和我们 hade 框架定义 Logger 服务的方法如出一辙,它**不定义具体的实现类,而是定义了具体的接口**。所以下一节课,我们将 Gorm 融合进入 hade 框架的时候,要做的事情就是封装一个实现了 Gorm 的 logger.Interface 接口的实现类,而这个实现类的具体实现方法,使用 hade 框架的日志服务类来实现。
|
|
|
|
|
|
|
|
|
|
### ConnPool
|
|
|
|
|
|
|
|
|
|
ConnPool 也定义了一个接口,它代表数据库的真实连接所在的连接池。这个接口的定义,我认为是Gorm 中最精妙的一个地方了:
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
// ConnPool db conns pool interface
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
这个接口定义了四个方法,但它们并不是随便定义的,而是根据 Golang 标准库的 database/sql 的 Conn 结构来定义的,这是什么意思呢?
|
|
|
|
|
|
|
|
|
|
首先我们要知道,Golang 的标准库 database/sql 其实定义了一套数据库连接规范。官方的基本思想就是,数据库的种类非常多,我不可能对每一个数据库都实现一套定制化的类库,所以我定义一套基本数据结构和方法,并且提供每个数据库需要实现的驱动接口。**使用者只需要实现驱动接口,就能使用这套基本数据结构和方法了**。
|
|
|
|
|
|
|
|
|
|
是不是和前面说的 Gorm 的驱动逻辑一样?是的。Golang 中所有的 ORM 库,底层都是基于标准库的 database/sql 来实现数据库的连接和基本操作。只是在具体操作上,会封装一层逻辑,当使用不同驱动接口的时候,实现不一样的接口操作。
|
|
|
|
|
|
|
|
|
|
这里的 ConnPool 就是 Gorm 对 database/sql 的数据结构的封装。换句话说,开头的 Gorm 使用例子,在底层 database/sql 的简要实现大致如下:
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"database/sql"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func main() {
|
|
|
|
|
|
|
|
|
|
dsn := "xxxx"
|
|
|
|
|
...
|
|
|
|
|
db, err = sql.Open("mysql", *dsn)
|
|
|
|
|
...
|
|
|
|
|
|
|
|
|
|
result, err := db.ExecContext(ctx, "INSERT INTO user (name) values ('jianfengye')")
|
|
|
|
|
...
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
这里 sql.Open 创建的 sql.DB 结构,就包含 ConnPool 中定义的四个接口:PrepareContext、ExecContext、QueryContext、QueryRowContext。也就是说:**database/sql 的 sql.DB 结构实现了 Gorm 库的 ConnPool 接口**。
|
|
|
|
|
|
|
|
|
|
而实际上,database/sql 里面的 sql.DB 结构就是一个连接池结构,我们可以通过以下四个方法设置连接池的不同属性:
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
// 设置连接的最大空闲时长
|
|
|
|
|
func (db *DB) SetConnMaxIdleTime(d time.Duration)
|
|
|
|
|
// 设置连接的最大生命时长
|
|
|
|
|
func (db *DB) SetConnMaxLifetime(d time.Duration)
|
|
|
|
|
// 设置最大空闲连接数
|
|
|
|
|
func (db *DB) SetMaxIdleConns(n int)
|
|
|
|
|
// 设置最大打开连接数
|
|
|
|
|
func (db *DB) SetMaxOpenConns(n int)
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
所以 gorm.DB 里面的 ConnPool 实际上存放的就是 database/sql 的 sql.DB 结构。
|
|
|
|
|
|
|
|
|
|
### callbacks
|
|
|
|
|
|
|
|
|
|
最后看 gorm.DB 里面的 callbacks 字段,它存放的是所有具体函数的调用方法。callback 指针指向的数据结构也是叫做同名的 callbacks:
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
// callbacks gorm callbacks manager
|
|
|
|
|
type callbacks struct {
|
|
|
|
|
processors map[string]*processor
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
它里面使用的 map 包含多个 processor。一个 processor 就是一种操作的处理器。processer 的结构定义为:
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
type processor struct {
|
|
|
|
|
db *DB // 对应的 gorm.DB
|
|
|
|
|
Clauses []string // 处理器对应的 sql 片段
|
|
|
|
|
fns []func(*DB) // 这个处理器对应的处理函数
|
|
|
|
|
callbacks []*callback // 这个处理器对应的回调函数,生成 fns
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
开头的那个例子,我们调用了 gorm.DB 的 Create 方法,它会去 gorm.DB 的 callbacks 中的 processors 里,寻找 key 为“create”的处理器 processor。然后逐个调用处理器中设置好的 fns。下面分析源码的时候也会看到具体的实现逻辑。
|
|
|
|
|
![](https://static001.geekbang.org/resource/image/59/9c/591e738e8aa8af160fac58fd44d8639c.jpg?wh=2185x1256)
|
|
|
|
|
|
|
|
|
|
## 源码
|
|
|
|
|
|
|
|
|
|
现在理解了 Gorm 在创建连接过程中涉及的几个关键对象,我们就再从源码开始梳理一下 Gorm 的核心逻辑,理解下 Gorm 是怎么使用 Open 创建数据库连接、怎么使用创建的数据库连接的 Create 方法来创建一条数据的。再把开头官网的例子拿出来。
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"gorm.io/gorm"
|
|
|
|
|
"gorm.io/driver/mysql"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type Product struct {
|
|
|
|
|
gorm.Model
|
|
|
|
|
Code string
|
|
|
|
|
Price uint
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func main() {
|
|
|
|
|
dsn := "xxxxxxx"
|
|
|
|
|
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
|
|
|
|
|
if err != nil {
|
|
|
|
|
panic("failed to connect database")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
...
|
|
|
|
|
|
|
|
|
|
// Create
|
|
|
|
|
db.Create(&Product{Code: "D42", Price: 100})
|
|
|
|
|
|
|
|
|
|
...
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
用第一节课教的思维导图的方式来分析这个 Gorm 的主流程,主要就是 gorm.Open 和 db.Create 两个方法。
|
|
|
|
|
|
|
|
|
|
### gorm.Open
|
|
|
|
|
|
|
|
|
|
首先是 gorm.Open:
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
我们将函数源码分为四个大步:
|
|
|
|
|
![](https://static001.geekbang.org/resource/image/a9/cf/a9afa90a5dba63aa3f2c341c47517ecf.png?wh=1356x1364)
|
|
|
|
|
|
|
|
|
|
第一大步,初始化 gorm.Config 结构。通过使用参数中的 Option 可变参数的 Apply 接口,对最终的配置结构 gorm.Config进行相应的修改,其中包括修改输出的 Logger 结构。
|
|
|
|
|
|
|
|
|
|
第二步,初始化 gorm.DB 结构:
|
|
|
|
|
|
|
|
|
|
```plain
|
|
|
|
|
db = &DB{Config: config, clone: 1}
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
第三步,初始化 gorm.DB 的 callbacks。
|
|
|
|
|
![](https://static001.geekbang.org/resource/image/f4/4a/f482ff99aa62eb3cd3991867d967f74a.png?wh=1920x814)
|
|
|
|
|
|
|
|
|
|
这里我们只拆解了这个例子的 create 函数相关的 callback。核心的关键函数在 Gorm 库 callback.go 的 RegisterDefaultCallbacks 方法。比如下列的代码,就是创建 create 相关的执行方法 fns:
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
createCallback := db.Callback().Create()
|
|
|
|
|
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)
|
|
|
|
|
if len(config.CreateClauses) == 0 {
|
|
|
|
|
config.CreateClauses = createClauses
|
|
|
|
|
}
|
|
|
|
|
createCallback.Clauses = config.CreateClauses
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
我们可以看到,Gorm 在一个 create 方法,定义了 7 个执行方法 fns,分别是:BeginTransaction、BeforeCreate、SaveBeforeAssociations、Create、SaveAfterAssociations、AfterCreate、CommitOrRollbackTransaction。这七个执行方法就是按照顺序,从上到下每个 Create 函数都会执行的方法。
|
|
|
|
|
|
|
|
|
|
其中关注一下 Create 方法,它又分为五个步骤:
|
|
|
|
|
![](https://static001.geekbang.org/resource/image/74/97/74168a832a7ea27fc84548cc980d1297.png?wh=1920x719)
|
|
|
|
|
|
|
|
|
|
我们看到了熟悉的 ExecContent 函数,这个就对应上了 Golang 标准库的 database/sql 中 sql.DB 的 ExecContext 方法。原来它藏在这里!
|
|
|
|
|
|
|
|
|
|
那前面说的 database/sql 的 sql.DB 的 Open 方法,又放在哪里呢?就在 gorm.Open 的第四大步中:
|
|
|
|
|
|
|
|
|
|
```plain
|
|
|
|
|
db.ConnPool, err = sql.Open(dialector.DriverName, dialector.DSN)
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
将 database/sql 中生成的 sql.DB 结构,设置在了 gorm.DB 的 ConnPool 上。
|
|
|
|
|
|
|
|
|
|
### db.Create
|
|
|
|
|
|
|
|
|
|
下面再来看 gorm.DB 的 Create 方法。它的任务就很简单了:触发启动 processor 中的 fns 方法。具体最核心的代码就在 Gorm 的 callback.go 中的 Execute 函数里。
|
|
|
|
|
![](https://static001.geekbang.org/resource/image/d4/7a/d41214f972f885c9c18da6aeb7e2e77a.png?wh=1920x566)
|
|
|
|
|
|
|
|
|
|
可以看到,在 Execute 函数中,最核心的是遍历 fns,调用 fn(db) 方法,其中就有我们前面定义的 Create 方法了,也就是执行了 database/sql 的 db.ExecContext 方法。
|
|
|
|
|
|
|
|
|
|
这里我们就根据思维导图找到了 Gorm 封装的 database/sql 的两个关键步骤:
|
|
|
|
|
|
|
|
|
|
* sql.Open
|
|
|
|
|
* db.ExecContext
|
|
|
|
|
|
|
|
|
|
理解了这一点,就基本理解了 Gorm 最核心的实现原理了。
|
|
|
|
|
![](https://static001.geekbang.org/resource/image/ee/88/eec03fdb85d622202f8e7c8ac7e70488.jpg?wh=4861x3258)
|
|
|
|
|
|
|
|
|
|
当然 Gorm 中还有一个部分,是将我们定义的 Model解析成为 SQL 语句,这里又是 Gorm 定义的一套非常庞大的数据结构支撑的了,其中包括 Statement、Schema、Field、Relationship 等和数据表操作相关的数据结构。
|
|
|
|
|
|
|
|
|
|
这需要用另外一个篇幅来描述了。不过这块 Model 解析,对我们下一章 hade 框架融合 Gorm 的影响并不大。有兴趣的同学可以追着上述 Create 方法中的 stmt.Parse 方法进一步分析。
|
|
|
|
|
|
|
|
|
|
今天我们还没有涉及代码修改,思维导图保存在 GitHub 上的 [geekbang/25](https://github.com/gohade/coredemo/tree/geekbang/25) 分支中根目录的 mysql.xmind 中了。
|
|
|
|
|
|
|
|
|
|
## 小结
|
|
|
|
|
|
|
|
|
|
我们分析了 Gorm 的具体数据结构和创建连接的核心源码流程。想要检验自己是否理解这节课也很简单,你可以对照开头为 user 表插入一行的代码,看看能不能清晰分析出它的底层是如何封装标准库的 database/sql 来实现的。
|
|
|
|
|
|
|
|
|
|
我们在阅读 Gorm 源码的同时,也是在学习它的优秀编码方式,比如今天讲到的 Option 方式、定义驱动、ConnPool 定义实现标准库方法的接口。这些都是 Gorm 设计精妙的地方。
|
|
|
|
|
|
|
|
|
|
当然 Gorm 的代码远不是一篇文章能说透的。其中包含的 Model 解析,以及更多的具体细节实现,都得靠你在后续使用过程中多看[官网](https://gorm.io/zh_CN/)、多思考、多解析,才能完全吃透这个库。
|
|
|
|
|
|
|
|
|
|
### 思考题
|
|
|
|
|
|
|
|
|
|
GORM 有一个功能我非常喜欢,DryRun 空跑,这个设置是在 gorm.DB 结构中的。如果我们设置了 gorm.DB 的 DryRun,能让我在这个 DB 中的所有 SQL 操作并不真正执行,这个功能在调试的时候是非常有用的。你能再顺着思维导图,分析出 DryRun 是怎么做到这一点的么?
|
|
|
|
|
|
|
|
|
|
欢迎在留言区分享你的思考。感谢你的收听,如果你觉得今天的内容对你有所帮助,也欢迎分享给身边的朋友,邀请他一起学习。我们下节课见~
|
|
|
|
|
|