教程:访问关系数据库
本教程介绍了使用 database/sql
及其标准库中的包访问关系数据库的基础知识.
如果您对 Go 及其工具有基本的了解,您将从本教程中获得最大收益。如果这是您第一次接触 Go,请参阅 教程:Go 入门以 获得快速介绍.
您将使用的包database/sql
包括用于连接数据库、执行事务、取消正在进行的操作等的类型和函数。有关使用该包的更多详细信息,请参阅 访问数据库。
在本教程中,您将创建一个数据库,然后编写代码来访问该数据库。您的示例项目将是有关老式爵士乐唱片的数据存储库.
在本教程中,您将逐步完成以下部分:
- 为您的代码创建一个文件夹.
- 建立一个数据库.
- 导入数据库驱动程序.
- 获取数据库句柄并连接.
- 查询多行.
- 查询单行.
- 添加数据.
注意: 有关其他教程,请参阅教程.
先决条件
- 一个安装MySQL的 关系数据库管理系统(DBMS)
- 已安装 Go. 有关安装说明,请参阅 安装 Go.
- 用于编辑代码的工具. 您拥有的任何文本编辑器都可以正常工作.
- 一个命令终端. Go 在 Linux 和 Mac 上的任何终端以及 Windows 中的 PowerShell 或 cmd 上都能很好地工作.
为您的代码创建一个文件夹
首先,为您要编写的代码创建一个文件夹.
-
打开命令提示符并切换到您的home目录.
在 Linux 或 Mac 上:
$ cd
在 Windows 上:
C:\> cd %HOMEPATH%
对于本教程的其余部分,我们将显示一个 $ 作为提示符。我们使用的命令也适用于 Windows.
-
在命令提示符下,为您的代码创建一个名为 data-access 的目录.
$ mkdir data-access $ cd data-access
-
创建一个模块,您可以在其中管理将在本教程中添加的依赖项.
运行
go mod init
命令,为其提供新代码的模块路径.$ go mod init example/data-access go: creating new go.mod: module example/data-access
此命令创建一个 go.mod 文件,您添加的依赖项将在其中列出以供跟踪。有关更多信息,请务必查看 管理依赖项.
注意: 在实际开发中,您会指定一个更符合您自己需求的模块路径。有关更多信息,请参阅 管理依赖项.
接下来,您将创建一个数据库.
建立一个数据库
在此步骤中,您将创建要使用的数据库。您将使用 DBMS 本身的 CLI 创建数据库和表,以及添加数据.
您将创建一个数据库,其中包含有关黑胶唱片上的老式爵士乐录音的数据.
这里的代码使用MySQL CLI,但大多数 DBMS 都有自己的 CLI,具有类似的功能.
-
打开一个新的命令提示符.
-
在命令行,登录到您的 DBMS,如下面的 MySQL 示例所示.
$ mysql -u root -p Enter password: mysql>
-
在
mysql
命令提示符下,创建一个数据库.mysql> create database recordings;
-
更改为您刚刚创建的数据库,以便您可以添加表.
mysql> use recordings; Database changed
-
在文本编辑器中,在data-access文件夹中,创建一个名为 create-tables.sql 的文件以保存用于添加表的 SQL 脚本.
-
将以下 SQL 代码粘贴到文件中,然后保存文件.
DROP TABLE IF EXISTS album; CREATE TABLE album ( id INT AUTO_INCREMENT NOT NULL, title VARCHAR(128) NOT NULL, artist VARCHAR(255) NOT NULL, price DECIMAL(5,2) NOT NULL, PRIMARY KEY (`id`) ); INSERT INTO album (title, artist, price) VALUES ('Blue Train', 'John Coltrane', 56.99), ('Giant Steps', 'John Coltrane', 63.99), ('Jeru', 'Gerry Mulligan', 17.99), ('Sarah Vaughan', 'Sarah Vaughan', 34.98);
在此 SQL 代码中,您:
-
删除(drop)名为
album
的表。如果要从头开始使用表,请先执行此命令可以更轻松地在以后重新运行脚本。 -
创建包含四列的
album
表:title
,artist
, 和price
。每行的id
值由 DBMS 自动创建。 -
添加四行值.
-
-
在
mysql
命令提示符下,运行您刚刚创建的脚本.你会使用
source
命令以以下形式:mysql> source /path/to/create-tables.sql
-
在 DBMS 命令提示符处,使用
SELECT
语句来验证您是否已成功创建包含数据的表。mysql> select * from album; +----+---------------+----------------+-------+ | id | title | artist | price | +----+---------------+----------------+-------+ | 1 | Blue Train | John Coltrane | 56.99 | | 2 | Giant Steps | John Coltrane | 63.99 | | 3 | Jeru | Gerry Mulligan | 17.99 | | 4 | Sarah Vaughan | Sarah Vaughan | 34.98 | +----+---------------+----------------+-------+ 4 rows in set (0.00 sec)
接下来,您将编写一些 Go 代码进行连接,以便进行查询.
查找并导入数据库驱动程序
现在你已经有了一个包含一些数据的数据库,开始写你的 Go 代码.
找到并导入一个数据库驱动程序,该驱动程序会将您通过 database/sql
包中的函数发出的请求转换为数据库可以理解的请求。
-
在您的浏览器中,访问SQLDrivers wiki 页面以发现您可以使用的驱动程序.
使用页面上的列表来识别您将使用的驱动程序。为了在本教程中访问 MySQL,您将使用 Go-MySQL-Driver.
-
请注意驱动程序的包名称 – 此处为,
github.com/go-sql-driver/mysql
. -
使用文本编辑器,创建一个用于编写 Go 代码的文件,并将该文件另存为 main.go,保存在您之前创建的data-access目录中。
-
进入main.go,粘贴以下代码导入驱动包.
package main import "github.com/go-sql-driver/mysql"
在此代码中,您:
-
将您的代码添加到
main
包中,以便您可以独立执行它. -
导入 MySQL 驱动程序
github.com/go-sql-driver/mysql
.
-
导入驱动程序后,您将开始编写代码以访问数据库.
获取数据库句柄并连接
现在编写一些 Go 代码,让您使用数据库句柄访问数据库.
您将使用指向 sql.DB
结构的指针,它表示对特定数据库的访问.
编写代码
-
在 main.go 中,在刚刚添加的
import
代码下方,粘贴以下 Go 代码以创建数据库句柄.var db *sql.DB func main() { // 捕获连接属性. cfg := mysql.Config{ User: os.Getenv("DBUSER"), Passwd: os.Getenv("DBPASS"), Net: "tcp", Addr: "127.0.0.1:3306", DBName: "recordings", //AllowNativePasswords: true, } // 获取数据库句柄. var err error db, err = sql.Open("mysql", cfg.FormatDSN()) if err != nil { log.Fatal(err) } pingErr := db.Ping() if pingErr != nil { log.Fatal(pingErr) } fmt.Println("Connected!") }
在此代码中,您:
-
声明类型为
*sql.DB
的db
变量。这是您的数据库句柄.使
db
成为一个全局变量简化了这个例子。在生产环境中,您会避免使用全局变量,例如将变量传递给需要它的函数或将其包装在结构中. -
使用 MySQL 驱动程序的
Config
– 以及该类型的FormatDSN
-– 来收集连接属性,并将它们格式化为连接字符串的 DSN。此
Config
结构使代码比连接字符串更容易阅读。 -
调用
sql.Open
初始化db
变量,传递FormatDSN
的返回值。 -
检查来自
sql.Open
的错误。例如,如果您的数据库连接细节格式不正确,则可能会失败。为了简化代码,您调用
log.Fatal
结束执行并将错误打印到控制台。在生产代码中,您会希望以更优雅的方式处理错误。 -
调用
DB.Ping
以确认连接到数据库是否正常工作。在运行时,sql.Open
可能不会立即连接,具体取决于驱动程序。你在此处使用Ping
来确认database/sql
包可以在需要时进行连接。 -
检查
Ping
中的错误,以防连接失败。 -
如果
Ping
连接成功,则打印一条消息.
-
-
在 main.go 文件的顶部附近,就在包声明的下方,导入支持刚编写的代码所需的包.
文件的顶部现在应该如下所示:
package main import ( "database/sql" "fmt" "log" "os" "github.com/go-sql-driver/mysql" )
-
保存 main.go.
运行代码
-
开始跟踪 MySQL 驱动程序模块作为依赖项。
使用
go get
添加 github.com/go-sql-driver/mysql 模块作为您自己模块的依赖项。使用点参数表示“获取当前目录中代码的依赖项。”$ go get . go get: added github.com/go-sql-driver/mysql v1.6.0
Go 下载了此依赖项,因为您在上一步中将其添加到了
import
声明中。有关依赖项跟踪的详细信息,请参阅添加依赖项. -
在命令提示符下,设置Go 程序使用的
DBUSER
和DBPASS
环境变量。在 Linux 或 Mac 上:
$ export DBUSER=username $ export DBPASS=password
在 Windows 上:
C:\Users\you\data-access> set DBUSER=username C:\Users\you\data-access> set DBPASS=password
-
在包含 main.go 的目录中的命令行中,通过键入带有 点 参数的
go run
来运行代码,表示"在当前目录中运行包。"$ go run . Connected!
你可以连接!接下来,您将查询一些数据.
查询多行
在本节中,您将使用 Go 执行旨在返回多行的 SQL 查询.
对于可能返回多行的 SQL 语句,请使用 database/sql
包中的 Query
方法,然后遍历它返回的行。(稍后将在查询单行部分中了解如何查询单行.)
编写代码
-
在 main.go 中,紧挨着
func main
的上方,粘贴以下Album
结构的定义。您将使用它来保存从查询返回的行数据.type Album struct { ID int64 Title string Artist string Price float32 }
-
在
func main
下,粘贴以下albumsByArtist
函数以查询数据库.// albumsByArtist对具有指定艺术家姓名的专辑进行查询. func albumsByArtist(name string) ([]Album, error) { // 一个专辑切片,用于保存返回行中的数据. var albums []Album rows, err := db.Query("SELECT * FROM album WHERE artist = ?", name) if err != nil { return nil, fmt.Errorf("albumsByArtist %q: %v", name, err) } defer rows.Close() // 遍历行,使用"扫描"将列数据分配给结构字段. for rows.Next() { var alb Album if err := rows.Scan(&alb.ID, &alb.Title, &alb.Artist, &alb.Price); err != nil { return nil, fmt.Errorf("albumsByArtist %q: %v", name, err) } albums = append(albums, alb) } if err := rows.Err(); err != nil { return nil, fmt.Errorf("albumsByArtist %q: %v", name, err) } return albums, nil }
在此代码中,您:
-
声明您定义的
Album
类型的albums
切片。这将保存返回行中的数据。结构字段名称和类型对应于数据库列名称和类型. -
使用
DB.Query
执行SELECT
语句来查询具有指定艺术家名字的专辑。Query
的第一个参数是 SQL 语句。在参数之后,您可以传递零个或多个任何类型的参数。这些为您提供了在 SQL 语句中指定参数值的位置。通过将 SQL 语句与参数值分开(而不是将它们连接起来,比如说,fmt.Sprintf
),您可以让database/sql
包将值与 SQL 文本分开发送,从而消除任何 SQL 注入风险。 -
推迟关闭
rows
,以便在函数退出时释放它所持有的任何资源。 -
循环遍历返回的行,使用
Rows.Scan
将每行的列值分配给Album
结构字段。Scan
获取指向 Go 值的指针列表,列值将被写入其中。在这里,您将指针传递到使用&
运算符创建的alb
变量中的字段 。Scan
通过指针写入以更新结构字段。 -
在循环内部,检查将列值扫描到结构字段中是否存在错误。
-
在循环中,将新
alb
追加到albums
切片。 -
在循环之后,使用
rows.Err
检查整个查询中的错误 。请注意,如果查询本身失败,则在此处检查错误是找出结果不完整的唯一方法。
-
-
更新您的
main
函数以调用albumsByArtist
.在
func main
的末尾,添加以下代码。albums, err := albumsByArtist("John Coltrane") if err != nil { log.Fatal(err) } fmt.Printf("Albums found: %v\n", albums)
在新代码中,您现在:
-
调用您添加的
albumsByArtist
函数,将其返回值分配给新的albums
变量. -
打印结果.
-
运行代码
从包含 main.go 的目录中的命令行,运行代码.
$ go run .
Connected!
Albums found: [{1 Blue Train John Coltrane 56.99} {2 Giant Steps John Coltrane 63.99}]
接下来,您将查询单行.
查询单行
在本节中,您将使用 Go 查询数据库中的单行.
对于您知道最多会返回一行的 SQL 语句,您可以使用 QueryRow
,这比使用Query
循环更简单.
编写代码
-
在
albumsByArtist
下方,粘贴以下albumByID
函数.// albumByID查询指定ID的专辑。 func albumByID(id int64) (Album, error) { // 一个专辑将保存返回行的数据。 var alb Album row := db.QueryRow("SELECT * FROM album WHERE id = ?", id) if err := row.Scan(&alb.ID, &alb.Title, &alb.Artist, &alb.Price); err != nil { if err == sql.ErrNoRows { return alb, fmt.Errorf("albumsById %d: no such album", id) } return alb, fmt.Errorf("albumsById %d: %v", id, err) } return alb, nil }
在此代码中,您:
-
使用
DB.QueryRow
以执行SELECT
语句以查询具有指定 ID 的专辑.它返回一个
sql.Row
。为了简化调用代码(您的代码!),QueryRow
不会返回错误。相反,它安排从Rows.Scan
稍后返回任何查询错误(例如sql.ErrNoRows
)。 -
使用
Row.Scan
将列值复制到结构字段中。 -
从
Scan
中检查错误。特殊错误
sql.ErrNoRows
表示查询未返回任何行。通常,该错误值得用更具体的文本替换,例如此处的“no such album”。
-
-
更新
main
来调用albumByID
。在
func main
的末尾,添加以下代码。// 在此处对 ID 2 进行硬编码以测试查询。 alb, err := albumByID(2) if err != nil { log.Fatal(err) } fmt.Printf("Album found: %v\n", alb)
在新代码中,您现在:
-
调用你添加的
albumByID
函数. -
打印返回的专辑 ID.
-
运行代码
从包含 main.go 的目录中的命令行,运行代码.
$ go run .
Connected!
Albums found: [{1 Blue Train John Coltrane 56.99} {2 Giant Steps John Coltrane 63.99}]
Album found: {2 Giant Steps John Coltrane 63.99}
接下来,您将向数据库添加专辑.
添加数据
在本节中,您将使用 Go 执行 SQL INSERT
语句以向数据库添加新行.
您已经了解了如何使用Query
和 QueryRow
处理返回数据的 SQL 语句。要执行 不 返回数据的SQL 语句,请使用 Exec
.
编写代码
-
在
albumByID
下面,粘贴以下addAlbum
函数以在数据库中插入新专辑,然后保存 main.go.// addAlbum 将指定的专辑添加到数据库中, // 返回新条目的专辑 ID func addAlbum(alb Album) (int64, error) { result, err := db.Exec("INSERT INTO album (title, artist, price) VALUES (?, ?, ?)", alb.Title, alb.Artist, alb.Price) if err != nil { return 0, fmt.Errorf("addAlbum: %v", err) } id, err := result.LastInsertId() if err != nil { return 0, fmt.Errorf("addAlbum: %v", err) } return id, nil }
在此代码中,您:
-
使用
DB.Exec
执行一个INSERT
语句.与
Query
一样,Exec
接受一条 SQL 语句,后跟 SQL 语句的参数值。 -
检查尝试
INSERT
时是否出现错误。 -
使用
Result.LastInsertId
检索插入的数据库行的 ID。 -
检查尝试检索 ID 时出现的错误。
-
-
更新
main
以调用新的addAlbum
函数.在
func main
的末尾,添加以下代码。albID, err := addAlbum(Album{ Title: "The Modern Sound of Betty Carter", Artist: "Betty Carter", Price: 49.99, }) if err != nil { log.Fatal(err) } fmt.Printf("ID of added album: %v\n", albID)
在新代码中,您现在:
- 使用新专辑调用
addAlbum
,将要添加的专辑的 ID 分配给albID
变量.
- 使用新专辑调用
运行代码
从包含 main.go 的目录中的命令行,运行代码.
$ go run .
Connected!
Albums found: [{1 Blue Train John Coltrane 56.99} {2 Giant Steps John Coltrane 63.99}]
Album found: {2 Giant Steps John Coltrane 63.99}
ID of added album: 5
结论
恭喜!您刚刚使用 Go 对关系数据库执行了简单的操作.
建议的下一个主题:
-
查看数据访问指南,其中包含有关此处仅涉及的主题的更多信息.
-
如果您是 Go 新手,您会发现Effective Go 和 如何编写 Go 代码中描述的有用的最佳实践.
-
Go 边学边练是对Go基础知识的一个很好的分步介绍。
完整代码
本节包含您使用本教程构建的应用程序的代码.
package main
import (
"database/sql"
"fmt"
"log"
"os"
"github.com/go-sql-driver/mysql"
)
var db *sql.DB
type Album struct {
ID int64
Title string
Artist string
Price float32
}
func main() {
// Capture connection properties.
cfg := mysql.Config{
User: os.Getenv("DBUSER"),
Passwd: os.Getenv("DBPASS"),
Net: "tcp",
Addr: "127.0.0.1:3306",
DBName: "recordings",
//AllowNativePasswords: true,
}
// Get a database handle.
var err error
db, err = sql.Open("mysql", cfg.FormatDSN())
if err != nil {
log.Fatal(err)
}
pingErr := db.Ping()
if pingErr != nil {
log.Fatal(pingErr)
}
fmt.Println("Connected!")
albums, err := albumsByArtist("John Coltrane")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Albums found: %v\n", albums)
// Hard-code ID 2 here to test the query.
alb, err := albumByID(2)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Album found: %v\n", alb)
albID, err := addAlbum(Album{
Title: "The Modern Sound of Betty Carter",
Artist: "Betty Carter",
Price: 49.99,
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("ID of added album: %v\n", albID)
}
// albumsByArtist queries for albums that have the specified artist name.
func albumsByArtist(name string) ([]Album, error) {
// An albums slice to hold data from returned rows.
var albums []Album
rows, err := db.Query("SELECT * FROM album WHERE artist = ?", name)
if err != nil {
return nil, fmt.Errorf("albumsByArtist %q: %v", name, err)
}
defer rows.Close()
// 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); err != nil {
return nil, fmt.Errorf("albumsByArtist %q: %v", name, err)
}
albums = append(albums, alb)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("albumsByArtist %q: %v", name, err)
}
return albums, nil
}
// albumByID queries for the album with the specified ID.
func albumByID(id int64) (Album, error) {
// An album to hold data from the returned row.
var alb Album
row := db.QueryRow("SELECT * FROM album WHERE id = ?", id)
if err := row.Scan(&alb.ID, &alb.Title, &alb.Artist, &alb.Price); err != nil {
if err == sql.ErrNoRows {
return alb, fmt.Errorf("albumsById %d: no such album", id)
}
return alb, fmt.Errorf("albumsById %d: %v", id, err)
}
return alb, nil
}
// addAlbum adds the specified album to the database,
// returning the album ID of the new entry
func addAlbum(alb Album) (int64, error) {
result, err := db.Exec("INSERT INTO album (title, artist, price) VALUES (?, ?, ?)", alb.Title, alb.Artist, alb.Price)
if err != nil {
return 0, fmt.Errorf("addAlbum: %v", err)
}
id, err := result.LastInsertId()
if err != nil {
return 0, fmt.Errorf("addAlbum: %v", err)
}
return id, nil
}