这是我参与「第三届青训营 -后端场」笔记创作活动的的第5篇笔记
概述
本节课一方面讲述了Go语言的Database/SQL的实现,一方面讲述了解GORM的实现原理,内容主要包括了理解database/sql、GORM 使用简介、GORM 设计原理、GORM 最佳实践
Database/Sql
Database/SQL 的基本用法
database/sql包定义了数据库访问的通用interface,是对各种数据库操作的抽象定义(比如数据库连接,数据库增删改方法等)。
在使用database/sql时,一般需要与对应的数据库类型驱动同步导入(因为database/sql包只是定义了通用的抽象的接口,并没有一个具体的操作/业务实现),如使用mysql数据库时,需要同时导入database/sql和github.com/go-sql-driver/mysql 两个包,即:
import (
"database/sql"
_ "github.com/go-sql-driver/mysql" // 匿名导入即可,目的是该包中的init()函数实现驱动的注册
)
Quick Start
package main
import (
"database/sql"
_ "github.com/go-sql-driver/mysql"
"log"
)
func main() {
db, err := sql.Open("mysql", "user:password@tcp(127.0.0.1:3306)/hello")
if err != nil {
log.Fatal(err)
}
defer func() {
err = db.Close()
// 处理错误
}()
rows, err := db.Query("select `name` from users where id = ?", 1)
if err != nil {
log.Fatal(err)
}
defer func() {
err = rows.Close()
// 处理错误
}()
name := ""
for rows.Next() {
err = rows.Scan(&name)
if err != nil {
log.Fatal(err)
}
}
if rows.Err() != nil {
// 处理错误
}
}
代码解析
设计原理
database/sql采极简接口的设计原则,为应用程序提供了数据库操作的标准接口,对下层的数据库驱动暴露数据库连接等管理接口(不同的数据库驱动底层只要实现对应的接口便可供应用层调用,从而实现多种类型的数据库支持),并在database/sql中基于池化技术维护数据库连接池
核心结构 sql.DB
连接池管理
连接池配置
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)
连接池状态
func (db *DB) Stats() DBStats
操作过程
操作过程伪实现
Driver连接处理
连接interface
type Driver interface {
// Open returns a new connection to the database.
// The name is a string in a driver-specific format.
//
// Open may return a cached connection (one previously
// closed), but doing so is unnecessary; the sql package
// maintains a pool of idle connections for efficient re-use.
//
// The returned connection is only used by one goroutine at a
// time.
Open(name string) (Conn, error)
}
驱动注册Driver方法
// Register makes a database driver available by the provided name.
// If Register is called twice with the same name or if driver is nil,
// it panics.
func Register(name string, driver driver.Driver) {
driversMu.Lock()
defer driversMu.Unlock()
if driver == nil {
panic("sql: Register driver is nil")
}
if _, dup := drivers[name]; dup {
panic("sql: Register called twice for driver " + name)
}
drivers[name] = driver
}
上述mysql驱动包中便是基于该方法进行驱动的注册
// github.com/go-sql-driver/mysql
// 注册处理
func init() {
sql.Register("mysql", &MySQLDriver{})
}
业务使用
package main
import (
"database/sql"
_ "github.com/go-sql-driver/mysql"
)
func main() {
db, err := sql.Open("mysql", "user:password@tcp(127.0.0.1:3306)/hello")
if err != nil {
log.Fatal(err)
}
...
}
在业务使用上可能存在一些问题,一方面可能忘记相应数据库driver包的导入,另一方面直接传递数据库连接的DSN(即user:password@tcp(127.0.0.1:3306)/hello这种类型的字符串)不大方便,可能存在字段含义不清晰,转义错误等问题,同时也可能受import顺序影响等
Driver连接的进一步改进
新的连接interface
type Connector interface {
// Connect returns a connection to the database.
// Connect may return a cached connection (one previously
// closed), but doing so is unnecessary; the sql package
// maintains a pool of idle connections for efficient re-use.
//
// The provided context.Context is for dialing purposes only
// (see net.DialContext) and should not be stored or used for
// other purposes. A default timeout should still be used
// when dialing as a connection pool may call Connect
// asynchronously to any query.
//
// The returned connection is only used by one goroutine at a
// time.
Connect(context.Context) (Conn, error)
// Driver returns the underlying Driver of the Connector,
// mainly to maintain compatibility with the Driver method
// on sql.DB.
Driver() Driver
}
业务使用
操作接口
DB连接的几种类型
- 直接连接/Conn
- 预编译/Stmt
- 事务/Tx 处理返回数据的几种方式
- Exec/ExecContext->Result
- Query/QueryContext->Rows(Columns)
- QueryRow/QueryRowContext->Row(Rows简化)
Rows interface(详见源码实现)
GORM使用简介
对象关系映射(Object Relational Mapping,简称ORM)是通过使用描述对象和数据库之间映射的元数据,将面向对象语言程序中的对象自动持久化到关系数据库中。对象关系映射(Object Relational Mapping,简称ORM)是通过使用描述对象和数据库之间映射的元数据,将面向对象语言程序中的对象自动持久化到关系数据库中。
后续章节部分建议直接查阅官方文档和源码,在此记录目的在于后续回顾时有所记忆
GORM的使用指南详见GORM 指南,不做赘述
连接demo
import (
"gorm.io/gorm"
"gorm.io/driver/mysql"
)
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"
// 数据库连接
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
var users []User
err = db.Select("id", "name").Find(&users, 1).Error
}
查询demo
// 获取第一条记录(主键升序)
db.First(&user)
// SELECT * FROM users ORDER BY id LIMIT 1;
// 获取一条记录,没有指定排序字段
db.Take(&user)\
// SELECT * FROM users LIMIT 1;
// 获取最后一条记录(主键降序)
db.Last(&user)
// SELECT * FROM users ORDER BY id DESC LIMIT 1;
result := db.First(&user)
result.RowsAffected // 返回找到的记录数
result.Error // returns error
// 检查 ErrRecordNotFound 错误
errors.Is(result.Error, gorm.ErrRecordNotFound)
关联操作
涉及关联操作详见文档:关联操作 | GORM
GORM设计原理
SQL是怎么生成的
每一条SQL STATEMENT都是由多条子句构成的,子句又可通过不同的表达式组成
在生成SQL时,GORM会构造GORM STATEMENT,通过Chain Method(添加子句的方法) 和 Finisher Method(决定statement的最终类型及执行的方法,比如这里的Find决定了这是一条Select Statement) 来构造,GORM STATEMENT最终执行生成最终的SQL
Where子句
Limit子句
Find方法(GORM Finisher方法执行GORM Statement)
插件是怎么工作的
基于插件机制,实现多租户,多数据库,读写分离,加解密,混沌工程等需求,灵活定制,自由扩展
文档详见:编写插件
主要是基于回调处理来运作插件
自定义插件的方法
多租户案例
通过setTenantScope插件来自动添加id过滤条件,通过setTenantID插件来自动生成用户id
多数据库,读写分离案例
ConnPool是什么
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
}
通过ConnPool可以基于具体的SQL类型来使用不同的DB进行读写等操作
实现预编译的缓存,在业务中如果有重复的SQL执行可以考虑开启预编译缓存
Dialector是什么
GORM 官方支持 sqlite、mysql、postgres、sqlserver。
有些数据库可能兼容 mysql、postgres 的方言,在这种情况下,你可以直接使用这些数据库的方言。
对于其它不兼容的情况,您可以自行编写一个新驱动,这需要实现 方言接口
type Dialector interface {
Name() string
Initialize(*DB) error
Migrator(db *DB) Migrator
DataTypeOf(*schema.Field) string
DefaultValueOf(*schema.Field) clause.Expression
BindVarTo(writer clause.Writer, stmt *Statement, v interface{})
QuoteTo(clause.Writer, string)
Explain(sql string, vars ...interface{}) string
}
GORM最佳实践,可以作为实践参考
- SQL表达式更新创建
- SQL表达式查询
- 数据序列化
- 批量创建,查询
- 批量更新
- 批量数据加速操作
- 代码复用
- 分库分表
- Sharding
10. 混沌工程
- 压测
- Logger/Trace
- 数据库迁移管理
- 数据库迁移
- Raw SQL
- Gen
- 安全问题
最后
这节内容更多的涉及GORM的设计思路与原理,在不了解GORM的情况下有点难吃透