有几种常用的操作可以从数据库中检索结果。

  1. 执行返回一条数据的查询操作。
  2. 准备要重复使用的语句,在多次执行该语句后进行销毁。
  3. 一次性的执行语句,并且不打算将其重复使用。
  4. 执行返回一条数据的查询,这种特殊情况有一个快捷方式。

Go 的 database/sql 包中函数名称很重要。如果一个函数名字包含 Query, 那么该函数旨在向数据库发出查询问题, 并且即使它为空,也将返回一组行。不返回行的语句不应该使用 Query 函数; 而应使用 Exec()

从数据库获取数据

让我们来看一个如何查询数据库和处理数据的例子. 我们将在用户表 users 中查询 id 为 1 的用户, 那么如何打印该用户的 idname 呢,我们需要使用 rows.Scan() 函数把数据遍历并赋值给变量。

var (
    id int
    name string
)
rows, err := db.Query("select id, name from users where id = ?", 1)
if err != nil {
    log.Fatal(err)
}
defer rows.Close()
for rows.Next() {
    err := rows.Scan(&id, &name)
    if err != nil {
        log.Fatal(err)
    }
    log.Println(id, name)
}
err = rows.Err()
if err != nil {
    log.Fatal(err)
}

下面是此段代码所做的事情:

  1. 我们使用 db.Query() 将查询请求发送给数据库,我们通常要检查是否报错。
  2. 我们需要在程序最后使用 rows.Close()来关闭,这是非常重要的。
  3. 我们将使用 rows.Next()来遍历每行的数据
  4. 我们使用 rows.Scan()来将每一行中每一列的数据赋值给变量。
  5. 当把每一行的数据进行遍历后,检查是否出错。

这几乎是 Go 中唯一的方法。例如,您无法获得一行作为 map。那是因为所有内容都是强类型的。您需要创建正确类型的变量,并将指针传递给它们。

其中的两个部分很容易出错,并可能带来严重的后果。

  • 您应该始终检查 for rows.Next() 循环的末尾是否有错误。如果循环过程中出现错误,您需要知道它。不要只假设循环会迭代,直到您处理完所有行。
  • 其次,只要存在打开的结果集(由 rows 表示),底层连接就很忙,不能用于任何其他查询。这意味着它在连接池中不可用。如果使用 rows.Next() 迭代所有行,最终将读取最后一行,并且rows.Next() 将遇到内部 EOF 错误并为您调用 rows.Close()。但是,如果出于某种原因您退出该循环——提前返回,等等情况——那么 rows 不会关闭,连接保持打开。(不过,如果rows.Next() 由于错误返回 false,那么它会自动关闭)。这是耗尽资源的一种简单方式。
  • 如果 rows.Close() 已经关闭,那么它是一个无害的 no-op,因此您可以多次调用它。但是请注意,我们首先检查错误,只有在没有错误时才调用 rows.Close(),以避免 runtime panic。
  • 您应该 始终 defer rows.Close(),即使您也在循环末尾显式调用 rows.Close(),这不是个坏主意。
  • 不要在循环内 defer。在函数退出之前,不会执行延迟语句,因此长时间运行的函数不应使用该语句。如果这样做,您将慢慢积累内存。如果要在循环中重复查询和使用结果集,则应在处理完每个结果后显式调用 rows.Close(),而不要使用 defer

Scan() 如何工作

当您遍历行并将其扫描到目标变量中时,Go 会在后台执行数据类型转换。它基于目标变量的类型。意识到这一点可以清理您的代码并有助于避免重复的工作。

例如,假设您从用字符串列定义的表中选择一些行,例如VARCHAR(45)或类似名称。但是,您偶然知道该表始终包含数字。如果将指针传递给字符串,Go 会将字节复制到字符串中。现在您可以使用 strconv.ParseInt() 或类似方法将值转换为数字。您必须检查 SQL 操作中的错误以及解析整数的错误。这是混乱而又乏味的。

或者,您可以仅传递 Scan() 指向整数的指针。 Go 将检测到该情况并为您调用 strconv.ParseInt()。如果转换中出现错误,则对 Scan() 的调用将返回该错误。您的代码现在变得更整洁,更小了。这是使用 database/sql 的推荐方法。

预处理查询

通常,您应该始终对要多次使用的查询执行预处理。预处理查询的结果是一个预处理语句,该语句可以具有占位符(也称为绑定值),用于执行该语句时将提供的参数。出于所有常见原因(例如,避免 SQL 注入攻击),这比串联字符串好得多。

在 MySQL 中,参数占位符是 ?,而在 PostgreSQL 中则是 $N,其中 N 是一个数字。 SQLite 接受任何一种。在 Oracle 中,占位符以冒号开头并被命名,例如 :param1。我们将使用 ?,因为我们使用 MySQL 作为例。

stmt,err := db.Prepare("select id from users where id = ?")
if err != nil {
    log.Fatal(err)
}
defer stmt.Close()
rows, err := stmt.Query(1)
if err != nil {
    log.Fatal(err)
}
defer rows.Close()
for rows.Next(){
    // ...
}
if err = rows.Err(); err != nil {
    log.Fatal(err)
}

在幕后,db.Query() 实际上做了预处理、执行并关闭了预处理语句。这是到数据库的三次往返。如果不小心,您的应用程序所进行的数据库交互次数可能会增加三倍!某些驱动程序在特定情况下可以避免这种情况,但并非所有驱动程序都可以。有关详细信息,请参阅 预处理语句

单条记录查询

查询结果多于一行的时候,这样来获取单条记录:

var name string
var has bool
err = db.QueryRow("select name from users where id = ?", 1).Scan(&name)
if err != nil {
    if err == sql.ErrNoRows {
        has = false
    } else {
        log.Fatal(err)
    }
} else { has = true }
fmt.Println(name, has)

错误信息会在 Scan() 之后返回。 QueryRow() 也可以用于预处理语句:

stmt, err := db.Prepare("select name from users where id = ?")
if err != nil {
    log.Fatal(err)
}
defer stmt.Close()
var name string
var has bool
err = stmt.QueryRow(1).Scan(&name)
if err != nil {
    if err == sql.ErrNoRows {
        has = false
    } else {
        log.Fatal(err)
    }
} else { has = true }
fmt.Println(name, has)

转自:https://learnku.com/docs/go-database-sql/retrieving/9477

最后编辑: kuteng  文档更新时间: 2021-11-19 09:18   作者:kuteng