[go] 图一乐,直接用 string builder 拼接 sql

403 阅读2分钟

之前接到过一个只有几个接口的项目。因为代码量太少所以不想用 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 显然是更好的选择。