打开一个数据库句柄

本文翻译自《Opening a database handle》。

目录

查找和导入一个数据库驱动程序

打开一个数据库句柄

确认一个数据库连接

存储数据库凭据

释放资源

database/sql包减少了你管理连接的需要,从而简化了数据库访问。与许多数据访问API不同,使用database/sql时,你不是显式地打开数据库连接、工作,然后关闭连接。相反,你的代码打开一个表示连接池的数据库句柄,然后使用该句柄执行数据访问操作,仅在需要释放资源时调用Close方法,例如检索到的行或准备好的语句所持有的资源。

换句话说,由sql.DB表示数据库句柄,用于处理连接,代表代码打开和关闭连接。当你的代码使用数据库句柄执行数据库操作时,这些操作可以并发地访问数据库。有关详细信息,请参阅管理数据库连接

注意:你也可以使用一条专用的数据库连接。有关更多信息,请参阅使用专用连接

除了database/sql包中提供的API之外,Go社区还为所有最常见(以及许多不常见)的数据库管理系统(DBMS)开发了驱动程序。

打开数据库句柄时,你执行以下高层级的步骤:

1 找到一个驱动程序。

驱动程序在Go代码和数据库之间转换请求和响应。有关详细信息,请参阅查找和导入一个数据库驱动程序

2 打开一个数据库句柄。

导入驱动程序后,可以打开访问特定数据库的一个句柄。有关详细信息,请参见打开一个数据库句柄

3 确认一个数据库连接。

打开数据库句柄后,代码就可以检查这个数据库连接是否可用。有关详细信息,请参阅确认一个数据库连接

你的代码通常不会显式地打开或关闭数据库连接——这是由数据库句柄完成的。然而,你的代码应该释放在此过程中获得的资源,例如包含查询结果的sql.Rows。有关详细信息,请参见释放资源

查找和导入一个数据库驱动程序

你需要一个支持你正在使用的DBMS的Go语言的数据库驱动程序。要查找数据库的驱动程序,请参阅SQLDrivers

为了使驱动程序可用于你的代码,你可以像导入另一个Go包一样导入它。下面是一个例子:

import "github.com/go-sql-driver/mysql"

请注意,如果你不直接从驱动程序包调用任何函数,例如当sql包隐式使用它时,你需要使用一个空白导入,该导入在导入路径前加一个下划线:

import _ "github.com/go-sql-driver/mysql"

注意:作为最佳实践,避免使用数据库驱动程序自己的API进行数据库操作。相反,应该使用database/sql包中的函数。这将有助于保持代码与DBMS的松散耦合,从而在需要时更容易切换到不同的DBMS。

打开一个数据库句柄

sql.DB数据库句柄提供了单独或在事务中读取和写入数据库的能力。

你可以通过调用sql.Open函数(使用连接字符串)或sql.OpenDB函数(使用driver.Connector)来获取数据库句柄。两者都返回一个指向sql.DB的指针。

注意:请确保将你的数据库凭据放在Go源代码之外。有关更多信息,请参阅存储数据库凭据

使用一个连接字符串

如果要使用连接字符串进行连接,请使用sql.Open函数。字符串的格式会因你使用的驱动程序而异。

以下是MySQL的一个示例:

db, err = sql.Open("mysql", "username:password@tcp(127.0.0.1:3306)/jazzrecords")
if err != nil {
    log.Fatal(err)
}

然而,你可能会发现,以更结构化的方式捕获连接属性可以为你提供更具可读性的代码。具体细节因数据库驱动而异。

例如,你可以将前面的示例替换为以下示例,该示例使用MySQL驱动程序的Config指定连接属性,并使用FormatDSN方法构建连接字符串。

// 指定连接属性。
cfg := mysql.Config{
    User:   username,
    Passwd: password,
    Net:    "tcp",
    Addr:   "127.0.0.1:3306",
    DBName: "jazzrecords",
}

