Gorm实战 | 青训营笔记

329 阅读5分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的的第2篇笔记

1 理解database/sql

1.1 基本用法

  1. import实现,使用driver + dsn初始化DB连接
  2. 执行一条sql,通过游标rows取出返回的数据,处理完毕后释放连接,避免资源的泄漏
  3. 进行相关的数据、错误处理
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包内部实现连接池的管理

支持不同的数据库只需要去实现一个连接接口,操作接口,包操作接口暴露给应用程序,从而实现不同的数据库的支持

连接池使用的是一个池化技术,当遇到请求量大的情况,池化技术可以明显优化性能,把比较昂贵、费时的资源放入特定的池子中,作相关的配置,如维持最小连接数,维持阻塞队列来方便管理池子,通过连接池来大幅度提高服务的性能,减少创建、销毁、垃圾回收的损耗

57d24007d4f9c7d70c6f57d3c1446c6.png

连接池配置方法

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设计原理

ead54fc1d1a49713b4e8564f70f57dc.png

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实用功能

  1. 数据序列化与SQL表达式
  2. 批量数据操作
  3. 代码复用、分库分表、Sharding
  4. 混沌工程、压力测试
  5. Logger/Trace
  6. Migrator
  7. Gen代码生成/Raw SQL
  8. 安全