如何将数据库行转换为结构体

112

假设我有一个结构体:

type User struct {
    Name  string
    Id    int
    Score int
}

如何将数据库行解析为结构体?假设有一个与数据库表具有相同模式的数据库表。我在下面添加了一个答案,但不确定它是否是最好的。

8个回答

116

Go语言包的测试通常提供了做事情的方法。例如,从 database/sql/sql_test.go 中可以得到以下提示:

func TestQuery(t *testing.T) {
    /* . . . */
    rows, err := db.Query("SELECT|people|age,name|")
    if err != nil {
            t.Fatalf("Query: %v", err)
    }
    type row struct {
            age  int
            name string
    }
    got := []row{}
    for rows.Next() {
            var r row
            err = rows.Scan(&r.age, &r.name)
            if err != nil {
                    t.Fatalf("Scan: %v", err)
            }
            got = append(got, r)
    }
    /* . . . */
}

func TestQueryRow(t *testing.T) {
    /* . . . */
    var name string
    var age int
    var birthday time.Time
    err := db.QueryRow("SELECT|people|age,name|age=?", 3).Scan(&age)
    /* . . . */
}

对于您的问题,将一行查询到结构中,翻译成类似以下内容:

var row struct {
    age  int
    name string
}
err = db.QueryRow("SELECT|people|age,name|age=?", 3).Scan(&row.age, &row.name)

我知道这看起来与你的解决方案类似,但重要的是展示如何找到解决方案。


121
如果手动将列与结构体字段绑定是简单的方法,那么我想知道困难的方法是什么。 - Anthony Hunt
19
很不幸,特别是在处理较大的结构体时,手动绑定结构体属性非常不方便,这完全是失败的... 使用jmoiron/sqlx或其他一些库会更加高效... - shadyyx
我一直从rows.Scan()中得不到任何东西。所有变量都被设置为空。 - filthy_wizard
28
这里的竖线语法是什么鬼?“SELECT|people”?我在 godocs 中没有看到任何相关参考。 - Brian
20
我和Brian一样感到困惑。结果发现这是一个假的驱动程序,纯粹用于测试SQL/数据库 (https://golang.org/src/database/sql/fakedb_test.go)。我本来很希望能在新代码中使用它! - Trey Stout

74

我推荐 github.com/jmoiron/sqlx

来自README:

sqlx 是一个库,它在go标准的 database/sql 库上提供了一组扩展。sqlx 版本的 sql.DBsql.TXsql.Stmt 等保留了底层接口不变,因此它们的接口是标准接口的超集。这使得与使用 database/sql 的现有代码库集成相对容易。

主要的附加概念包括:

  • 将行解析到结构体(带嵌入式结构体支持)、映射和切片中
  • 支持命名参数,包括预处理语句
  • GetSelect 快速从查询到结构体/切片

README 还包括一个代码片段,演示如何将行扫描到结构体中:

type Place struct {
    Country       string
    City          sql.NullString
    TelephoneCode int `db:"telcode"`
}
// Loop through rows using only one struct
place := Place{}
rows, err := db.Queryx("SELECT * FROM place")
for rows.Next() {
    err := rows.StructScan(&place)
    if err != nil {
        log.Fatalln(err)
    } 
    fmt.Printf("%#v\n", place)
}

请注意,我们不必手动将每一列映射到结构体的字段上。sqlx有一些默认映射规则来将数据库列映射到结构体的字段上,并且可以使用标记指定数据库列(请注意上面Place结构体中的TelephoneCode字段)。您可以在文档中了解更多相关信息。


老问题,但这可能会在未来帮助某些人:http://jmoiron.github.io/sqlx/#safety - Bruno Luiz K.

45

这里有一种方法 - 只需在Scan函数中手动分配所有结构值。

func getUser(name string) (*User, error) {
    var u User
    // this calls sql.Open, etc.
    db := getConnection()
    // note the below syntax only works for postgres
    err := db.QueryRow("SELECT * FROM users WHERE name = $1", name).Scan(&u.Id, &u.Name, &u.Score)
    if err != nil {
        return &User{}, err
    } else {
        return &u, nil
    }
}

1
@eslammostafa,在哪个点上这段代码可能会出现NULL值的问题? - deFreitas
@deFreitas 在这里举个例子,我指的是来自数据库的空值。 - shehata
@eslammostafa 我理解,这确实很痛苦。 - deFreitas
@deFreitas 目前我使用 sql.NullString 和 sql.NullInt64 等来处理空值,虽然需要额外的工作。 - shehata
@eslammostafa,问题在于如果您使用sql.NullString并将结构体转换为JSON,则生成的内容不够友好,需要使用VO或类似的东西。 - deFreitas
有一种方法可以解决这个问题,就是为您的结构编写一个JSONMarshal,或者只需使用一个名为Null的包,它可以帮助处理json、yaml等中的sql nulls。 - shehata

7
rows, err := connection.Query("SELECT `id`, `username`, `email` FROM `users`")

if err != nil {
    panic(err.Error())
}

for rows.Next() {
    var user User

    if err := rows.Scan(&user.Id, &user.Username, &user.Email); err != nil {
        log.Println(err.Error())
    }

    users = append(users, user)
}

Full example


6

这里有一个专门用于此目的的库:scany

您可以像这样使用它:

type User struct {
    Name  string
    Id    int
    Score int
}

// db is your *sql.DB instance
// ctx is your current context.Context instance

// Use sqlscan.Select to query multiple records.
var users []*User
sqlscan.Select(ctx, db, &users, `SELECT name, id, score FROM users`)

// Use sqlscan.Get to query exactly one record.
var user User
sqlscan.Get(ctx, db, &user, `SELECT name, id, score FROM users WHERE id=123`)

这个库的文档详尽,易于使用。

免责声明:我是该库的作者。


1

有一个专门的包可以解决这个问题: sqlstruct

不幸的是,上次我检查时它不支持嵌入式结构(但你可以自己实现 - 我在几小时内就有了一个可用的原型)。

我刚刚提交了对sqlstruct所做的更改。


0

这里已经有一些很好的答案了,我想再补充一个我专门为解决在最常用的Golang库上执行简单查询时的复杂性问题而编写的SQL库;它叫做KSQL(不是Kafka的那个,K代表Keep It Stupid Simple,即保持简单愚蠢)。

使用它,你可以像这样将用户扫描到一个结构体中:

import (
    "context"
    "fmt"

    "github.com/vingarcia/ksql"
    "github.com/vingarcia/ksql/adapters/kpgx"
)

type User struct {
    Id    int    `ksql:"id"`
    Name  string `ksql:"name"`
    Score int    `ksql:"score"`
}

// Just a simple struct containing the table name and
// the name of the ID column or columns:
var UsersTable = ksql.NewTable("users", "id")

func main() {
    ctx := context.Background()
    dsn := "host=localhost user=postgres password=postgres dbname=postgres port=5432 sslmode=disable TimeZone=UTC"
    db, err := kpgx.New(ctx, dsn, ksql.Config{})
    if err != nil {
        panic("failed to connect database")
    }

    // Lets insert a user so the query below has something to return:
    err = db.Insert(ctx, UsersTable, &User{
        Name:  "SomeUser",
        Score: 42,
    })
    if err != nil {
        panic(err)
    }

    var user User
    err = db.QueryOne(ctx, &user, "FROM users WHERE name = $1", "SomeUser")
    if err != nil {
        panic(err)
    }

    fmt.Printf("%+v\n", user) // {Id:1 Name:SomeUser Score:42}
}

-1

使用: go-models-mysql sqlbuilder

val, err = m.ScanRowType(row, (*UserTb)(nil))

或者完整的代码

import (
    "database/sql"
    "fmt"

    lib "github.com/eehsiao/go-models-lib"
    mysql "github.com/eehsiao/go-models-mysql"
)

// MyUserDao : extend from mysql.Dao
type MyUserDao struct {
    *mysql.Dao
}

// UserTb : sql table struct that to store into mysql
type UserTb struct {
    Name       sql.NullString `TbField:"Name"`
    Id         int            `TbField:"Id"`
    Score      int            `TbField:"Score"`
}

// GetFirstUser : this is a data logical function, you can write more logical in there
// sample data logical function to get the first user
func (m *MyUserDao) GetFirstUser() (user *User, err error) {

    m.Select("Name", "Id", "Score").From("user").Limit(1)
    fmt.Println("GetFirstUser", m.BuildSelectSQL().BuildedSQL())
    var (
        val interface{}
        row *sql.Row
    )

    if row, err = m.GetRow(); err == nil {
        if val, err = m.ScanRowType(row, (*UserTb)(nil)); err == nil {
            u, _ := val.(*UserTb)

            user = &User{
                Name:       lib.Iif(u.Name.Valid, u.Nae.String, "").(string),
                Id:         u.Id,
                Score:      u.Score,
            }
        }
    }
    row, val = nil, nil

    return
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接