字节跳动青训营第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 包向应用程序提供一些操作接口用于操作数据库,这个包本身管理了一个连接池,再根据不同数据库的连接接口和操作接口进行处理。
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)
插件扩展
插件工作方式:
- Finisher Method
- 决定statement 类型
- 执行Callbacks
- 生成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 实践
后续补充