GORM设计原理 | 青训营笔记

626 阅读5分钟

这是我参与【第五届青训营】伴学笔记创作活动的第6天

一、本堂课重

  • 1、GORM设计原理
    • SQL生成
    • 扩展机制
    • ConnPool
    • Dialector
  • 2、GORM最佳实践
    • 企业级开发
    • FAQ

二、详细知识点介绍

1、GORM设计原理

SQL---GORM

SELECT —— FROM —— WHERE ——GROUP BY —— ORDER BY —— LIMIT —— FOR

GORM:

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

ConnPool

【GORM】 --- SQL ---> 【DB Conn】---连接池---> 【数据库】

image.png

//全局模式,所有 DB 操作都会预编译并缓存 (缓存不含参数部分)
db,err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{PrepareStmt: true})
db.First(&user,1)

// 会话模式,后续会话的操作都会预编译并缓存
tx := db.Session(&SessionfPrepareStmt: true})
tx.Find(&users)
tx.Model(&user).Update("Age",18)

// 全局缓存的语句可被会话使用
tx.First(&user,2)

stmtManger,ok := tx.ConnPool.(*PreparedStmtDB)
//关闭当前会话的预编译语句
stmtManger.Close)

Callback

image.png

多租户

image.png

多数据库、读写分离

DB.Use(dbresolver.Register(dbresolver.Config{
    // `db2`作为 sources、`db3`、`db4`作为 replicas
    Sources:  []gorm.Dialector{ mysql.Open("db2_dsn")},
    Replicas: []gorm.Dialector[ mysql.Open("db3_dsn"),mysql.Open("db4_dsn")},
    // sources/repLicas 负载均衡策略
    Policy: dbresolver.RandomPolicy{},
}).Register(dbresolver.Config{
    //`db1`作为 sources (DB 的默认连接),对于UserAddress使用db5 作为 replicas
    Replicas:[]gorm.Dialectorf mysql.Open("db5_dsn")},
},&User{},&Address{}).Register(dbresolver.Config{
    //`db6`、`db7`作为 sources,对于orders、Product 使用`db8` 作为 replicas
    Sources:[]gorm.Dialector mysql.Open("db6_dsn"),mysql.Open("db7_dsn")},
    Replicas: []gorm.Dialectorf mysql.Open("db8_dsn")},
},"orders",&Product{},"secondary"))

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

bytegorm

import(
    "code .byted.org/gorm/bytedgorm"
    "gorm.io/gorm"
)

// 初始化
DB,err := gorm.Open(
    // psm 的格式为 p.s.m 无需 _write,_read 等后缀,dbname 为数据库名
    bytedgorm.MySQL("p.s.m"/*数据库PSM*/,"dbname"/*数据库名\*/).WithReadReplicas()
    bytedgorm.WithDefaults(),
)

Dialector

import "gorm.io/driver/mysql"
dsn := "user:passetcp(127.0.0.1:3306)/dbname?charset=utf8n"seTime=True&loc=Local
db,err := gorm.Open(mysql .Open(dsn).&gorm.Config)

import "gorm.io/driver/postgres"
db, err := gorm.Open(postgres .Open(dsn), &gorm.Configf)

import "gorm.io/driver/clickhouse"
db,err := gorm.Open(clickhouse.Open(dsn), &gorm.Config.)

import "xxx.io/caches"
db,err := gorm.Open(caches .New(caches.Config{
    Fallback: mysql.Open(dsn),
    Store:    lru.New(lru.Config{}),
}),&gorm.Config{})

image.png

2、GORM最佳实践

SQL表达式更新创建