// 获得一个数据库句柄。
db, err = sql.Open("mysql", cfg.FormatDSN())
if err != nil {
    log.Fatal(err)
}

使用一个连接器

当你想利用连接字符串中无法设置的特定于驱动程序的连接特性时,请通过sql.OpenDB函数。每个驱动程序都支持自己的连接特性集,通常用于自定义特定于DBMS的连接请求。

将前面的sql.Open示例改编为使用sql.OpenDB,你可以使用以下代码创建一个数据库句柄:

// 指定连接属性。
cfg := mysql.Config{
    User:   username,
    Passwd: password,
    Net:    "tcp",
    Addr:   "127.0.0.1:3306",
    DBName: "jazzrecords",
}

// 获取一个特定驱动的连接器
connector, err := mysql.NewConnector(&cfg)
if err != nil {
    log.Fatal(err)
}

// 获取一个数据库句柄。
db = sql.OpenDB(connector)

处理错误

你的代码应该检查尝试创建句柄时是否出现了一个错误,例如使用sql.Open函数。这不会是连接错误。相反,如果sql.Open函数无法初始化句柄,则会出现一个错误。例如,如果它无法解析你指定的DSN,就可能会发生这种情况。

确认一个数据库连接

打开一个数据库句柄时,sql包可能不会立即创建一个新的数据库连接。相反,它可能会在你的代码需要时创建这个连接。如果你不想立即使用数据库,并且想确认可以建立连接,请调用PingPingContext函数。

以下示例中的代码ping数据库以确认连接。

db, err = sql.Open("mysql", connString)

// 确认是否能连接数据库成功。
if err := db.Ping(); err != nil {
    log.Fatal(err)
}

存储数据库凭据

避免在Go源代码中存储数据库凭据,因为这可能会将数据库的内容暴露给其他人。相反,找到一种方法将它们存储在代码之外但可供使用的位置。例如,考虑一个秘密保管器应用程序,该应用程序存储凭据并提供一个API,代码可以使用该API来检索凭据,以便与DBMS进行身份验证。

一种流行的方法是在程序启动前将秘密信息存储在你的环境变量中,然后你的Go程序可以使用os.Getenv函数:

username := os.Getenv("DB_USER")
password := os.Getenv("DB_PASS")

这种方法还允许你自己设置用于本地测试的环境变量。

释放资源

尽管你没有使用database/sql包显式地管理或关闭连接,但当不再需要资源时,你的代码应该会释放所获得的资源。这些包括sql.Rows所拥有的资源,表示从查询返回的行数据,或者sql.Stmt表示准备好的语句。

通常,通过延迟对Close函数的调用来释放资源,以便在外部函数退出之前释放资源。

以下示例中的代码延迟调用Close函数以释放sql.Rows所拥有的资源。

rows, err := db.Query("SELECT * FROM album WHERE artist = ?", artist)
if err != nil {
    log.Fatal(err)
}
defer rows.Close()

// 循环遍历返回的行数据。

执行事务

本文翻译自《Executing transactions》。

目录

最佳实践

例子

你可以使用代表事务的sql.Tx结构体执行数据库事务操作。除了表示事务特定语义的CommitRollback方法之外,sql.Tx还具有用来执行常见数据库操作的所有方法。要获取sql.Tx,你可以调用DB.BeginDB.BeginTx方法。

数据库事务将多个数据库操作合成一组,作为更大目标的一部分。其中的所有数据库操作都必须成功执行或都不执行,在任何一种情况下都会保持数据的完整性。 通常,事务操作的流程包括:

1 开始事务。

2 执行一组数据库操作。

3 如果没有错误发生,提交事务以进行数据库更改。

4 如果发生错误,回滚事务以保持数据库不变。

sql包提供了开始和结束事务的方法,以及执行中间数据库操作的方法。这些方法对应于上述流程中的四个步骤。

1 开始事务。

