设计模式之 Database/SQL 与 GORM 实践 | 青训营笔记

488 阅读6分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的的第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 {
      // 处理错误
   }
}

代码解析

image.png

设计原理

database/sql采极简接口的设计原则,为应用程序提供了数据库操作的标准接口,对下层的数据库驱动暴露数据库连接等管理接口(不同的数据库驱动底层只要实现对应的接口便可供应用层调用,从而实现多种类型的数据库支持),并在database/sql中基于池化技术维护数据库连接池

image.png

核心结构 sql.DB

image.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

操作过程

操作过程伪实现

image.png

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
}
业务使用

image.png

操作接口

DB连接的几种类型

  • 直接连接/Conn
  • 预编译/Stmt
  • 事务/Tx 处理返回数据的几种方式
  • Exec/ExecContext->Result
  • Query/QueryContext->Rows(Columns)
  • QueryRow/QueryRowContext->Row(Rows简化)

Rows interface(详见源码实现)

image.png

image.png

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

image.png

SQL是怎么生成的

每一条SQL STATEMENT都是由多条子句构成的,子句又可通过不同的表达式组成 image.png

在生成SQL时,GORM会构造GORM STATEMENT,通过Chain Method(添加子句的方法)Finisher Method(决定statement的最终类型及执行的方法,比如这里的Find决定了这是一条Select Statement) 来构造,GORM STATEMENT最终执行生成最终的SQL

image.png

Where子句

image.png

Limit子句

image.png

Find方法(GORM Finisher方法执行GORM Statement)

image.png

插件是怎么工作的

基于插件机制,实现多租户,多数据库,读写分离,加解密,混沌工程等需求,灵活定制,自由扩展

文档详见:编写插件

image.png

主要是基于回调处理来运作插件

image.png

自定义插件的方法

image.png

多租户案例

通过setTenantScope插件来自动添加id过滤条件,通过setTenantID插件来自动生成用户id

image.png

多数据库,读写分离案例

image.png

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
}

image.png

通过ConnPool可以基于具体的SQL类型来使用不同的DB进行读写等操作

image.png

实现预编译的缓存,在业务中如果有重复的SQL执行可以考虑开启预编译缓存

image.png

Dialector是什么

GORM 官方支持 sqlitemysqlpostgressqlserver。 有些数据库可能兼容 mysqlpostgres 的方言,在这种情况下,你可以直接使用这些数据库的方言。 对于其它不兼容的情况,您可以自行编写一个新驱动,这需要实现 方言接口

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最佳实践,可以作为实践参考

  1. SQL表达式更新创建

image.png

  1. SQL表达式查询

image.png

  1. 数据序列化

image.png

  1. 批量创建,查询

image.png

  1. 批量更新

image.png

  1. 批量数据加速操作

image.png

  1. 代码复用

image.png

  1. 分库分表

image.png

  1. Sharding

image.png 10. 混沌工程

image.png

  1. 压测

image.png

  1. Logger/Trace

image.png

  1. 数据库迁移管理

image.png

  1. 数据库迁移

image.png

  1. Raw SQL

image.png

  1. Gen

image.png

  1. 安全问题

image.png

最后

这节内容更多的涉及GORM的设计思路与原理,在不了解GORM的情况下有点难吃透