这是我参与「第三届青训营 -后端场」笔记创作活动的的第2篇笔记
1 理解database/sql
1.1 基本用法
- import实现,使用driver + dsn初始化DB连接
- 执行一条sql,通过游标rows取出返回的数据,处理完毕后释放连接,避免资源的泄漏
- 进行相关的数据、错误处理
package main
import (
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
)
type User struct {
ID string
Name string
Pwd string
Salt string
FollowCount string
FlollowerCount string
}
func main() {
dns := "root:123456@tcp(127.0.0.1:3306)/douyin?charset=utf8&parseTime=True&loc=Local"
db, err := sql.Open("mysql", dns)
rows, err := db.Query("select id, name, pwd, salt, follow_count, follower_count from user where id = ?", 1)
if err != nil {
panic(err)
}
defer rows.Close()
var users []User
for rows.Next() {
var user User
rows.Scan(&user.ID, &user.Name, &user.Pwd, &user.Salt, &user.FollowCount, &user.FlollowerCount)
users = append(users, user)
}
for _, u := range users {
fmt.Println(u.ID)
fmt.Println(u.Name)
}
if rows.Err() != nil {
panic(rows.Err())
}
}
1.2 设计原理
database/sql对应用程序(上层应用)提供标准api操作接口,对数据库提供简单的驱动接口(连接接口与操作接口)。在database/sql包内部实现连接池的管理
支持不同的数据库只需要去实现一个连接接口,操作接口,包操作接口暴露给应用程序,从而实现不同的数据库的支持
连接池使用的是一个池化技术,当遇到请求量大的情况,池化技术可以明显优化性能,把比较昂贵、费时的资源放入特定的池子中,作相关的配置,如维持最小连接数,维持阻塞队列来方便管理池子,通过连接池来大幅度提高服务的性能,减少创建、销毁、垃圾回收的损耗
连接池配置方法
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
操作过程的伪代码实现
for i := 0; i < maxBadConnRetries; i++ {
//从连接池获取连接或通过driver新建连接
dc,err := db.conn(ctx, strategy)
// 从空闲连接 -> reuse -> max life time
// 新建连接 -> max open
//将连接放回连接池
defer dc.db.putConn(dc,err,true)
//validateConnection 有无错误
//max life time, max idle conns 检查
//连接实现driver.Queryer,driver.Execer等interface
if err == nil {
err = dc.ci.Query(sql,args...)
}
isBadConn = errors.Is(err,driver,ErrBadConn)
if !isBadConn {
break
}
}
Driver连接接口
//Driver接口
type Driver interface {
//Open returns a new connection to the database
Open(name string) (Conn,error)
}
//注册全局driver
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
}
}
业务代码
import _"github.com/go-sql-driver/mysql"
func main() {
db,err := sql.Open("mysql", "gorm:gorm@tcp(localhost:9910)/gorm?charset=utf8&parseTime=True&loc=Local")
}
//github.com/go-sql-driver/mysql/driver.go
//注册Driver
func init() {
sql.Register("mysql",&MySQLDriver{})
}
DB连接的几种类型
- 直接连接/Conn
- 预编译/Stmt
- 事务/Tx
处理返回数据的几种方式
- Exec/ExecContext -> Result
- Query/QueryContext -> Rows(Columns)
- QueryRow/QueryRowContext -> Row(Rows简化)
2 GORM基础使用
设计原则:API精简、测试优先、最小惊讶、灵活扩展、无依赖
功能完善:
- 关联:一对一、一对多、单表自关联、多态;Preload、Joins预加载、级联删除;关联模式;自定义关联表
- 事务:事务代码块、嵌套事务、Save Point
- 多数据库、读写分离、命名参数、Map、子查询、分组条件、代码共享、SQL表达式(查询、创建、更新)、自动选字段、查询优化器
- 字段权限、软删除、批量数据处理、Prepared Stmt、自定义类型、命名策略、虚拟字段、自动track时间、SQL Builder、Logger
- 代码生成、复合主键、Constraint、Prometheus、Auto Migration、跨数据库兼容
- 多模式灵活扩展
- Developer Friendly
GORM的基础用法参考官方文档
3 GORM设计原理
3.1 SQL是怎么生成的
GORM STATEMENT由多个Chain Method组成,如Where()方法、Limit方法、Order()方法和最终的Find()方法(用于决定类型&执行),最终Find方法将支持子句反到Build方法进行翻译,最后交给ConnectionPool去执行。GORM参考了SQL的生成进行仿生设计
Where方法
// 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
}
Find方法
// Find find records that match given conditions
func (db *DB) Find(dest interface{}, conds ...interface{}) (tx *DB) {
tx = db.getInstance()
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.Dest = dest
return tx.callbacks.Query().Execute(tx)
}
为什么这样设计SQL的生成
- 自定义Clause Builder
- 方便扩展Clause
- 自由选择Clauses
3.2 插件是怎么工作的
首先经过Finisher Method方法决定Statement类型,然后执行callbacks,最后生成SQL并执行,其中Callbacks分为Create,Query,Update,Delete,Row,Raw。
多租户下将TenantId加入到过滤条件中,保证操作的数据都绑定租户的关系
func setTenantScope(db *gorm.DB) {
if tenantID, err := getTenantId(db.Statement.Context);err!= nil {
db.Where("tenant_id = ?",tentantID)
}else {
db.AddError(err)
}
}
多数据库、读写分离,可以指定Resolver和write模式,来读取数据
3.3 ConnPool是什么
DBConn连接会替换为ConnPool来实现,如以下例子,PrepareStmt实现了ConnectionPool接口,当收到一个执行操作的时候,会查找缓存的预编译SQL,未找到则将收到的SQL和Vars进行预编译,然后使用缓存的预编译SQL执行
//全局模式,所有DB操作都会预编译并缓存()
db, err := gorm.Open(sqlite.Open("gorm.db"),&gorm.Config{prepareStmt:true})
//会话模式,后续会话的操作都会预编译并缓存
tx := db.Session(&Session{PrepareStmt:true})
tx.Find(&users)
tx.Model(&user).Update("Age",18)
//全局缓存的语句可被会话使用
tx.First(&user,2)
stmtManager, ok := tx.ConnPool.(*PreparedStmtDB)
//关闭当前会话的预编译语句
stmtManger.Close()
课程中提到了如何通过一行代码的配置提高SQL执行的性能
可以通过设置
interpolateParams = false
在执行有参数的SQL时,会经过三个步骤
- 执行前预编译SQL
- 调用预编译的SQL
- 关闭预编译的SQL
由于执行完SQL后会关闭预编译,预编译主要解决SQL注入的问题,目前大多数服务用的UTF-8格式,关闭预编译可以减少两次RTT时间,如果数据库在国外,那么可以节省几百毫秒的时间,性能大大提升
3.4 Dialector是什么
Dialector支持
1、定制SQL生成
2、定制GORM插件
3、定制ConnPool
4、定制企业特性逻辑
4 GORM实用功能
- 数据序列化与SQL表达式
- 批量数据操作
- 代码复用、分库分表、Sharding
- 混沌工程、压力测试
- Logger/Trace
- Migrator
- Gen代码生成/Raw SQL
- 安全