database/sql 包与 GORM 实践 | 青训营笔记

141 阅读5分钟

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

基础知识

数据库

  • 数据库(Database, DB)是将大量数据保存起来,通过计算机加工而成的可以进行高效访问的数据集合。
  • 用来管理数据库的计算机系统称为数据库管理系统(Database Management System,DBMS)。
  • DBMS的种类
    • 层次数据库(Hierarchical Database, HDB):最古老的数据库之一,它把数据通过层次结构(树形结构)的方式表现出来。
    • 关系数据库(Relational Database, RDB):关系数据库是现在应用最广泛的数据库。
    • 面向对象数据库(Object Oriented Database, OODB):把数据以及对数据的操作集合起来以对象为单位进行管理。
    • XML数据库(XML Database, XMLDB):XML 数据库可以对 XML 形式的大量数据进行高速处理。
    • 键值存储系统(Key-Value Store, KVS):这是一种单纯用来保存查询所使用的主键(Key)和值(Value)的组合的数据库。

DSN(data source name)

  • 一个使用相关数据结构来描述与数据源连接的字符串。
  • DSN 中可能包含但不限于:
    • 数据源的名字
    • 数据源的位置
    • 可以访问数据源的数据库驱动的名字
    • 用于访问数据的用户ID
    • 用于访问数据的用户密码

ORM

ORM(Object-relational mapping),中文翻译为对象关系映射,是一种为了解决面向对象关系数据库存在的互不匹配的现象的技术。简单的说,ORM是通过使用描述对象和数据库之间映射的元数据,将程序中的对象自动持久化到关系数据库中。

理解 database/sql 包

基本用法

  • database/sql 只实现了统一的接口,需要不同的数据库实现具体的 driver。
  • Go-MySQL-Driver 是 database/sql 包的一个 MySQL-Driver。
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")
    
    rows, err := db.Query("select id, name from users where id = ?", 1)
    if err != nil {
        // xxx
    }
    defer rows.Close()
    
    var users []User
    for rows.Next() {
        var user User
        err := rows.Scan(&user.ID, &user.Name)
        if err != nil {
            // ...
        }
        users = append(users, user)
    }
    if row.Err() != nil {
        // ...
    }
}

设计原理

[应用程序]--(操作接口)--[database/sql]--(连接接口/操作接口)--[数据库]
  • 使用连接池(池化技术)
  • 连接接口1(注册 driver):
    • 提供 Driver 接口(database/sql/driver/driver.go)
    type Driver interface {
        Open(name string) (Conn, error)
    }
    
    • 提供 Register 函数(database/sql/sql.go)
    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
    }
    
    • 注册 driver (github.com/go-sql-driver/mysql/driver.go)
    func init() {
        sql.Register("mysql", &MySQLDriver{})
    }
    
    • 业务代码
    import _ "github.com/go-sql-driver/mysql"
    
    func main() {
        db, err := sql.Open("mysql", "gorm:gorm@tcp(localhost:9910)/gorm?charset=utf&&parseTime=True&loc=local")
    }
    
  • 连接接口2
    • 提供 Connector 接口 (database/sql/driver/driver.go)
    type Connector interface {
        Connect(context.Context) (Conn, error)
        Driver() Driver
    }
    
    • 提供 OpenDB 函数(database/sql/sql.go)
    func OpenDB(c driver.Connector) *DB {
        // ...
    }
    
    • 业务代码
    import "github.com/go-sql-driver/mysql"
    
    func main() {
        connector, err := mysql.NewConnector(&mysql.Config{
            User: "gorm",
            Passwd: "gorm",
            Net: "tcp",
            Addr: "127.0.0.1:3306",
            DBName: "gorm",
            ParseTime: true,
        })
    }
    
    db := sql.OpenDB(connector)
    

GORM 的使用简介

GORM 的基本用法

import (
    "gorm.io/gorm"
    "gorm.io/driver/mysql"
)

