gorm子句构造器Clause的使用

一、说明

gorm与子句生成器有关的类,按父级到子集排列为 DB --> Statement --> Clause --> Expression (分别对应 数据库连接对象–> 语句 --> 子句 --> 表达式),它们都是以属性形式保存在父类中。只要知道这个结构,看源码就会轻松很多。

在实际操作用,每次使用FindFirst这些写方法时,都会生成一个Statement对象,后面就是对Statement中的Clauses属性进行添加、修改和执行,执行过程中调用Expression接口的表达式生成器,生成最终的sql语句。

二、初始化clause的过程和方式

  1. 在使用gorm的FindFirst这些写方法时,通过参数传入。会将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)
    }
    
  2. 上面的代码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
    }
    
  3. 使用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覆盖掉
  4. 针对模型字段,可以按照以下接口为其添加子句生成器。

    //调用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本身已经定义了很多的子句构造器,除了本文中的示例,我们也可以直接参考源码来实现自己的子句构造器

1. 使用DB.Clauses方法

1.1 传入clause.Interface类型

通过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中的更新操作。

1.2 传入Expression类型

Expression 是表达式生成器的接口,同时属于clause的子类。比如通过clause.Eq(equal,等值表达式生成器)生成WHERE id=100

重写表达式可能会导致的问题
重写的表达式生成器在特定的场景下,可能会拿不到字段名(拿到nil)。这时候有两种方法,一种是做判断跳过。还有一种就是从父类开始重写。

1.3 传入StatementModifier类型

StatementModifier,修改staement的方法。

2. 为单个字段实现开发构造器

2.1 实现方法

只要自定义的模型字段中包含以下的其中一种方法即可,

CreateClauses(*Field) []clause.Interface
QueryClauses(*Field) []clause.Interface
UpdateClauses(*Field) []clause.Interface
DeleteClauses(*Field) []clause.Interface

这个返回值类型可以实现StatementModifier接口 或者 clause.Interface接口都行。相关的实现细节可以参考源码的软删除字段gorm.DeletedAt字段。

2.2 示例:软删除字段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
  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值