如何利用 Go 标准库的 database/sql 包来读取多条数据
在查询 SQL 记录时,我发现您通常属于以下三种使用情况之一:
- 您要检索一条记录。例如,您可能想要查找特定用户。 (请参阅本文)
- 您想从同一表(或通过联接)检索许多记录。例如,您可能需要由特定用户创建的所有评论。
- 您对多个结果集感兴趣。这种用例很少见,但是当您要在查询之间使用一些中间数据时,通常会弹出该用例。例如,您想创建一个具有特定属性的临时用户表,然后查询有关该用户的许多信息。
直到最近,Go 的 database / sql
程序包仅支持前两个用例。要实现最后一个,您将需要在查询之间来回传递数据(或构造不同的 SQL 查询)。在 Go 1.8 中,添加了对多个结果集的支持,这使此过程变得更加简单。
在本文中,我们将介绍第二个用例-查询多个记录。
本文所需的数据
本文假设您在 Postgres 数据库中只有一个名为 users
的表,以及一些要查询的记录。它还假设您已在代码中与数据库建立了有效的连接(如果您需要与此相关的任何帮助,请参阅以前的帖子)。
您可以使用以下 SQL 代码创建一个表,并插入一些记录(如果还没有的话)。
CREATE TABLE users (
id SERIAL PRIMARY KEY,
age INT,
first_name TEXT,
last_name TEXT,
email TEXT UNIQUE NOT NULL
);
INSERT INTO users (age, email, first_name, last_name)
VALUES (30, 'jon@calhoun.io', 'Jonathan', 'Calhoun');
INSERT INTO users (age, email, first_name, last_name)
VALUES (52, 'bob@smith.io', 'Bob', 'Smith');
INSERT INTO users (age, email, first_name, last_name)
VALUES (15, 'jerryjr123@gmail.com', 'Jerry', 'Seinfeld');
查询多条记录
在上一篇文章中,我们讨论了如何使用 QueryRow()
方法查询单条记录。我们将在此处介绍的大多数内容非常相似,但不是使用 QueryRow()
方法,而是使用 Query() 方法。
让我们直接跳到一个示例,然后解释一下是怎么回事。
rows, err := db.Query("SELECT id, first_name FROM users LIMIT $1", 3)
if err != nil {
// handle this error better than this
panic(err)
}
defer rows.Close()
for rows.Next() {
var id int
var firstName string
err = rows.Scan(&id, &firstName)
if err != nil {
// handle this error
panic(err)
}
fmt.Println(id, firstName)
}
// get any error encountered during iteration
err = rows.Err()
if err != nil {
panic(err)
}
就像 QueryRow()
一样,这允许我们传递参数并避免任何形式的 SQL 注入。这是我们在 SQL 语句的 $ 1
部分的第一行中执行的操作,然后将 3
参数传递给 Query()
方法。
QueryRow()
和 Query()
之间的主要区别之一是如何处理错误。使用 QueryRow()
,将错误处理推迟到您调用 Scan()
时,发现 0 条记录时将返回错误。Query()
的行为与此非常不同。
对于初学者来说,当找不到记录时,它不会返回错误。您可以通过将 LIMIT 更改为 0 来使用上述代码进行实际测试。所有发生的事情是我们的 for 循环从未真正运行过。
接下来要注意的是,调用 Query()
会同时返回指向 Rows 的指针以及错误。这意味着您可能甚至在开始迭代结果之前就可能遇到错误,因此您需要先检查一下错误。
假设没有错误,我们的* Rows
对象不应为nil,我们可以继续执行并推迟调用以关闭行。我们通过调用defer rows.Close()
来做到这一点。
在大多数情况下,您实际上不需要手动关闭行对象,因为在调用Next()
方法并且不再有结果集时会发生这种情况,但是在发生错误的情况下,可能需要手动调用Close()
。这也是一个幂等方法,意味着您可以多次调用它而不会产生任何负面影响,因此我建议通过defer
进行调用,只要Rows
不为零即可。
之后,我们进入for循环,在其中循环访问SQL语句返回的每个记录。我们通过调用rows.Next()
来完成此操作,当成功准备下一行时返回true,否则返回false。
通常来说,rows.Next()
的false
返回值表示没有更多记录了,但这并非总是如此。我们将在本系列的下一篇文章中看到多个结果集如何影响此结果,并且在准备下一行时发生错误时也有可能获得false
返回值。这就是为什么您在示例末尾看到对rows.Err()的调用-这是我们在调用rows.Next()时验证是否没有错误的方法。
我怀疑代码是通过这种方式设计的,从而简化了编写循环的过程,但是这样做的缺点是忘记检查错误非常容易,因此*不要忘记调用rows.Err()
,并检查错误!。
在rows.Next()
循环中,您可以使用rows
变量,与使用单个Row
的方式非常相似。您在每个单独的行上调用Scan()
并传递数据的目标位置。如果存在一个,则该方法调用将返回错误,否则返回nil。一个例外是在这种情况下您永远不会收到ErrNoRows错误,但是您可能会收到抱怨在调用Next()
之前调用Scan()
。
除了Scan()
方法之外,行
还具有其他一些辅助方法,例如:
- rows.Columns()-此方法用于检索SQL查询返回的每个列的名称。如果您正在编写类似sqlx的库,并且想要将struct标记映射到数据库中的列,则这很有用。
- rows.ColumnTypes()-此方法用于检索有关每一列的信息,例如类型,长度或是否为空。我本人并没有经常使用它,所以我不确定Postgres对它的支持程度如何。
最终,我们可以打印出我们的数据(或使用它做你需要做的其他任何事情),并随程序一起运行。只要确保检查rows.Err()
是否有任何错误!
常见错误
此时你可能会遇到的最常见错误是:
sql: expected 2 destination arguments in Scan, not 1
这意味着你将错误的参数数量传递给Scan()
方法。你需要为SQL语句检索的所有列传递足够的参数。如果不确定这是多少,建议你调用rows.Columns 方法,该方法将返回由SQL语句返回的列名称。
概要
就像其他讨论使用 Go 的database/sql
包的文章一样,本文实际上并没有包含太多的 Go 代码。而 database/sql
包旨在帮助你编写安全的 SQL 查询,避免一些陷阱。这样做的好处是,如果您已经了解 SQL,则无需学习太多知识就可以快速提高工作效率。
这种方法的缺点是,有时您可能会发现自己编写了更多代码,例如将指针传递User
给该Scan()
方法的对象中的每个字段 。
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
推荐文章: