GORM最佳实践day04 | 青训营笔记

99 阅读6分钟

GORM最佳实践

1、数据序列化与SQL表达式

在 GORM 中构建 SQL 表达式通常使用链式调用的方式,可以通过 GORM 提供的方法来构建 SQL 查询语句。

SQL表达式更新创建:
  • 通过gorm.Expr使用SQL表达式
// product's ID is `3` 
db.Model(&product).Update("price",gorm.Expr("price * ? + ?", 2, 100)) 
// UPDATE "products" SET "price" = price * 2 + 100, "updated_at" = '2013-11-17 21:34:10' WHERE "id" = 3;  

db.Model(&product).Updates(map[string]interface{}{"price": gorm.Expr("price * ? + ?", 2, 100)}) 
// UPDATE "products" SET "price" = price * 2 + 100, "updated_at" = '2013-11-17 21:34:10' WHERE "id" = 3;  

db.Model(&product).UpdateColumn("quantity", gorm.Expr("quantity - ?", 1)) 
// UPDATE "products" SET "quantity" = quantity - 1 WHERE "id" = 3;  

db.Model(&product).Where("quantity > 1").UpdateColumn("quantity", gorm.Expr("quantity - ?", 1)) 
// UPDATE "products" SET "quantity" = quantity - 1 WHERE "id" = 3 AND quantity > 1;
  • 通过GORMValuer使用SQL表达式

// Create from customized data type 
type Location struct {     
    X, Y int 
} 
func (loc Location) GormValue(ctx context.Context, db *gorm.DB) clause.Expr {   
    return clause.Expr{     
        SQL:  "ST_PointFromText(?)",     
        Vars: []interface{}{fmt.Sprintf("POINT(%d %d)", loc.X, loc.Y)},   
    } 
} 
db.Model(&User{ID: 1}).Updates(User{   
    Name:  "jinzhu",   
    Location: Location{X: 100, Y: 100}, 
}) 
// UPDATE `user_with_points` SET `name`="jinzhu",`location`=ST_PointFromText("POINT(100 100)") WHERE `id` = 1 

  • 通过*gorm.DB 使用SubQuery
subQuery db.Model(&Company.Select("name").Where("companies.id = users.company_id")
db.Model(&user).Updates(map[string]interface{}{}{"company_name": subQuery})
//UPDATE "users" SET "company_name" = (SELECT name FROM companies WHERE companies.id = users.company_id);
SQL表达式查询
    1. 使用gorm.Expr
    1. struct 定义 GormValuer
    1. 自定义查询 SQL 实现接口 clause.Expression
    1. 使用子查询SubQuery
//方法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.Sprintf("POINT(%d %d)", loc.X, loc.Y))
 }
db.Where("location = ?", Location{X: 100,Y: 100}).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)
数据序列化

Golang本身只支持一些基本的数据类型,在实际使用过程中,我们可能需要更复杂的数据结构来实现业务需求。Golang中通过自定义数据格式实现Serializer接口扩展。

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 {/****/}

//自定义数据格式实现Serializer接口
type Password string
type SerializerInterface 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)
}

2、批量数据操作

批量创建

为了批量插入数据,可以像Create方法传入一个切片。GORM会生成插入SQL语句,批量插入数据。同时还提供了CreateInBatcher等方法设置需要插入数据的批次大小,每个批次插入都是一个事务。批量操作时默认调用钩子函数。


var users = []User{{Name: "jinzhu1"}, {Name: "jinzhu2"}, {Name: "jinzhu3"}} 
db.Create(&users)  
 
for _, user := range users {   
    user.ID // 1,2,3 
}

// 指定批次大小
var users = []User{{Name: "jinzhu_1"}, ...., {Name: "jinzhu_10000"}}  
// batch size 100 
db.CreateInBatches(users, 100)

批量查询

可以通过Scan方法或者ScanRows方法获取批量的数据

//批量查询
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{})
批量数据加速操作

批量数据加速的操作包括

  • 关闭默认事务;
  • 调用SkipHooks方法,跳过钩子函数达到加速目的;
  • 通过预编译方法(Prepared Statement),创建一个缓存,加速后续的操作。
//方法1:关闭默认事务
db,err :gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{
    SkipDefaultTransaction: true,
})
db.Create(&user)
tx := db.Session(&Session{SkipDefaultTransaction: true})
tx.Create(&user)

//方法2:默认批量导入会调用Hooks方法,使用`SkipHooks`跳过
DB.Session(&gorm.Session{SkipHooks: true}).Create(&users)
DB.Session(&gorm.Session{SkipHooks: true}).CreateInBatches(users, 1000)

//方法3:使用Prepared Statement
db,err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{PrepareStmt: true})
db.Create(&users)

//混合使用
tx := db.Session(&Session{
    PrepareStmt:true,SkipDefaultTransaction: true, SkipHooks: true, CreateBatchSize: 1000,})
tx.Create(&users)

3、代码复用、分库分表、Sharding

