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表达式查询
-
- 使用gorm.Expr
-
- struct 定义 GormValuer
-
- 自定义查询 SQL 实现接口 clause.Expression
-
- 使用子查询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
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.NamedArg、map[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;")