DB.BeginDB.BeginTx开始一个新的数据库事务,返回一个代表它的sql.Tx结构体。

2 执行数据库操作。

使用sql.Tx,你可以在单个数据库连接中查询或更新数据库。为了支持这一点,Tx导出了以下方法:

3 使用以下其中一项结束事务:

如果Commit方法执行成功(返回nil错误),则所有查询结果都被确认为有效,并且所有已执行的更新都作为单个原子更改应用于数据库。如果Commit方法执行失败,则TxQueryExec的所有结果都应被视为无效而丢弃。

即使Tx.Rollback方法执行失败,事务也不再有效,也不会提交到数据库。

最佳实践

遵循以下最佳实践,以更好地应对事务有时需要的复杂语义和连接管理。

  • 使用本节中描述的API来管理事务。不要直接使用BEGINCOMMIT等与事务相关的SQL语句——这样做会使你的数据库处于不可预测的状态,尤其是在并发程序中。
  • 使用事务的过程中,请注意不要直接调用非事务的sql.DB的方法,因为它们会在事务外部执行,从而使你的代码对数据库状态的看法不一致,甚至会导致死锁。

例子

以下示例中的函数使用事务为相册(album)创建新的客户订单(customer order)。在此过程中,该函数的代码将:

1 开始事务。

2 延迟事务的回滚。如果事务执行成功,它将在该函数退出之前提交,使延迟回滚调用(defer tx.Rollback())成为空操作。如果事务执行失败,则不会提交,这意味着回滚将在函数退出时被调用。

3 确认客户订购的专辑有足够的库存(inventory)。

4 如果足够,更新库存数量,减少订购的专辑数目。

5 创建一个新订单并获取新订单为客户生成的ID。

6 提交事务并返回这个ID。

此示例的Tx方法需要一个context.Context参数。使得函数的执行(包括数据库操作)在运行时间过长或客户端连接关闭时被取消。有关更多信息,请参阅取消正在进行中的数据库操作

// CreateOrder函数为相册(album)创建一个新的客户订单(customer order)并返回这个新订单的ID。
func CreateOrder(ctx context.Context, albumID, quantity, custID int) (orderID int64, err error) {

    // 创建一个帮助函数用于返回失败的结果。
    fail := func(err error) (int64, error) {
        return 0, fmt.Errorf("CreateOrder: %v", err)
    }

    // 获取一个事务实例tx用来执行事务操作。
    tx, err := db.BeginTx(ctx, nil)
    if err != nil {
        return fail(err)
    }
    // 延迟执行一个回滚操作,以防在此事务中某些操作执行失败
    defer tx.Rollback()

    // 确认相册库存足以下这个订单。
    var enough bool
    if err = tx.QueryRowContext(ctx, "SELECT (quantity >= ?) from album where id = ?",
        quantity, albumID).Scan(&enough); err != nil {
        if err == sql.ErrNoRows {
            return fail(fmt.Errorf("no such album"))
        }
        return fail(err)
    }
    if !enough {
        return fail(fmt.Errorf("not enough inventory"))
    }

    // 更新相册库存以减去本订单中的相册数目。
    _, err = tx.ExecContext(ctx, "UPDATE album SET quantity = quantity - ? WHERE id = ?",
        quantity, albumID)
    if err != nil {
        return fail(err)
    }

    // 在album_order表中创建一个新行。
    result, err := tx.ExecContext(ctx, "INSERT INTO album_order (album_id, cust_id, quantity, date) VALUES (?, ?, ?, ?)",
        albumID, custID, quantity, time.Now())
    if err != nil {
        return fail(err)
    }
    // 获得刚刚创建的订单条目的ID。
    orderID, err = result.LastInsertId()
    if err != nil {
        return fail(err)
    }

    // 提交这个事务。
    if err = tx.Commit(); err != nil {
        return fail(err)
    }

    // 返回这个订单的ID。
    return orderID, nil
}