func main() {
    db, err := gorm.Open(
        mysql.Open("user:password@tcp(127.0.0.1:3306)/hello")
    )
    
    var users []User
    err = db.Select("id", "name").Find(&users, 1).Error
}
  • CRUD
user := User{Name: "jinzhu", Age: 18, Birthday: time.Now()}
result := db.Create(&user)

var product Product
db.First(&product, 1) // 查询 id 为 1
db.First(&product, "code = ?", "L1212") // 查询 code 为 L1212

// 更新某个字段
db.Model(&product).Update("Price", 2000)

db.Delete(&product)

Model 定义

type Model struct {
	ID        uint `gorm:"primarykey"`
	CreatedAt time.Time
	UpdatedAt time.Time
	DeletedAt DeletedAt `gorm:"index"`
}

惯例约定

  • 结构体名的蛇形命名法(小写字母+下划线)复数形式:表名
  • 结构体字段名的蛇形命名法单数形式:字段名
  • ID/Id 字段:主键,如果为数字则为自增主键
  • CreatedAt 字段:创建的时间
  • UpdatedAt 字段:创建、更新时的时间
  • gorm.DeletedAt 字段:默认开启 soft delete 模式

关联介绍

  • Has One, Has Many, Belongs To, Many To Many, Polymorphism, Single-table, inheritance
  • 关联操作:
    • CRUD
    • Preload / Joins 预加载
    • 级联删除

GORM 的设计原理

[应用程序]--[GORM]--(操作接口)--[database/sql]--(连接接口/操作接口)--[数据库]

SQL 生成的机制

GORM statement 是模仿 SQL statement 的组成来实现的,;例如:

db.Where("role <> ?", "manager").Where("age > ?", 35).Limit(100).Order("age desc").Find(&user)

由一系列 Chain Method 和最后的 Finisher Method 组成,其中 Chain Method 决定了 SQL Clauses,而 Finisher Method 决定了语句的类型并给予执行:

SELECT * FROM users WHERE role <> "manager" AND age > 35 ORDER BY age desc LIMIT 100
  • 可以自定义 Builder。不同数据库/不同版本支持的 SQL 不同,通过自定义 Builder 隐藏具体细节。
  • 可以扩展子句。例如db.Clauses(hints.UseIndex("idx_user_name")).Find(&User{})
  • 可以选择子句。处理不同数据库的 SQL 差异,例如
    /* PostgreSQL*/
    DELETE FROM users WHERE condition
    /* MySQL*/
    DELETE FROM users WHERE condition ORDER BY age desc LIMIT 100
    

插件扩展机制

Finisher Method --> 决定 statement 类型 --> 执行 callbacks --> 生成 SQL 并执行
                    (_______________________插件系统_______________________)
  • callback的模式:Create, Query, Update, Delete, Row, Raw
  • 每一种模式对应一组 callbacks,在执行的时候一一执行所有注册的 callbacks.例如Create:
    • 注册的callbacks
    db.Callback().Create().Register("gorm:begin_transaction", BeginTransacntion)
    db.Callback().Create().Register("gorm:before_create", BeforeCreate)
    //...
    
    • 具体执行db.Create()的过程(伪代码):
    func Create(data interface{}) error {
        for _, f := range db.callbacks.creates {
            f()
        }
    }
    
  • 扩展插件的API
db.Callback().Create().Register()
db.Callback().Create().Remove()
db.Callback().Create().Replace()
db.Callback().Create().Get()
//...
  • 因此可以实现:多租户,多数据库、读写分离,加解密等

ConnPool 扩展机制

    (实现接口)      (连接池)
[GORM] --> [ConnPool] --> [数据库]
               ↑(实现接口)
     [DB Conn(database/sql)]

Dialector 扩展机制

例如:

db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})

中的 mysql.Open(dsn) 就是一个 dialector。通过 dialector 实现定制化的功能。

GORM 最佳实践

  • 数据序列化与SQL表达式
  • 批量数据操作
  • 代码复用、分库分表、sharding
  • 混沌工程/压测
  • Logger/Trace
  • Migrator
  • Gen代码生成/Raw SQL
  • 安全