代码复用
func Paginate(r *http.Request ) func(db *gorm.DB) *gorm.DB {
    return func (db *gorm.DB) *gorm.DB {
        page, _ := strconv.Atoi(r.Query("page"))
        if page == 0 {
            page = 1
        }
    pageSize, _ := strconv.Atoi(r.Query("page_size"))
    switch {
    case pagesize > 100:
        pagesize = 100
    case pagesize < 0:
        pageSize = 10
    }
    offset := (page -1) * pagesize
    return db.Offset(offset).Limit(pagesize)
    }
}
//代码共享
db.Scopes(Paginate(r)).Find(&users)
db.Scopes(Paginate(r).Find(&articles)
分库分表
//使用传入数据分表
func TableOfYear(user *User,year int) func(db *gorm.DB) *gorm.DB {
    return func(db *gorm.DB)*gorm.DB {
        tableName := user.TableName()+strconv.Itoa(year)
        return db.Table(tableName)
    }
}

DB.Scopes(TableOfYear(user, 2019)).Find(&users)
//SELECT FROM users_2019;

//使用传入数据分库(同一个连接)
func TableOfOrg(user *User, dbName string) func(db *gorm.DB) *gorm.DB {
    return func(db *gorm.DB) *gorm.DB {
        tableName := dbName + "." + user.TableName()
        return db.Table(tableName)
    }
}
DB.Scopes(TableOfOrg(user, "org1")).Find(&users)
//SELECT FROM org1.users;

//使用对象信息获取表名/interface
func TableofUser(user *User) func(db *gorm.DB) *gorm.DB {
    return func(db *gorm.DB) *gorm.DB {
        year := getYearInfoFromUserID(user.ID)
        return db.Table(user.TableName() + strconv.Itoa(year))
    }
}
Sharding

Sharding 是一个高性能的 Gorm 分表中间件。它基于 Conn 层做 SQL 拦截、AST 解析、分表路由、自增主键填充,带来的额外开销极小。对开发者友好、透明,使用上与普通 SQL、Gorm 查询无差别,只需要额外注意一下分表键条件提供高性能的数据库访问。分片相关知识

db.Use(sharding.Register(sharding.Config{    
     ShardingKey: "user_id",    
     NumberOfShards: 64,     
     PrimaryKeyGenerator: sharding.PKSnowflake, 
}, "orders").Register(sharding.Config{     
    ShardingKey: "user_id",     
    NumberOfShards: 256,     
    PrimaryKeyGenerator: sharding.PKSnowflake,    
    // This case for show up give notifications, audit_logs table use same sharding rule. 
}, Notification{}, AuditLog{}))

4、混沌工程/压测

混沌工程,是一种提高技术架构弹性能力的复杂技术手段。 Chaos工程经过实验可以确保系统的可用性。混沌工程旨在将故障扼杀在襁褓之中,也就是在故障造成中断之前将它们识别出来。通过主动制造故障,测试系统在各种压力下的行为,识别并修复故障问题,避免造成严重后果。 GORM中的sqlchaos插件用来检查系统能否监控数据的不一致等问题。简单示例如下:sqlchaos会篡改插入数据,验证系统能否检测出错误。

import
    "example.com/gorm/sqlchaos"
    "gorm.io/gorm"
db,err :gorm.Open(
    mysql.Open(dsn),
    &gorm.Config{},
    sqlchaos.WithChaos(sqlchaos.Config{
        PSM: "service name",
        DBName: "dbname",
        EnvList: []string{"ppe", "boe"},//演练环境
        
db.Create(&UserID: 1024, Name: "rick", Result: 10})
// INSERT INTO table (id',user,result)VALUES (1024,rick,10)
sqlchaos篡改为
//INSERT INTO table (id',user,result)VALUES (1024,morty,100)

压测:GORM可以使用context构造压测环境

5、Logger/Trace

移步Logger官方文档

6、Migrator

GORM支持自动迁移数据库,提供了大量的接口操作表、字段、约束,index等等,能够满足大部分业务需求。ColumnType接口提供了表名、数据库类型等等信息。

迁移简单示例:

//自动迁移数据库
db.AutoMigrate(&User)
//版本管理数据库
import "github.com/go-gormigrate/gormigrate/v2"
m := gormigrate.New(db, gormigrate.DefaultOptions, []*gormigrate.Migration{
    {
    ID:"201608301400",
    Migrate:func(tx *gorm.DB)error
        type User struct {
            Name string
        }
        return tx.AddColumn(&User, "Name")
    },
    Rollback: func(tx *gorm.DB) error {
        return tx.DropColumn(&User{}, "Name")
    },
  },
})
m.Migrate()

7、Gen代码生成/Raw SQL

Gen代码生成 Gen通过代码生成,让 GORM 对复制SQL场景能都更好的处理,也更加安全。 移步Gen官方使用文档

GORM提供了原生SQL代码的执行,简单示例如下:

type Result struct {
    ID int
    Name string 
    Age int 
}
var result Result
db.Raw("SELECT id, name, age FROM users WHERE id = ?", 3).Scan(&result)
db.Exec("DROP TABLE users")  
db.Exec("UPDATE orders SET shipped_at = ? WHERE id IN ?", time.Now(), []int64{1, 2, 3})  
  
// Exec with SQL Expression  
db.Exec("UPDATE users SET money = ? WHERE name = ?", gorm.Expr("money * ? + ?", 10000, 1), "jinzhu")

GORM 支持 sql.NamedArgmap[string]interface{}{} 或 struct 形式的命名参数

// 原生 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"

8、安全

GORM对所有SQL语句执行过程中,都使用参数的形式传入,防止SQL注入等问题。

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

//危险,SQL注入。避免使用用户输入拼接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;")