字节跳动青训营第5课:设计模式之Database/SQL与GORM实践|青训营笔记

296 阅读3分钟

字节跳动青训营第5课:设计模式之Database/SQL与GORM实践

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

[TOC]

database/sql

目标:通过统一的接口,操作不一样的数据库

基本用法

import {
    "database/sql"
    "github.com/go-sql-driver/mysql"
}

func main() {
    // 使用driver和DSN初始化DB链接
    db, err := sql.Open("mysql", "user:password@tcp(127.0.0.1:3306)/hello")

    // 执行一条sql
    rows, err := db.Query("select id, name from users where id = ?", 1)
    if err != nil {
        // some code
    }
    // 释放资源
    defer db.Close()

    // 数据和错误处理
    var users []User
    for rows.Next() {
        var user User
        err := rows.Scan(&user.id, &user.name)

        if err != nil {
            // some code
        }

        users = append(users, user)
    }

    // 需要注意对rows的处理
    if rows.Err() != nil {
        // some code
    }
}

设计原理

基本设计原理概念图如下:

database/sql 设计原理

database/sql 包向应用程序提供一些操作接口用于操作数据库,这个包本身管理了一个连接池,再根据不同数据库的连接接口和操作接口进行处理。

DB连接的几种类型:

  • 直接连接/Conn
  • 预编译/Stmt
  • 事务/Tx

gorm基础使用

设计简洁,功能强大,自由扩展的全功能ORM

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
}
// 操作数据库
db.AutoMigrate(&Product{})
db.Migrator().CreateTable(&Product{})

// 创建
user := User{Name: "XinZF", Age: 24, Birthday: time.Now()}
result := db.Create(&user)  // 传递数据的指针来创建

// 返回主键 last insert id
user.ID
result.Error
// 返回影响的行数
result.RowsAffected

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

result := db.Find(&users, []int{1, 2, 3})
result.RowsAffected
errors.Is(result.Error, gorm.ErrorRecordNotFound)

// 更新
db.Model(&product).Update("Price", 2000)
db.Model(&product).UpdateColumn("Price", 2000)

// 更新多个字段
db.Model(&product).Update(Product{Price: 2000, code: "L1212"})
db.Model(&product).Update(map[string]interface{}{Price: 2000, code: "L1212"})

// 批量更新
db.Model(&Product{}).Where("price < ?", 2000).Updates("...")

// 删除
db.Delete(&product)

Model定义

type Model struct {
    ID              uint            `gorm:"primaryKey"`
    CreatedAt       time.Time
    UpdatedAt       time.Time
    // gorm.DeletedAt 默认软删除
    DeletedAt       gorm.DeletedAt  `gorm:"index"`
}

// 表名为Users,单例名为user,以此类推
type User struct {
    gorm.Model
    ID              uint    
    Name            string
    Email           *string
    Age             uint8
    Birthday        *time.Time
    MemberNumber    sql.NullString
    ActivateAt      sql.NullTime
}

关联操作

type User struct {
    gorm.Model
    Name            string
    // 拥有一个Account(has one)
    Account         Account
    // 拥有多个Pets(has many)
    Pets            []*Pet
    // 拥有多个Pets(多态 has many)
    Toys            []*Toy      `gorm:"polymorphic:Ower"`
    // 属于某个company
    CompanyID       *int
    Company         Company
    // 属于某个Manager,单表belongs to
    ManagerID       *uint
    Manager         *User
    Team            []User      `gorm:"foreignKey:ManagerID"`
    // 掌握若干种语言(many to many)
    Languages       []Language  `gorm:"many2many:UserSpeak;"`
    Friends         []*User     `gorm:"many2many:user_friends;"`
}

通常通过如下语句进行关联操作

association := db.Model(&user).Association("...")

GORM 设计原理

SQL生成

每一个SQL Statement都由很多个子句组成

  • SELECT Clause
  • FROM Clause
  • WHERE Clause
  • ORDER BY Clause
  • LIMIT Clause
  • FOR Clause

gorm通过链式方法实现各个子句的构成

设计原因:

  • 自定义Clause Builder
  • 方便扩展Clause
  • 自由选择Clauses

自定义Clause Builder

对于不同的数据库,或者同一数据库的不同发行版,可能存在子句上的差异

// MySQL < 8, MariaDB
// SELECT * FROM `users` LOCK IN SHARE MODE
// MySQL 8
// SELECT * FROM `users` FOR SHARE OF `users`
db.Clauses(clause.Locking{
    Strength: "SHARE",
    Table: clause.Table{Name: clause.CurrentTable}
}).Find(&users)

插件扩展

插件工作方式:

  1. Finisher Method
  2. 决定statement 类型
  3. 执行Callbacks
  4. 生成SQL并执行

主要的Callbacks类型

  • Create

  • Query

  • Update

  • Delete

  • Row

  • Raw

  • db.Callback().Create().Register("gorm:begin_transaction", BeginTransaction)

    在执行Create时若不属于事务,则去开启一个事务

  • db.Callback().Create().Register("gorm:before_create", BeforeCreate)

    执行当前Model的所有Before create的方法

  • db.Callback().Create().Register("gorm:save_before_association", SaveBeforeAssociation)

    执行Create时同时创建或保存一些前置关联信息

  • db.Callback().Create().Register("gorm:create", Create)

  • db.Callback().Create().Register("gorm:save_after_association", SaveAfterAssociation)

    执行Create时同时创建或保存一些后 置关联信息

在代码执行过程中,取出所有注册的Callbacks方法,然后一一执行

通过插件系统实现一个多租户系统

希望不同租户的数据有一定的隔离

// 根据 TenantID 进行过滤
func setTenantScope(db *gorm.DB) {
    if tenantID, err := getTenantID(db.Statement.Context); err != nil {
        db.Where("tenant_id = ?", tenantID)
    } else {
        db.AddError(err)
    }
}

db.Callback.Query().Before("gorm:query").Register("set_tenant_scope", setTenantScope)
db.Callback.Delete().Before("gorm:delete").Register("set_tenant_scope", setTenantScope)
db.Callback.Update().Before("gorm:update").Register("set_tenant_scope", setTenantScope)

通过插件系统实现多数据库、读写分离

DB.Use(dbresolver.Register(dbresolver.Config{
    Sources: []gorm.Dialector{
        mysql.Open("db2_dsn")},
    Replicas: []gorm.Dialector{
        mysql.Open("db3_dsn"), 
        mysql.Open("db4_dsn")},
    // 负载均衡策略
    Policy: dbresolver.RandomPolicy{}
})).Register(dbresolver.Config{
    // 对于 `User` `Address`使用db5作为replicas
    Replicas: []gorm.Dialector{
        mysql.Open("db5_dsn")}
}, &User{}, &Address{}).Register(dbresolver.Config{
    Sources: []gorm.Dialector{
        mysql.Open("db6_dsn"), 
        mysql.Open("db7_dsn")},
    Replicas: []gorm.Dialector{
        mysql.Open("db8_dsn")}
}, "orders", &Product{}, "secondary")

// 使用Write模式:从sources db `db1` 读取user
DB.Clauses(dbresolver.Write).Find(&user)
// 指定Resolver:从secondary db 的 replicas db `db8`读取user
DB.Clauses(dbresolver.Use("secondary")).First(&user)
// 指定resolver和Write模式:从secondary db的replicas db `db6` 或 `db7` 读取user
DB.Clauses(dbresolver.Use("secondary"), dbresolver.Write).First(&user)

GORM 实践

后续补充