之前接到过一个只有几个接口的项目。因为代码量太少所以不想用 orm。不过确实还需要动态拼接 sql。所以就直接用了 strings.Builder。结果发现写出来的代码还挺有意思的。
代码
INSERT 语句大概长这样:
// 表实体
type TestTableName struct {
Id uint // 主键
FieldOne int // 字段一
FieldTwo string // 字段二
CreatedAt sql.NullTime // 创建于
}
// 创建数据
func createData(db *sql.DB, data []TestTableName) error {
n := time.Now()
var s strings.Builder
s.WriteString("INSERT INTO test_table_name ")
s.WriteString("(field_one,field_two,created_at) ")
s.WriteString("VALUES ")
args := make([]any, 0, len(data)*3)
for i, v := range data {
if i != 0 {
s.WriteRune(',')
}
s.WriteString("(?,?,?)")
args = append(args, v.FieldOne, v.FieldTwo, n)
}
_, err := db.Exec(s.String(), args...)
if err != nil {
return err
}
return nil
}
可以看到,用 strings.Builder 写出来的代码总体上还是比较直观的。只不过代码风格偏命令式。
这是 SELECT 语句的代码:
// 查询条件
type Query struct {
FieldOne json.Number // 字段一
FieldTwo string // 字段二
Current int // 当前页
PageSize int // 每页的数量
}
// 查询数据
func fetchData(db *sql.DB, arg Query) ([]TestTableName, error) {
var s strings.Builder
s.WriteString("SELECT ")
s.WriteString("id,field_one,field_two,created_at ")
s.WriteString("FROM test_table_name")
var args []any
var b bool
if i, _ := arg.FieldOne.Int64(); i != 0 {
b = true
s.WriteString(" WHERE ")
s.WriteString("field_one = ?")
args = append(args, i)
}
if arg.FieldTwo != "" {
if !b {
b = true
s.WriteString(" WHERE ")
} else {
s.WriteString(" AND ")
}
s.WriteString("field_two LIKE ?")
args = append(args, "%"+arg.FieldTwo+"%")
}
s.WriteString(" LIMIT ? OFFSET ?")
offset := (arg.Current - 1) * arg.PageSize
args = append(args, arg.PageSize, offset)
rows, err := db.Query(s.String(), args...)
if err != nil {
return nil, err
}
defer rows.Close()
var data []TestTableName
for rows.Next() {
var v TestTableName
err := rows.Scan(&v.Id, &v.FieldOne, &v.FieldTwo, &v.CreatedAt)
if err != nil {
return nil, err
}
data = append(data, v)
}
return data, nil
}
查询语句的逻辑会复杂一点。可以看到在处理 where 条件的时候确实有点乱。不过好在查询条件比较少,代码整体还能接受。
上面的代码可以去这里获取。例子里用到的 mysql 和表结构是用 docker compose 维护的。感兴趣的话可以自己 clone 代码运行一下。
扩展
上面的代码偏命令式。构造 sql 的代码与业务字段耦合在一起,没有什么抽象。一是代码冗长难以阅读。二是查询 sql 的构造逻辑无法复用。那么我们是否可以抽取 sql 的构造逻辑,将代码改成这样呢?
type Clause struct {
Selects []string
Table string
Where []string
Limit int
Offset int
Args []any
}
func (c *Clause) String() string {
var b strings.Builder
b.WriteString("SELECT ")
for i, v := range c.Selects {
if i != 0 {
b.WriteByte(',')
}
b.WriteString(v)
}
b.WriteByte(' ')
b.WriteString("FROM ")
b.WriteString(c.Table)
for i, v := range c.Where {
if i == 0 {
b.WriteString(" WHERE ")
} else {
b.WriteString(" AND ")
}
b.WriteString(v)
}
b.WriteByte(' ')
if c.Limit != 0 {
b.WriteString("LIMIT ? OFFSET ?")
c.Args = append(c.Args, c.Limit, c.Offset)
}
return b.String()
}
更进一步地,可不可以将 select、from、where 等子句分到不同的函数里面去呢?再进一步地,可不可以让每个子句都改成独立的结构体呢?可以。但在这种情况下用 gorm 显然是更好的选择。