Optimizing GoFrame Queries with a Fluent Builder Pattern
Standard GoFrame interactions require explicit verification for empty parameters before appending filters. A wrapper class can automate this validation using a fluent API to reduce boilerplate and improve readability.
The following implementation defines a QBuilder struct that extends the native gdb.Model. By disabling safe mode during initialization, the underlying model instance remains mutable across chain calls, ensuring efficient state propagation without unnecessary cloning overhead. Helper methods check for null or empty values before executing SQL clauses, preventing unintended logic errors.
package helper
import (
"github.com/gogf/gf/v2/database/gdb"
)
// QBuilder serves as a fluent interface wrapper around the native gdb.Model
type QBuilder struct {
model *gdb.Model
}
// NewBuilder initializes the query builder, configuring safety settings for optimization
func NewBuilder(baseModel *gdb.Model) *QBuilder {
baseModel.Safe(false) // Disable auto-cloning to allow direct mutation within the chain
return &QBuilder{model: baseModel}
}
// Fetch executes both count retrieval and data scanning in a single operation
func (qb *QBuilder) Fetch(dest interface{}, totalCount *int, additionalFilters ...any) error {
var total int64
total, err := qb.model.Count(additionalFilters...)
if err != nil {
return err
}
err = qb.model.Scan(dest, additionalFilters...)
*totalCount = int(total)
return err
}
// WhereEq builds an exact match condition with automatic null-safety
func (qb *QBuilder) WhereEq(col string, val any) *QBuilder {
if !isEmpty(val) {
qb.model.Where(col, val)
}
return qb
}
// WhereNot constructs a non-matching condition safely
func (qb *QBuilder) WhereNot(col string, val any) *QBuilder {
if !isEmpty(val) {
qb.model.WhereNot(col, val)
}
return qb
}
// LikeLeft matches values starting with the provided string
func (qb *QBuilder) LikeLeft(col string, val string) *QBuilder {
if val != "" {
qb.model.WhereLike(col, "%"+val)
}
return qb
}
// LikeRight matches values ending with the provided string
func (qb *QBuilder) LikeRight(col string, val string) *QBuilder {
if val != "" {
qb.model.WhereLike(col, val+"%")
}
return qb
}
// FullMatch performs a wildcard search within the column
func (qb *QBuilder) FullMatch(col string, val string) *QBuilder {
if val != "" {
qb.model.WhereLike(col, "%"+val+"%")
}
return qb
}
// isEmpty checks if a value is effectively zero or empty for query purposes
func isEmpty(v any) bool {
if v == nil {
return true
}
if s, ok := v.(string); ok {
return len(s) == 0
}
return false
}
Practical Application
Using this wrapper significantly reduces the cognitive load in business logic layers. Instead of checking fields individually before appending them to a Where clause, developers simply call the fluant methods. If a field is empty, the condition is silently skipped.
Consider a controller scenario filtering device records:
func GetDeviceList(ctx context.Context, req *v1.DeviceReq) (*result.Res, error) {
var result []do.Device
var total int
columns := do.Columns()
builder := NewBuilder(do.NewDao().Ctx(ctx))
builder
.WhereEq(columns.Name, req.TableName)
.WhereNot(columns.Id, req.ExcludeId)
.FullMatch(columns.Description, req.Keyword)
.Fetch(&result, &total)
return result.Success(result.DataWithPagination(result, total)), nil
}
For scenarios requiring extensive filtering that may span multiple lines for readability, the fluent style supports seamless continuation:
builder
.WhereEq(columns.Name, "MainTable")
.FullMatch(columns.Status, "Active")
.Fetch(&result, &total)
This approach centralizes query logic, minimizes syntax errors regarding column names, and ensures consistent handling of nullable parameters throughout the application stack.