golang 操作数据库,不使用 ORM 的情况下,查询数据一般使用 Scan。
网上能查到的例子,大多数是先定义一个实体,然后再通过结果集的 Scan,放到结构体的字段中。
这样做的局限性就是不通用,不同的表的查询需要定义不同的结构体,然后写不同的 Scan。
于是考虑有没有一种通用的方式,把取出来的结果集放到 Map 切片中。
终于功夫不负有心人,做了一次代码的搬运工。

本文相关代码: hellogo/database at main · bettersun/hellogo (github.com)

golang查询数据库的常用写法

// Book 书籍
type Book struct {
   BookId      int    // 书籍 ID
   Title       string // 书名
   Author      string // 作者
   PublishDate string // 出版日期

附赠建表SQL:

-- 书籍
CREATE TABLE m_book
    book_id      int8 NOT NULL, -- 书籍 ID
    title        text NOT NULL, -- 书名
    author       text NULL,     -- 作者
    total_page   int4 NULL,     -- 总页数
    publish_date date NULL,     -- 出版日期
    CONSTRAINT book_pkey PRIMARY KEY (book_id)

网上能找到的常用写法:

// RetreiveBook 查询书籍 func RetreiveBook(db *sql.DB) ([]Book, error) { var books []Book // 查询 sql := `select book_id, title, author, to_char(publish_date, 'YYYY/MM/DD') as publish_date from m_book` rows, err := db.Query(sql) if err != nil { log.Println(err) return books, err defer rows.Close() for rows.Next() { var book Book // 获取各列的值,放到对应的地址中 rows.Scan(&book.BookId, &book.Title, &book.Author, &book.PublishDate) books = append(books, book) return books, nil

这种写法的优点非常高内聚,针对单个表的查询 SQL 语句,和获取数据的过程,都写在函数里,对外只返回结构体的切片,查询完成后使用结构体变量处理复杂业务也比较方便。
缺点也很明显。一个是不同的表需要定义不同的结构体,然后写不同的 SQL 语句,然后Scan的参数也不同;另一个是如果要修改查询的字段个数,那 Scan 的参数也需要对应修改。

如果查询的表或业务比较少,可以每个表和业务都定义相关的结构体,然后写相应的查询函数。
如果要查询的表比较多,就会觉得比较重复。

如果只是查询出来的内容返回个Map,然后在前端使用的话,那更没有必要定义一堆结构体,写一堆查询函数。直接写一个通用的查询函数,查询的结果集输出为Map,就能省去很多代码。

SQL查询结果集输出到Map切片

一开始试着看 golang 的操作数据库的源码,看看有没有什么方式能实现。
发现白搭,脑子不灵光,根本看不进去,还是得依靠强大的网络。

最终实现如下:

// RetrieveMap SQL查询结果输出为Map
func RetrieveMap(db *sql.DB, sSql string) ([]map[string]interface{}, error) {
   // 准备查询语句
   stmt, err := db.Prepare(sSql)
   if err != nil {
      log.Println(err)
      return nil, err
   defer stmt.Close()
   // 查询
   rows, err := stmt.Query()
   if err != nil {
      log.Println(err)
      return nil, err
   defer rows.Close()
   // 数据列
   columns, err := rows.Columns()
   if err != nil {
      log.Println(err)
      return nil, err
   // 列的个数
   count := len(columns)
   // 返回值 Map切片
   mData := make([]map[string]interface{}, 0)
   // 一条数据的各列的值(需要指定长度为列的个数,以便获取地址)
   values := make([]interface{}, count)
   // 一条数据的各列的值的地址
   valPointers := make([]interface{}, count)
   for rows.Next() {
      // 获取各列的值的地址
      for i := 0; i < count; i++ {
         valPointers[i] = &values[i]
      // 获取各列的值,放到对应的地址中
      rows.Scan(valPointers...)
      // 一条数据的Map (列名和值的键值对)
      entry := make(map[string]interface{})
      // Map 赋值
      for i, col := range columns {
         var v interface{}
         // 值复制给val(所以Scan时指定的地址可重复使用)
         val := values[i]
         b, ok := val.([]byte)
         if ok {
            // 字符切片转为字符串
            v = string(b)
         } else {
            v = val
         entry[col] = v
      mData = append(mData, entry)
   return mData, nil

上述代码参考:
stackoverflow.com/questions/1…

主要逻辑没改,原代码直接转成了 JSON 字符串,另外个人主要是为了理解加了注释。

  • 只需要在调用函数前定义好查询用的 SQL,而且查询的字段数增加或减少都无需修改函数,比单个表分别写查询函数的方式方便很多。
  • 结果使用 Map 切片接收,Map 切片可以直接返回给前端使用。
  • 结果使用 Map 切片接收也是双刃剑,查询后的业务处理复杂的情况下,使用 Map 会比较麻烦。
  • 调用前创建 DB 连接,然后定义好查询用的 SQL。
    这里使用的是 postgre 数据库。

    import (
       "database/sql"
       "log"
       "testing"
       _ "github.com/lib/pq"
    func TestRetrieveMap(t *testing.T) {
       db, err := sql.Open("postgres", "host=127.0.0.1 port=5432 user=postgres password=12 dbname=postgres sslmode=disable")
       defer db.Close()
       if err != nil {
          log.Fatalln("无法连接数据库")
       // 查询书籍
       sql := "select book_id, title, author, to_char(publish_date, 'YYYY/MM/DD') as publish_date from m_book"
       m, err := RetrieveMap(db, sql)
       if err != nil {
          log.Println(err)
       log.Printf("%v", m)
    

    查询结果例:

    === RUN   TestRetrieveMap
    2021/11/05 12:02:06 [map[author:司马迁 book_id:1 publish_date:2020/12/12 title:史记] map[author:当年明月 book_id:2 publish_date:2020/09/12 title:明朝那些事儿]]
    --- PASS: TestRetrieveMap (0.03s)
    复制代码
    分类:
    后端
    标签: