登录
首页 >  Golang >  Go问答

在 Go 中推广 *sql.Rows 的遍历

来源:stackoverflow

时间:2024-02-18 20:18:24 358浏览 收藏

从现在开始,我们要努力学习啦!今天我给大家带来《在 Go 中推广 *sql.Rows 的遍历》,感兴趣的朋友请继续看下去吧!下文中的内容我们主要会涉及到等等知识点,如果在阅读本文过程中有遇到不清楚的地方,欢迎留言呀!我们一起讨论,一起学习!

问题内容

我正在使用go开发一个web api,有很多冗余的数据库查询扫描代码。

func (m *contractmodel) workquestions(cid int) ([]models.workquestion, error) {
    results, err := m.db.query(queries.work_questions, cid)
    if err != nil {
        return nil, err
    }

    var workquestions []models.workquestion
    for results.next() {
        var wq models.workquestion
        err = results.scan(&wq.contractstateid, &wq.questionid, &wq.question, &wq.id, &wq.answer, &wq.compulsory)
        if err != nil {
            return nil, err
        }
        workquestions = append(workquestions, wq)
    }

    return workquestions, nil
}

func (m *contractmodel) questions(cid int) ([]models.question, error) {
    results, err := m.db.query(queries.questions, cid)
    if err != nil {
        return nil, err
    }

    var questions []models.question
    for results.next() {
        var q models.question
        err = results.scan(&q.question, &q.answer)
        if err != nil {
            return nil, err
        }
        questions = append(questions, q)
    }

    return questions, nil
}

func (m *contractmodel) documents(cid int) ([]models.document, error) {
    results, err := m.db.query(queries.documents, cid)
    if err != nil {
        return nil, err
    }

    var documents []models.document
    for results.next() {
        var d models.document
        err = results.scan(&d.document, &d.s3region, &d.s3bucket, &d.source)
        if err != nil {
            return nil, err
        }
        documents = append(documents, d)
    }

    return documents, nil
}

我需要概括此代码,以便可以将结果 *sql.rows 传递给函数并获取包含扫描行的结构切片。我知道 sqlx 包中有一个 structscan 方法,但无法使用该方法,因为我有大量使用 go 标准 database/sql 包编写的代码。

使用 reflect 包,我可以创建通用的 structscan 函数,但 reflect 包无法从传递的 interface{} 类型创建结构体切片。我需要实现的目标如下

func RowsToStructs(rows *sql.Rows, model interface{}) ([]interface{}, error) {
    // 1. Create a slice of structs from the passed struct type of model
    // 2. Loop through each row,
    // 3. Create a struct of passed mode interface{} type
    // 4. Scan the row results to a slice of interface{}
    // 5. Set the field values of struct created in step 3 using the slice in step 4
    // 6. Add the struct created in step 3 to slice created in step 1
    // 7. Return the struct slice
}

我似乎找不到一种方法来扫描作为模型参数传递的结构并使用反射包创建它的一个切片。有没有解决方法,或者我是否以错误的方式看待这个问题?

结构体字段具有从结果返回的正确列数和正确的顺序


解决方案


您可以通过将指向目标切片的指针作为参数传递来避免在调用函数中使用类型断言。这是经过修改的 rowstostructs:

// rowstostructs scans rows to the slice pointed to by dest.
// the slice elements must be pointers to structs with exported
// fields corresponding to the the columns in the result set.
//
// the function panics if dest is not as described above.
func rowstostructs(rows *sql.rows, dest interface{}) error {

    // 1. create a slice of structs from the passed struct type of model
    //
    // not needed, the caller passes pointer to destination slice.
    // elem() dereferences the pointer.
    //
    // if you do need to create the slice in this function
    // instead of using the argument, then use
    // destv := reflect.makeslice(reflect.typeof(model).

    destv := reflect.valueof(dest).elem()

    // allocate argument slice once before the loop.

    args := make([]interface{}, destv.type().elem().numfield())

    // 2. loop through each row

    for rows.next() {

        // 3. create a struct of passed mode interface{} type
        rowp := reflect.new(destv.type().elem())
        rowv := rowp.elem()

        // 4. scan the row results to a slice of interface{}
        // 5. set the field values of struct created in step 3 using the slice in step 4
        //
        // scan directly to the struct fields so the database
        // package handles the conversion from database
        // types to a go types.
        //
        // the slice args is filled with pointers to struct fields.

        for i := 0; i < rowv.numfield(); i++ {
            args[i] = rowv.field(i).addr().interface()
        }

        if err := rows.scan(args...); err != nil {
            return err
        }

        // 6. add the struct created in step 3 to slice created in step 1

        destv.set(reflect.append(destv, rowv))

    }
    return nil
}

这样称呼它:

func (m *contractmodel) documents(cid int) ([]*models.document, error) {
    results, err := m.db.query(queries.documents, cid)
    if err != nil {
        return nil, err
    }
    defer results.close()
    var documents []*models.document
    err := rowstostruct(results, &documents)
    return documents, err
}

通过将查询移至辅助函数来消除更多样板:

func querytostructs(dest interface{}, db *sql.db, q string, args ...interface{}) error {
    rows, err := db.query(q, args...)
    if err != nil {
        return err
    }
    defer rows.close()
    return rowstostructs(rows, dest)
}

这样称呼它:

func (m *contractmodel) documents(cid int) ([]*models.document, error) {
    var documents []*model.document
    err := querytostructs(&documents, m.db, queries.documents, cid)
    return documents, err
}

我处理“通用代码”的建议非常简单,主要依赖于这样的前提:检索/获取操作大多数时候用于将这些结果返回到 web 浏览器客户端。因此,这些结果反过来会利用 json 包转换为 json。

文件:db_common.go

// interface for things that performs an scan over a given row.
// actually it is a common interface for https://pkg.go.dev/database/sql#rows.scan and https://pkg.go.dev/database/sql#row.scan
type rowscanner interface {
    scan(dest ...interface{}) error
}

// scans a single row from a given query
type rowscanfunc func(rows rowscanner) (interface{}, error)

// scans multiples rows using a scanner function in order to build a new "scanable" struct
func scanmultiples(rows *sql.rows, rowscanfunc rowscanfunc) ([]interface{}, error) {
    scaneables := []interface{}{}
    for rows.next() {
        scanable, err := rowscanfunc(rows)
        if scanable == nil {
            return nil, err
        }
        scaneables = append(scaneables, scanable)
    }
    err := rows.err()
    if err != nil {
        return nil, err
    }
    return scaneables, nil
}

然后我像这样使用了上面的抽象:

文件:dao/crud_operations.go

// Type that models a row of a given db table
type TableRow struct {
    Id int
    Name string
    // more fields...
}

func GetAll() ([]interface{}, error) {
    rows, err := Db.Query(`
        SELECT id, name
        FROM a_table`
    if err != nil {
        return nil, err
    }
    defer rows.Close()
    return ScanMultiples(rows, scanRow)
}

// Gets an specific legal entity by id
func GetById(id int) (interface{}, error) {
    row := Db.QueryRow(`
        SELECT id, name
        FROM a_table
        WHERE id = $1`, id)
    return scanRow(row)
}


// Scans a row interpreting it as 'TableRow' struct
func scanRow(rows RowScanner) (*TableRow, error) { // DO NOTE that implements the RowScanner interface!
    var tableRow TableRow

    err := rows.Scan(tableRow.Id,tableRow.Name)
    if err != nil {
        return nil, err
    }
    return &tableRow, nil
}
  • 好处是您只需编写一个扫描行的函数即可。
  • 另一个好处是代码不会与外部依赖项纠缠在一起。
  • 另一个好处是代码不依赖 reflect 包或那些复杂的东西。
  • 坏事是您返回了 interface{},并且您可能必须将其转换为原始数据类型,为此必须编写 3 行(在最坏的情况下)。
  • 同样,您不必担心结果是否会像 bytes, err := json.marshal(results) 那样进行封送,因为 json 包接收 interface{} (=any) 作为参数.

以上就是《在 Go 中推广 *sql.Rows 的遍历》的详细内容,更多关于的资料请关注golang学习网公众号!

声明:本文转载于:stackoverflow 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>