如何在Go DRY中扫描数据库行?

时间:2018-11-06 16:16:37

标签: database go

我在数据库中有一个包含用户帐户信息的表。我有一个称为用户定义的结构。

type User struct {
  Id        uint
  Username  string
  Password  string
  FirstName string
  LastName  string
  Address1  string
  Address2  string
  .... a bunch more fields ...
}

为获取单个用户帐户,我定义了一个方法

func (user *User) GetById(db *sql.DB, id uint) error {
  query := `SELECT 
            ...a whole bunch of SQL ...
            WHERE id = $1
            ... more SQL ...
            LIMIT 1`
  row := db.QueryRow(query, id)
  err := row.Scan(
    &user.Id,
    &user.UserName,
    &user.Password,
    &user.FirstName,
    &user.LastName,
    ... some 20 more lines of fields read into the struct ...
  )
  if err != nil {
    return err
  }
  return nil
}

在系统中有几个地方需要作为更大查询的一部分来获取用户信息。也就是说,我要获取其他类型的对象,但还要获取与此对象相关的用户帐户。

这意味着,我必须一遍又一遍地重复整个rows.Scan(&user.Username, &user...)的事情,并且要花费整个页面,而且容易出错,如果我更改用户表结构,则必须更改其中的代码。一大堆地方。我该如何使它更干燥?

编辑:我不确定为什么将其标记为重复,但是由于需要进行此编辑,因此我将尝试再解释一次。我不是在问如何将一行扫描到一个结构中。正如上面的代码清楚显示的那样,我已经知道该怎么做。我在问如何构造结构扫描代码,以免每次扫描相同类型的结构时都不必重复扫描代码的同一页面。

编辑:也是,是的,我知道sqlstruct和sqlx以及类似的库。我故意避免使用这些方法,因为它们依赖于具有详细记录的性能问题的反射包。而且我打算使用这些技术来扫描数百万行(不是数百万用户,但是这个问题扩展到其他记录类型)。

编辑:是的,我知道我应该编写一个函数。我不确定该函数应采用什么参数以及应返回什么结果。可以说,我要容纳的另一个查询看起来像这样

SELECT
    s.id,
    s.name,
    ... more site fields ...
    u.id,
    u.username,
    ... more user fields ...
FROM site AS s
JOIN user AS u ON (u.id = s.user_id)
JOIN some_other_table AS st1 ON (site.id = st1.site_id)
... more SQL ...

我有一个嵌入用户结构的站点结构方法。我不想在这里重复用户扫描代码。我想调用一个函数,该函数会将原始文件的用户部分扫描到用户结构中,就像在上述用户方法中一样。

1 个答案:

答案 0 :(得分:1)

要消除重复扫描*sql.Rows结构所需的步骤,可以引入两个接口。描述*sql.Rows*sql.Row的已实现行为的代码。

// This interface is already implemented by *sql.Rows and *sql.Row.
type Row interface {
    Scan(...interface{}) error
}

另一种抽象出该行的实际扫描步骤。

// have your entity types implement this one
type RowScanner interface {
    ScanRow(Row) error
}

RowScanner界面的示例实现如下所示:

type User struct {
    Id       uint
    Username string
    // ...
}

// Implements RowScanner
func (u *User) ScanRow(r Row) error {
    return r.Scan(
        &u.Id,
        &u.Username,
        // ...
    )
}

type UserList struct {
    Items []*User
}

// Implements RowScanner
func (list *UserList) ScanRow(r Row) error {
    u := new(User)
    if err := u.ScanRow(r); err != nil {
        return err
    }
    list.Items = append(list.Items, u)
    return nil
}

使用这些接口,您现在可以使用这两个功能来为实现RowScanner接口的所有类型的行扫描代码干燥。

func queryRows(query string, rs RowScanner, params ...interface{}) error {
    rows, err := db.Query(query, params...)
    if err != nil {
        return err
    }
    defer rows.Close()

    for rows.Next() {
        if err := rs.ScanRow(rows); err != nil {
            return err
        }
    }
    return rows.Err()
}

func queryRow(query string, rs RowScanner, params ...interface{}) error {
    return rs.ScanRow(db.QueryRow(query, params...))
}

// example
ulist := new(UserList)
if err := queryRows(queryString, ulist, arg1, arg2); err != nil {
    panic(err)
}

// or
u := new(User)
if err := queryRow(queryString, u, arg1, arg2); err != nil {
    panic(err)
}

如果您有要扫描的复合类型,但又不想避免重复其元素字段的枚举,则可以引入一种返回类型字段的方法,并在需要时重用该方法。例如:

func (u *User) ScannableFields() []interface{} {
    return []interface{}{
        &u.Id,
        &u.Username,
        // ...
    }
}

func (u *User) ScanRow(r Row) error {
    return r.Scan(u.ScannableFields()...)
}

// your other entity type
type Site struct {
    Id   uint
    Name string
    // ...
}

func (s *Site) ScannableFields() []interface{} {
    return []interface{}{
        &p.Id,
        &p.Name,
        // ...
    }
}

// Implements RowScanner
func (s *Site) ScanRow(r Row) error {
    return r.Scan(s.ScannableFields()...)
}

// your composite
type UserWithSite struct {
    User *User
    Site *Site
}

// Implements RowScanner
func (u *UserWithSite) ScanRow(r Row) error {
    u.User = new(User)
    u.Site = new(Site)
    fields := append(u.User.ScannableFields(), u.Site.ScannableFields()...)
    return r.Scan(fields...)
}

// retrieve from db
u := new(UserWithSite)
if err := queryRow(queryString, u, arg1, arg2); err != nil {
    panic(err)
}