本文翻译自《Querying for data》。
目录
执行返回行数据的SQL语句时,请使用database/sql
包中提供的Query
方法之一。其中每一个都返回一行或多行,你可以使用Scan
方法将其数据复制到变量中。例如,你可以使用这些方法来执行SELECT
语句。
执行不返回行数据的语句,可以改用Exec
或ExecContext
方法。有关详细信息,请参阅执行不返回数据的SQL语句。
database/sql
包提供了两种执行查询并返回结果的方法:
- 查询单行–
QueryRow
方法从数据库中最多返回一行作为一个Row
结构体。有关详细信息,请参阅查询单行。 - 查询多行–
Query
方法返回所有匹配的行作为代码可以循环的Rows
结构体。有关详细信息,请参阅查询多行。
如果你的代码将重复执行相同的SQL语句,请考虑使用准备好的语句(译者注:预处理语句)。有关详细信息,请参阅使用准备好的语句。
注意:不要使用fmt.Sprintf
等字符串拼接函数来组装SQL语句!否则你可能会引入SQL注入风险。有关更多信息,请参阅避免SQL注入风险。
查询单行
QueryRow
函数最多检索单个数据库行,例如通过唯一ID查找数据时。如果查询返回多行,则Scan
方法会丢弃除第一行以外的所有行。
QueryRowContext
函数的工作方式类似于QueryRow
,但带有context.Context
参数。有关更多信息,请参阅取消正在进行的数据库操作。
以下示例使用查询来确定是否有足够的库存来支持购买。如果足够,则SQL语句返回true
,否则返回false
。Row.Scan
方法通过指针将布尔返回值复制到enough
变量中。
func canPurchase(id int, quantity int) (bool, error) {
var enough bool
// 查询基于单行的值。
if err := db.QueryRow("SELECT (quantity >= ?) from album where id = ?",
quantity, id).Scan(&enough); err != nil {
if err == sql.ErrNoRows {
return false, fmt.Errorf("canPurchase %d: unknown album", id)
}
return false, fmt.Errorf("canPurchase %d: %v", id)
}
return enough, nil
}
注意:预处理语句中的参数占位符因所使用的DBMS和驱动程序而异。例如,Postgres的pq驱动程序需要像$1
这样的占位符,而MySQL驱动程序使用?
号。
处理错误
QueryRow
函数本身不返回错误。相反,Scan
方法会报告查找和扫描中的任何错误。当查询未找到任何行时,它返回sql.ErrNoRows
错误。
以下列出返回单行的函数
单独运行单行查询。
在较大的事务中运行单行查询。有关更多信息,请参阅执行事务。
使用一个准备好的语句运行单行查询。有关更多信息,请参阅使用准备好的语句。
与专用的数据库连接一起使用。有关更多信息,请参阅管理数据库连接。
查询多行
你可以使用Query
或QueryContext
函数查询多行,它们返回表示查询结果的Rows
结构体。你的代码使用rows.Next
方法对返回的行进行迭代。每次迭代都调用Scan
方法将列值复制到变量中。
QueryContext
函数的工作方式与Query
类似,但带有context.Context
参数。有关更多信息,请参阅取消正在进行中的数据库操作。
以下示例执行查询以返回指定艺术家(artist
)的专辑(album
)信息。album
在sql.Rows
中返回。以下代码使用Rows.Scan
函数将列值复制到由指针指出的变量中。
func albumsByArtist(artist string) ([]Album, error) {
rows, err := db.Query("SELECT * FROM album WHERE artist = ?", artist)
if err != nil {
return nil, err
}
defer rows.Close()
// 一个切片用来存储返回的行数据。
var albums []Album
// 循环遍历行数据,使用Scan方法把每一行的各列的值,赋值给一个结构体变量的对应字段。Loop through rows, using Scan to assign column data to struct fields.
for rows.Next() {
var alb Album
if err := rows.Scan(&alb.ID, &alb.Title, &alb.Artist,
&alb.Price, &alb.Quantity); err != nil {
return albums, err
}
albums = append(albums, album)
}
if err = rows.Err(); err != nil {
return albums, err
}
return albums, nil
}
请注意对rows.Close
函数的延迟调用。无论函数如何返回,这都会释放sql.Rows
占有的所有资源。虽然循环遍历完毕所有的行也会隐式关闭它,但最好使用defer
来确保无论如何都能关闭。
注意:预处理语句中的参数占位符因所使用的DBMS和驱动程序而异。例如,Postgres的pq驱动程序需要像$1
这样的占位符,而MySQL驱动程序使用?
号。
处理错误
请务必在遍历查询结果后检查sql.Rows
中是否有错误发生。如果查询失败,这就是你的代码发现错误的方式。
以下列出返回多行数据的函数
单独运行查询。
在较大的事务中执行查询。有关更多信息,请参阅执行事务。
使用一个准备好的语句运行查询。有关更多信息,请参阅使用准备好的语句。
与专用的数据库连接一起使用。有关更多信息,请参阅管理数据库连接。
处理可null列的值
当列的值可能为null时,database/sql
包提供了几种特殊类型,可以用作Scan
函数的参数。每个特殊类型都包括一个Valid
字段,该字段报告列值是否为非null,如果为非null则该类型有另一个字段包含该列值。
以下示例中的代码用于查询客户名称。如果名称值为null,则代码将用另一个值替换它,以在应用程序中使用。
var s sql.NullString
err := db.QueryRow("SELECT name FROM customer WHERE id = ?", id).Scan(&s)
if err != nil {
log.Fatal(err)
}
name := "Valued Customer"
// 如果客户名称不为null(合法,s.Valid的值为true),那么赋值给name变量
if s.Valid {
name = s.String
}
请参阅sql包关于每个特殊类型的更多信息:
从列中获取数据
在查询返回的行上循环遍历时,可以使用Scan
函数将行的列值复制到Go值中,例如rows.Scan
的参考文档中所述。
所有驱动程序都支持一组基本的数据类型转换,例如将SQL的INT
转换为Go的INT
。一些驱动程序扩展了这组转换;有关详细信息,请参阅每个驱动程序的文档。
正如你所料,Scan
函数将把列类型转换为类似的Go类型。例如,Scan
将把SQL的CHAR
、VARCHAR
和TEXT
转换为Go字符串。但是,Scan
还可以把列值转换为另一个非常适合的Go类型。例如,如果列是始终包含数字的VARCHAR
,则可以指定数字Go类型(例如int
)来接收值,Scan将使用strconv.Atoi
函数进行转换。
有关Scan
函数进行类型转换的更多详细信息,请参阅Rows.Scan
参考文档。
处理多个结果集
当数据库操作可能返回多个结果集时,你可以使用Rows.NextResultSet
函数来检索这些结果集。例如,当你发送分别查询多个表的一个SQL语句,并为其中每个表都返回一个结果集时,这可能很有用。
Rows.NextResultSet
函数会准备下一个结果集,以便调用Rows.Next
函数从下一个集合中检索第一行。它返回一个布尔值,指示是否存在下一个结果集。
以下示例中的代码使用DB.Query
函数来执行两条Select
语句。第一个结果集来自第一个Select
查询,检索相簿album
表中的所有行。下一个结果集来自第二个Select
查询,从歌曲表song
中检索行。
rows, err := db.Query("SELECT * from album; SELECT * from song;")
if err != nil {
log.Fatal(err)
}
defer rows.Close()
// 循环遍历第一个结果集。
for rows.Next() {
// 处理结果集…
}
// 前进到下一个结果集。
rows.NextResultSet()
// 循环遍历第二个结果集。
for rows.Next() {
// 处理结果集…
}
// 检查在处理所有结果集的过程中,是否有错误发生。
if err := rows.Err(); err != nil {
log.Fatal(err)
}