//方法1:通过 gorm.Expr 使用 SQL 表达式
db.Model(User3)Create(map[stringlinterface{}{
    "Name":"jinzhu",
    "Location": gorm.Expr("ST_PointFromText(?)","POINT(100 100)"),
}
// INSERT INTO "user_with_points" ("name""location") VALUES ("jinzhu"ST_PointFromText("POINT(100 100)");
db.Model(&product).Update("price",gorm.Expr("price * ? + ?", 2, 100))

// 方法2: 使用 GORMValuer 使用 SQL 表达式 / SubQuery
type Location struct {
    X, Y int
}
func (loc Location) GormValue(ctx context.Context, db *gorm.DB) clause.Expr {
    return gorm.Expr("ST_PointFromText(?)", fmt.Sprintf("POINT(%d %d)", loc.X, loc.Y))
}
db.Create(UserName:"jinzhu",Location:Location{X: 100,Y: 100}})
db.Model(&User{ID: 1}).Updates(User{Name:"jinzhu",Location: Location{X: 100,Y: 100}})

//方法3: 通过 *gorm.DB 使用 SubQuery
subQuery := db,Model(&Companyf+).Select("name"),Where("companies.id = users,company_id")
db.Model(&user)Updates(map[stringlinterface{}{}{"company-name": subQuery})
// UPDATE "users"SET "company-name"  (SELECT name FROM companies WHERE companies,id = users.company_id);

SQL表达式查询

// 方法1:使用gorm.Expr
db.Where("location = ?", gorm.Expr("ST_PointFromText(?)", "POINT(100 100)")).First(&user)
// SELECT * FROM `USERS` WHERE `location` = ST_PointFromText("POINT(100 100)");

// 方法2:Struct定义GormValuer
func (loc Location) GormValue(ctx context.Context, db *gorm.DB) clause.Expr{
    return gorm.Expr("ST_PointFromText(?)", fmt.Sprint("POINT(%d %d)", loc.X, loc.Y))
db.Where("location = ?", LocationfX: 100,Y: 1003)First(&user)
// SELECT *FROM users’ WHERE Location'= ST_PointFromText("POINT(100 100)");

// 方法3:自定义查询 SQL 实现接口 clause.Expression
type Expression interface {
    Build(builder Builder)
}
db.Find(&user,datatypes .JSONQuery("attributes").HasKey("role"))
db . Clauses(datatypes .JSONQuery("attributes").HasKey("org","name")).Find(&user)

// 方法4: SubQuery
db.where("name in (?)",db.Model(&User ).Select("name").Where("id > 10")).Find(&user)

数据序列化

type User struct{
    Name        []byte                  `gorm:"serializer:json"`
    Roles       Roles                   `gorm:"serializer:json"`
    Contracts   map[string]interface{}  `gorm:"serializer:gob"`
    JobInfo     Job                     `gorm:"type:bytes;serializer:gob"`
    CreatedTime int64                   `gorm:"serializer:unixtime;type:time"`
    Password    Password               
    Attributes  datatypes.JSON
}
    
// 自定义数据格式实现接口 Scanner,Valuer
func (j JSON) Value) (driver .Value, error)  {/** **/}
func (j *JSON) Scan(value interface{}) error f {/** **/}

//自定义数据格式实现 Serializer 接口
type Password string

type Serializer interface {
    {Scan(context.Context,f *schema.Field,dst reflect.Value,dbValue interface{}) error
    Value(context.Context,f *schema.Field,dst reflect.Value,fieldV interface{}) 
(interface{},error
}

批量创建/查询

// 批量创建
var users =  []User{{Name: "jinzhu1"},{Name: "jinzhu2"},{Name: "jinzhu3"}}
db.Create(&users)
db.CreateInBatches(users,100)
for _, user := range users {
    user.ID // 1,2,3
}
//批量查询
rows,err := db.Model(&User{}).Where("role = ?","admin").Rows()
for rows.Next() {
    // 方法1: sql.Rows Scan
    rows.Scan(&name,&age,&email) // NULL 值的情况?
    // 方法2: gorm ScanRows
    db.ScanRows(rows,&user)
    // xxx
}
DB.Where("role = ?""admin").FindInBatches(&results, 100, func(tx *gorm.DB, batch int) error {
})

批量更新

//忽路数据冲突
db.Clauses(clause.0nConflictfDoNothing: true}).Create(&users)
// INSERT INTO `users`*** ON DUPLICATE KEY DO NOTHING; // PostgresQL
// INSERT INTO `users` *** ON DUPLICATE KEY UPDATE `id`=`id`; // MySQL

db,Clauses(clause.Insert{Modifier: "IGNORE"}).Create(&users)
//INSERT IGNORE INTO `users` ***; // MySQL

// 数据冲突时更新某些字段
db.Clauses(clause.OnConflict{
    Columns:   []clause.Column{{Name: "id"}},
    DoUpdates: clause.Assignments(map[string]interface{}{"deleted_at": nil}),
}).Create(&users)
// INSERT INTO `users` *** ON DUPLICATE KEY UPDATE `deleted_at`=NULL;

db.Clauses(clause.OnConflict{
    Columns:   []clause.Column{{Name: "id"}},
    DoUpdates: clause.Assignments(map[string]interface{}{"count": gorm.Expr("GREATEST(count, VALUES(count))")}
}).Create(&users) 
// 使用 SOL 表达式更新INSERT INTO `users` ON DUPLICATE KEY UPDATE `count`=GREATEST(count,VALUES(count));

//数据冲突时更新某些字段为新值
db.Clauses(clause.OnConflict{
    Columns:[]clause.Column{{Name: "id"}},
    DoUpdates: clause.AssignmentColumns([]string {"nare""age"}),
}).Create &users)
// INSERT INTO `users` *** ON DUPLICATE KEY UPDATE `name`=VALUES(name),`age`=VALUES(age); 

// 数据冲突时更新全部字段(除主键) 为新值
db.Clauses(clause.0nConflictfUpdateAll: true )Create(&users)
// INSERT INTO `users` *** O DUPLICATE KEY UPDATE `name`=VALUES(name),`age`=VALUES(age),...;

……

Gen代码生成/Raw SQL - Raw SQL

//原生 SQL
db.Raw("SELECT id,name,age FROM users WHERE name = ?", "jinzhu")Scan(&result)
db.Raw("SELECT id,name,age FROM users WHERE name = jinzhu").Scan(&result)
db.Exec("UPDATE orders SET shipped_at=? WHERE id IN ?", time.Now(), []int64{1,2,3})

//Row & Rows
//使用 GORM API 构建 SQL
row := db.Table("users").Where("name = ?","jinzhu").Select("name", "age").Row()
// SELECT name,age FROM users WHERE name = "jinzhu"
row.Scan(&name,&age)

// 使用 Named Argument 做为 Where 条件
DB.Where("namel = @name OR name2 = @name"sql,Named("name","jinzhu")).Find(&user)
DB.Where("name1= @name OR name2= @name", map[string]interface{}{"name":"jinzhu"}).Find &user)
DB.Where("name1 = @Name OR name2 = @Name", User{Name:"jinzhu"}).Find(&user)
// SELECT * FROM users’ WHERE name1 = "jinzhu" OR name2 = "jinzhu'

// 命名参数和原生 SQL
DB.Raw(
    "SELECT * FROM users WHERE name1 = @name OR name2 = @name2 OR name3 = @name", sql.Named("name""jinzhu1"), sql.Named("name2","jinzhu2")
).Find(&user)
// SELECT * FROM users WHERE name1 = "jinzhu1"OR name2 = "jinzhu2" OR name3 = "jinzhu1"

Gen代码生成/Raw SQL - Gen

type Query interface {
    // SELECT *FROM @@table
    // {{where}}
    // {{if id>0}} id-@id {{end}}
    // {{if name !=""}}AND name=@name {{end}}
    // {{if age >18}}AND age>@age {{end}}
    // {{end}}
FindUser(id int32,name string,age int)(gen.T,err)
}

// 生成自定义 SQL 静态代码
g := gen.NewGenerator(gen.Config{
    OutPath:"../dal/query",
})
g.AppLyBasic(model.User{}) // 生成对应的 CURD API
g.ApplyInterface(func(query method.Query) {} ,model.User{}) // 给 User 生成 Query Interface 方法
g.Execute()

//查询数据
user,err := query.User.FindUser(10,"jinzhu",16)
// SELECT * FROM users WHERE id = 10 AND name ="jinzhu"

安全问题

// 安全,会以参数传入
db.Create(UserfName: userInput})
db.Model(user).Update( "name",userInput)
db.Where(UserName: userInput3).First(&user)
db.Where("name = ?",userInput).First(&user)

// 危险,SQL 注入
sql := fmt.Sprintf("name = %v", userInput)
db .Where(sql).First &user)
db.Select("name; drop table users;").First(&user)
db.Distinct("name; drop table users;").First(&user)
db.Model(&user).Pluck("name; drop table users;", &names)
db.Group("name; drop table users;").First(&user)
db.Group("name").Having("1 = 1;drop table users;").First(&user)
db.Raw("select name from users; drop table users;").First(&user)
db.Exec("select name from users; drop table users;")

三、课后个人总结

从 SQL、插件扩展、ConnlPool以及 Dialector 分别展开,了解到GORM设计原理等等。

四、课程链接学习

GORM 设计原理 - 掘金 (juejin.cn)

GORM 最佳实践 - 掘金 (juejin.cn)