gorm与子句生成器有关的类,按父级到子集排列为 DB --> Statement --> Clause --> Expression (分别对应 数据库连接对象–> 语句 --> 子句 --> 表达式),它们都是以属性形式保存在父类中。只要知道这个结构,看源码就会轻松很多。
在实际操作用,每次使用Find
、First
这些写方法时,都会生成一个Statement对象,后面就是对Statement中的Clauses属性进行添加、修改和执行,执行过程中调用Expression接口的表达式生成器,生成最终的sql语句。
在使用gorm的Find
、First
这些写方法时,通过参数传入。会将clause添加到Statement中。
// Find find records that match given conditions
func (db *DB) Find(dest interface{}, conds ...interface{}) (tx *DB) {
tx = db.getInstance()
if len(conds) > 0 {
if exprs := tx.Statement.BuildCondition(conds[0], conds[1:]...); len(exprs) > 0 {
tx.Statement.AddClause(clause.Where{Exprs: exprs})
}
}
tx.Statement.Dest = dest
return tx.callbacks.Query().Execute(tx)
}
上面的代码return tx.callbacks.Query().Execute(tx)
执行时,会添加gorm默认的Clause到Statement中
https://github.com/go-gorm/gorm/blob/master/callbacks.go
关键部分代码:
if len(stmt.BuildClauses) == 0 {
stmt.BuildClauses = p.Clauses
resetBuildClauses = true
}
使用DB.Clauses
方法时,可以将自定义的子句生成器添加到Statement中。重点是传入的参数需要实现StatementModifier接口中的方法。
// Clauses Add clauses
func (db *DB) Clauses(conds ...clause.Expression) (tx *DB) {
tx = db.getInstance()
var whereConds []interface{}
for _, cond := range conds {
if c, ok := cond.(clause.Interface); ok {
tx.Statement.AddClause(c)
} else if optimizer, ok := cond.(StatementModifier); ok {
optimizer.ModifyStatement(tx.Statement)
} else {
whereConds = append(whereConds, cond)
}
}
if len(whereConds) > 0 {
tx.Statement.AddClause(clause.Where{Exprs: tx.Statement.BuildCondition(whereConds[0], whereConds[1:]...)})
}
return
}
通过代码可以看到,支持3种类型的参数参入:
clause.Interface
子句生成器接口StatementModifier
修改staement的接口clause.Expression
表达式生成器接口,这里的表达式最终会添加到clause.Where中,也就是把原先的clause.Where覆盖掉。针对模型字段,可以按照以下接口为其添加子句生成器。
//调用gorm的创建方法时生效
type CreateClausesInterface interface {
CreateClauses(*Field) []clause.Interface
}
//调用gorm的查询方法时生效
type QueryClausesInterface interface {
QueryClauses(*Field) []clause.Interface
}
//调用gorm的更新方法时生效
type UpdateClausesInterface interface {
UpdateClauses(*Field) []clause.Interface
}
//调用gorm的删除方法时生效
type DeleteClausesInterface interface {
DeleteClauses(*Field) []clause.Interface
}
这些方法它只针对携带这个方法的字段生效。如gorm.DeletedAt字段,会自动添加查询条件deleted_at IS NULL
。
gorm本身已经定义了很多的子句构造器,除了本文中的示例,我们也可以直接参考源码来实现自己的子句构造器
通过db.Clauses()
方法,可以对gorm的SQL语句做一些拓展。clause.Interface是clause的接口。而传入的参数需要实现接口中的方法。
我们来看一下网上引用比较多的gorm自带的子句生成器clause.OnConflict{},它实现的是clause.Interface
这个接口,也就是子句生成器。
// 使用SQL语句
db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "id"}},
DoUpdates: clause.Assignments(map[string]interface{}{"count": gorm.Expr("GREATEST(count, VALUES(count))")}),
}).Create(&users)
// INSERT INTO `users` *** ON DUPLICATE KEY UPDATE `count`=GREATEST(count, VALUES(count));
这是使用示例,表示创建时如果id字段重复就会执行DoUpdates中的更新操作。
Expression 是表达式生成器的接口,同时属于clause的子类。比如通过clause.Eq(equal,等值表达式生成器)生成WHERE id=100
。
重写表达式可能会导致的问题
重写的表达式生成器在特定的场景下,可能会拿不到字段名(拿到nil
)。这时候有两种方法,一种是做判断跳过。还有一种就是从父类开始重写。
StatementModifier,修改staement的方法。
只要自定义的模型字段中包含以下的其中一种方法即可,
CreateClauses(*Field) []clause.Interface
QueryClauses(*Field) []clause.Interface
UpdateClauses(*Field) []clause.Interface
DeleteClauses(*Field) []clause.Interface
这个返回值类型可以实现StatementModifier接口 或者 clause.Interface接口都行。相关的实现细节可以参考源码的软删除字段gorm.DeletedAt字段。
gorm包本身带有一个软删除字段gorm.DeletedAt,它具有一个与本文有关的功能点是,在生成SQL查询语句时,gorm会自动调用其自带的QueryClauses
函数,添加查询条件deleted_at IS NULL
。
而在MYSQL中,这个查询条件有一个短板——耗时(因为这个字段默认是带索引的,但是空值会导致索引失效)。
起初可能没什么感觉,只要数据量过10万,查询速度会很明显的变慢。我想把它自动生成的查询条件改为IFNULL(deleted_at IS NULL,0)=0
,IFNULL这个方法可以让查询速度几何倍提升。
接下来,我们需要重写QueryClauses
方法,示例如下:
import (
"gorm.io/gorm"
"gorm.io/gorm/clause"
"gorm.io/gorm/schema"
)
//DeletedAt 继承gorm.DeletedAt
type DeletedAt struct {
gorm.DeletedAt
}
//重写QueryClauses
func (DeletedAt) QueryClauses(f *schema.Field) []clause.Interface {
var inherit = gorm.SoftDeleteQueryClause{Field: f}
return []clause.Interface{BeautifulIsNULLSearch{inherit}}
}
QueryClauses
实际是返回了 StatementModifier接口的实例,我们继续重写这个类:
type BeautifulSoftDeleteQueryClause struct {
gorm.SoftDeleteQueryClause
}
func (b BeautifulIsNULLSearch) ModifyStatement(stmt *gorm.Statement) {
if _, ok := stmt.Clauses["soft_delete_enabled"]; !ok {
if c, ok := stmt.Clauses["WHERE"]; ok {
if where, ok := c.Expression.(clause.Where); ok && len(where.Exprs) > 1 {
for _, expr := range where.Exprs {
if orCond, ok := expr.(clause.OrConditions); ok && len(orCond.Exprs) == 1 {
where.Exprs = []clause.Expression{clause.And(where.Exprs...)}
c.Expression = where
stmt.Clauses["WHERE"] = c
break
}
}
}
}
stmt.AddClause(clause.Where{Exprs: []clause.Expression{
//调用自定义表达式生成器
Eq{Column: clause.Column{Table: clause.CurrentTable, Name: b.Field.DBName}, Value: nil},
}})
stmt.Clauses["soft_delete_enabled"] = clause.Clause{}
}
}
ModifyStatement
方法调用了表达式生成器Eq
(equal),真正生成查询条件的地方也就是这里,我们继续重写:
// Eq equal to for where
type Eq struct {
Column interface{}
Value interface{}
clauseName string
}
func (eq Eq) Build(builder clause.Builder) {
switch eq.Value.(type) {
case []string, []int, []int32, []int64, []uint, []uint32, []uint64, []interface{}:
builder.WriteQuoted(builder)
builder.WriteString(" IN (")
rv := reflect.ValueOf(eq.Value)
for i := 0; i < rv.Len(); i++ {
if i > 0 {
builder.WriteByte(',')
}
builder.AddVar(builder, rv.Index(i).Interface())
}
builder.WriteByte(')')
default:
column, _ := eq.Column.(clause.Column)
stmt, _ := builder.(*gorm.Statement)
if eqNil(eq.Value) {
// 重写原生方法 builder.WriteString(" IS NULL")
builder.WriteString(fmt.Sprintf("IFNULL(`%s`.%s, 0) = 0", stmt.Table, column.Name))
} else {
builder.WriteString(fmt.Sprintf("`%s`.%s = ", stmt.Table))
builder.AddVar(builder, eq.Value)
}
}
}
func (eq Eq) NegationBuild(builder clause.Builder) {
Neq(eq).Build(builder)
}
func eqNil(value interface{}) bool {
if valuer, ok := value.(driver.Valuer); ok && !eqNilReflect(valuer) {
value, _ = valuer.Value()
}
return value == nil || eqNilReflect(value)
}
func eqNilReflect(value interface{}) bool {
reflectValue := reflect.ValueOf(value)
return reflectValue.Kind() == reflect.Ptr && reflectValue.IsNil()
}
效果
数据表模型如下
type MODEL struct {
ID uint `gorm:"primarykey"` // 主键ID
CreatedAt time.Time // 创建时间
UpdatedAt time.Time // 更新时间
DeletedAt DeletedAt `gorm:"index" json:"-"` // 删除时间
}
type User struct{
MODEL
Name string
}
func (User)TableName()string{
return "sys_user"
}
查询示例
var user []User
DB.Find(&user)
// SELECT * FROM `sys_user` WHERE IFNULL(`sys_user`.deleted_at, 0) = 0