Article directory
I. Introduction
1. Background
When I was there code review
, I found the following code in the project:
// GetUpdateFieldStr 组装更新的sql和参数
func GetUpdateFieldStr(ctx context.Context, updateFields *infra.UpdateFeilds) (sqlUpdate string, args []interface{
}) {
// 拼接update字段
log.Debugf("getUpdateFieldStr: (%#v)", updateFields)
sqlUpdate = ""
// 拼接update字段
if updateFields.MapId != "" {
sqlUpdate += " map_id = ?,"
args = append(args, updateFields.MapId)
}
if updateFields.ObjBase != "" {
sqlUpdate += " obj_base = ?,"
args = append(args, updateFields.ObjBase)
}
....
log.Debugf("sqlUpdate : %s,args:(%#v)", sqlUpdate, args)
return
}
// GetSqlWhereStr 组装where条件和参数
func GetSqlWhereStr(ctx context.Context, wheres *infra.WhereFields) (sqlWhere string, args []interface{
}) {
}
The one used in the project is sqlx
for adding, deleting, modifying, and checking. Generally speaking, sqlx
it orm
is relatively primitive. You have to write it yourself sql
or encapsulate the method to splice it sql
. However, there are several problems with the above code.
1. The more table fields, the longer the function, and the greater the error probability.
2. If there are multiple tables, these two methods will continue to increase and become redundant.
3. If you want to update the fields to empty, such as string types. Field, the goal is to update to ""
an empty string, the above method is obviously inconsistent.
For the above code, a better way is to use builder
the generator pattern to construct an builder
object, continuously splice fields through chain calls, and finally build()
output it through methods. If you have nothing to do on the weekend, let’s refactor it.
2. Generator mode
Reference:
golang implements generator pattern 1
golang implements generator pattern 2
golang implements generator pattern 3
Goals of generator pattern:
- Separate the construction of complex objects from their representation.
- Allows the same build process to create different representations.
- Encapsulate construction logic in a separate builder class.
- Allows the creation of objects with many optional or changing parts.
2. Builder generates sql statement
1. Define the objects and builders to be generated
package mysql
import (
"strings"
)
// SqlClause 定义要生成的对象,组装表sql
type SqlClause struct {
table string
sql string
args []interface{
}
}
// 为了节省空间,具体的SelectBuilder暂时不写
type SelectInterface interface {
Select(field string) SelectInterface
Count(field string) SelectInterface
Build() (sql string, args []interface{
})
}
// 定义update语句的接口
type UpdateInterface interface {
Update(field string, value interface{
}) UpdateInterface
Build() (sql string, args []interface{
})
}
// 定义where语句的接口
type WhereInterface interface {
Where(field string, value interface{
}) WhereInterface
In(field string, valStr string) WhereInterface
Build() (sql string, args []interface{
})
}
// UpdateBuilder update语句生成器
type UpdateBuilder struct {
UpdateSql *SqlClause
WhereSql *WhereBuilder
}
func NewUpdateBuilder(table string) *UpdateBuilder {
return &UpdateBuilder{
UpdateSql: &SqlClause{
table: table,
sql: "update " + table + " set ",
args: make([]interface{
}, 0),
},
WhereSql: NewWhereBuilder(),
}
}
// 通过update语句组装
func (u *UpdateBuilder) Update(field string, value interface{
}) UpdateInterface {
u.UpdateSql.sql += " " + field + " = ?,"
u.UpdateSql.args = append(u.UpdateSql.args, value)
return u
}
// 输出完整的update语句
func (u *UpdateBuilder) Build() (sql string, args []interface{
}) {
if len(u.UpdateSql.args) < 1 {
return
}
if len(u.WhereSql.sqlClause.args) < 1 {
return
}
// 拼接sql
sql = strings.TrimRight(u.UpdateSql.sql, ", ")
args = append(args, u.UpdateSql.args...)
sql += strings.TrimRight(u.WhereSql.sqlClause.sql, "and ")
args = append(args, u.WhereSql.sqlClause.args...)
return
}
// WhereBuilder where语句生成器
type WhereBuilder struct {
sqlClause *SqlClause
}
func NewWhereBuilder() *WhereBuilder {
return &WhereBuilder{
sqlClause: &SqlClause{
sql: " where ",
args: make([]interface{
}, 0),
},
}
}
// 通用where条件组装
func (w *WhereBuilder) Where(field string, value interface{
}) WhereInterface {
w.sqlClause.sql += " " + field + " = ? and "
w.sqlClause.args = append(w.sqlClause.args, value)
return w
}
// where语句的in查询
func (w *WhereBuilder) In(field string, valStr string) WhereInterface {
fieldSlice := strings.Split(valStr, ",")
var inStr string
for i, v := range fieldSlice {
if i == 0 {
inStr = "?"
} else {
inStr += ", ?"
}
w.sqlClause.args = append(w.sqlClause.args, v)
}
w.sqlClause.sql += " " + field + " in ( " + inStr + ") and "
return w
}
// 输出结果
func (w *WhereBuilder) Build() (sql string, args []interface{
}) {
if len(w.sqlClause.args) < 1 {
return
}
// 拼接where sql
sqlWhere := strings.TrimRight(w.sqlClause.sql, "and ")
sql += sqlWhere
args = append(args, w.sqlClause.args...)
return
}
2. Each table implements details based on its own fields.
// =========================== table级别方法 =================================
// MappingTaskBuilder map表的builder,组装select,update等sql,调用方式如下:
// 完整的update sql : MappingTaskBuilder.Update().UpdatemapId().WhereTaskId().Build()
// 完整的select sql : MappingTaskBuilder.Select().SelectFields().WhereTaskId().Build()
// 单独的where sql : MappingTaskBuilder.WhereCodes().WhereTaskId().BuildWhereSql()
type MappingTaskBuilder struct {
updateBuilder *mysql.UpdateBuilder
selectBuilder *mysql.SelectBuilder
whereBuilder *mysql.WhereBuilder
}
func NewMappingTaskBuilder() *MappingTaskBuilder {
return &MappingTaskBuilder{
whereBuilder: mysql.NewWhereBuilder(),
}
}
func (m *MappingTaskBuilder) Update(table string) *MappingTaskBuilder {
m.updateBuilder = mysql.NewUpdateBuilder(table)
return m
}
func (m *MappingTaskBuilder) Select(table string) *MappingTaskBuilder {
m.selectBuilder = mysql.NewSelectBuilder(table)
return m
}
// ------- update条件,定义具体字段所属函数是为了编码的清晰
// map_id
func (m *MappingTaskBuilder) UpdateMapId(field string, value interface{
}) *MappingTaskBuilder {
m.updateBuilder.Update(field, value)
return m
}
// obj_base
func (m *MappingTaskBuilder) UpdateObjBase(field string, value interface{
}) *MappingTaskBuilder {
m.updateBuilder.Update(field, value)
return m
}
// ---- select条件,可以传单个字段,也可以一次性传输
// Select 组装select语句,selectStr为空则返回
func (m *MappingTaskBuilder) SelectFields(selectStr string) *MappingTaskBuilder {
if selectStr == "" {
return m
}
m.selectBuilder.Select(selectStr)
return m
}
// ---- where条件
// codes
func (m *MappingTaskBuilder) WhereCodes(field string, valStr string) *MappingTaskBuilder {
m.whereBuilder.In(field, valStr)
return m
}
// task_id
func (m *MappingTaskBuilder) WhereTaskId(field string, value interface{
}) *MappingTaskBuilder {
m.whereBuilder.Where(field, value)
return m
}
// --------- 输出
// 输出完整update语句
func (m *MappingTaskBuilder) BuildUpdateSql() (sql string, args []interface{
}) {
// where语句赋值
m.updateBuilder.WhereSql = m.whereBuilder
sql, args = m.updateBuilder.Build()
return
}
// 输出完整select语句
func (m *MappingTaskBuilder) BuildSelectSql() (sql string, args []interface{
}) {
// where语句赋值
m.selectBuilder.WhereSql = m.whereBuilder
sql, args = m.selectBuilder.Build()
return
}
// 输出 where的sql
func (m *MappingTaskBuilder) BuildWhereSql() (sql string, args []interface{
}) {
sql, args = m.whereBuilder.Build()
return
}
// 输出完整的sql
func (m *MappingTaskBuilder) Build() (sql string, args []interface{
}) {
if m.selectBuilder == nil && m.updateBuilder == nil {
return
}
if m.selectBuilder == nil {
return m.BuildUpdateSql()
}
if m.updateBuilder == nil {
return m.BuildSelectSql()
}
// 没有设置update和select则只输出where sql语句
return m.BuildWhereSql()
}
// Reset 重置
func (m *MappingTaskBuilder) Reset() {
m = nil
}
3. Calling method
builder := xengine.NewMappingTaskBuilder()
sqlUpdate, args = builder.Update("mapping_task").
UpdateMapId("map_id", updateFields.MapId).
UpdateObjBase("obj_base", updateFields.ObjBase).
WhereMapId("map_id", wheres.MapId).
WhereDel("del", wheres.Del).
Builder()
3. Some thoughts
After writing the above code, I found that it is orm
somewhat similar. Each table is an object, assembled and executed through chain calls sql
... Our above encapsulation is the simplest assembly sql
.
In a sense, it also shows that it is a matter of technology selection. When the business is not that complicated, although the selection is orm
a little heavier, at least the code is easy to write and maintain. It is still possible in sqlx
complex projects such as statistics, but it is a bit painful to start with dozens of lines.sql
sql
orm
Unfortunately, the project has already taken shape. Changing sql
the engine is too big a change, so we have to continuously improve the code level, otherwise code review
it will be a kind of torture. .
======== 20230715 update =============
Recently, the above code has been iterated, including adding new hash
support for field value coverage and build
unified function assembly. As well as support where
in order by,limit,group by
etc., overall orm
the operation is becoming more and more biased. . . Therefore, the benevolent sees benevolence and the wise see wisdom, and it is recommended that the matter be settled simply and sql
directly orm
.
end