教程:访问关系型数据库

本文翻译自《Tutorial: Accessing a relational database》。

目录

先决条件

为你的代码创建一个文件夹

建立一个数据库

查找并导入数据库的驱动程序

获取数据库句柄并连接

查询多行

查询单行

添加数据

结论

完成全部代码

本教程介绍了使用Go及其标准库中的database/sql包访问关系数据库的基础知识。

如果你对Go及其工具有基本的了解,你将充分利用本教程。如果这是你第一次接触Go,请先参阅教程:Go入门以获得快速介绍。

你将使用的database/sql包包括用于连接数据库、执行事务、取消正在进行的操作等的类型和函数。有关使用该包的更多详细信息,请参阅访问关系型数据库

在本教程中,你将创建一个数据库,然后编写代码来访问该数据库。你的示例项目将是有关古典爵士乐唱片的数据存储库。

在本教程中,你将逐步完成以下部分:

1 为你的代码创建一个文件夹。

2 建立一个数据库。

3 导入数据库驱动。

4 获取数据库句柄并连接。

5 查询多行。

6 查询单行。

7 添加数据。

注意:有关其他教程,请参阅教程

先决条件

  • 安装MySQL关系数据库管理系统 (DBMS)。
  • 安装Go语言开发环境。有关安装说明,请参阅安装Go
  • 一个编辑代码的工具。你拥有的任何文本编辑器都可以正常工作。
  • 一个命令行终端。Go在Linux和Mac中的任何终端,以及Windows中的PowerShell或cmd上都能很好地工作。

为你的代码创建一个文件夹

首先,为你要编写的代码创建一个文件夹。

1 打开命令提示符并切换到你的家目录。 在Linux或Mac上:

$ cd

在Windows上:

C:\> cd %HOMEPATH%

对于本教程的其余部分,我们将显示$作为提示符。我们使用的命令也适用于 Windows。

2 在命令提示符下,为你的代码创建一个名为data-access的目录。

$ mkdir data-access
$ cd data-access

3 创建一个模块,你可以在其中管理将在本教程中添加的依赖项。

运行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。

1 打开新的命令提示符。

2 在命令行中,登录DBMS,如下面的MySQL示例所示。

$ mysql -u root -p
Enter password:

mysql>

3 在mysql命令提示符下,创建一个数据库。

mysql> create database recordings;

4 切换到你刚刚创建的数据库,以便你可以添加表。

mysql> use recordings;
Database changed

5 在文本编辑器的数据访问文件夹中,创建一个名为create-tables.sql的文件来保存用于添加表的SQL脚本。

6 在文件中,粘贴以下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代码中,你:

  • 删除一个名为album的表。如果你想重新开始使用该表,执行此命令即可。
  • 创建一个包含四列的album表:titleartistprice。每行的id值由DBMS自动创建。
  • 添加四行值。

7 在mysql命令提示符下,运行你刚刚创建的脚本。

使用以下形式的source命令:

mysql> source /path/to/create-tables.sql

8 使用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包中的函数发出的请求转换为数据库可以理解的请求。

1 在你的浏览器中,访问SQLDrivers wiki页面以确定你可以使用的驱动程序。

使用页面上的列表来确定你将使用的驱动程序。为了在本教程中访问MySQL,你将使用Go-MySQL-Driver

2 请注意驱动程序的包名称,此处为github.com/go-sql-driver/mysql

3 使用你的文本编辑器,创建一个用于编写你的Go代码的文件,命名为main.go保存在你之前创建的数据访问目录中。

4 进入main.go,粘贴以下代码导入驱动包。

package main

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

在此代码中,你:

  • 将你的代码添加到main包中,以便可以独立执行它。
  • 导入MySQL驱动程序github.com/go-sql-driver/mysql

导入驱动程序后,你将开始编写代码来访问数据库。

获取数据库句柄并连接

现在编写一些Go代码,使用数据库句柄(handler)访问数据库。

你将使用指向sql.DB结构体的指针,该结构表示对特定数据库的访问。

编写代码

1 在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",
    }
    // 获取一个数据库句柄。
    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结构体使得代码比直接使用连接字符串DSN更具可读性。

  • 调用sql.Open初始化db变量,并传入FormatDSN函数的返回值。
  • 检查sql.Open返回的错误变量。例如,如果数据库连接格式不正确,它可能会连接失败。

为了简化代码,我们调用log.Fatal函数来结束执行并将错误打印到控制台。在生产环境中,你应该以更优雅的方式来处理错误。

  • 调用DB.Ping函数以确认是否正常连接到数据库。在运行时,sql.Open可能不会立即连接到数据库,具体取决于驱动程序。你在这里使用DB.Ping函数来确认database/sql包可以在需要时连接到数据库。
  • 检查Ping函数是否返回错误,以防连接失败。
  • 如果Ping函数连接成功,则打印消息。

2. 在main.go文件的顶部附近,就在包声明的下方,导入支持你刚刚编写的代码的包。

main.go文件的顶部现在应该是这样的:

package main

import (
    "database/sql"
    "fmt"
    "log"
    "os"

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

3. 保存main.go文件

运行这段代码

1. 开始将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声明中。有关跟踪依赖项的详细信息,请参阅添加一个依赖项

在命令提示符下,设置DBUSERDBPASS环境变量以供这个Go程序使用。

在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

3. 在包含main.go的目录中的命令行中,通过键入go run和一个点参数来运行代码,意思是“运行当前目录中的main包”:

$ go run .
Connected!

你可以连接成功!接下来,你将查询一些数据。

查询多行

在本节中,你将使用Go执行一个SQL查询,该查询旨在返回多行。

对于可能返回多行的SQL语句,可以使用database/sql包中的Query方法,然后循环获取它返回的行。(稍后将在查询单行一节中学习如何查询单行。)

编写代码

1. 进入main.go,在func main的正上方,粘贴Album结构体的以下定义。你将使用它来保存从查询返回的行数据。

type Album struct {
    ID     int64
    Title  string
    Artist string
    Price  float32
}

2. 在func main下,粘贴以下albumsByArtist函数以查询数据库:

// albumsByArtist函数查询具有指定艺术家(Artist)姓名的专辑。
func albumsByArtist(name string) ([]Album, error) {
    // 一个切片用于保存返回行数据的相册(Album)信息。
    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()
    // 遍历每一行,使用Scan函数将列数据分配给结构体的对应字段。
    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类型的一个album切片,用来存储来自返回行的数据。结构体字段名称和类型对应于数据库列的名称和类型。
  • 使用DB.Query执行SELECT语句以查询具有指定艺术家(Artist)姓名的专辑。

查询的第一个参数是SQL语句。在该参数之后,可以传递零个或多个任意类型的参数。这些参数的值将依次填入你在SQL语句中使用?号占据的位置。通过将SQL语句与参数值分开(而不是将它们用fmt.Sprintf函数连接成一个字符串),你可以让database/sql包将参数值与SQL文本分开发送,从而消除任何SQL注入风险。

  • defer rows.Close()延迟关闭rows,以便在函数退出时释放它持有的任何资源。
  • 遍历返回的行,使用Rows.Scan将每行的列值分别分配给Album结构体字段。

Scan函数获取指向Go变量的指针列表,对应的列值将写入其中。在这里,你传入&运算符加alb结构体变量中的字段。通过这些指针Scan函数写入以更新结构体的字段。

  • 在循环体内,也检查Scan函数是否有错误返回。
  • 在循环体内,将新的alb变量附加到albums切片。
  • 在循环体之后,使用rows.Err函数检查整个查询是否有错误发生。请注意,如果查询本身失败了,则检查此处的错误是发现结果不完整的唯一方法。

3. 更新你的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函数更简单。

写出代码

1. 在albumsByArtist函数下,粘贴以下albumByID函数的代码:

// albumByID函数使用指定ID来查询相册(album)信息。
func albumByID(id int64) (Album, error) {
    // 声明一个用来存储返回行数据的Album结构体变量。
    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)”。

2. 更新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语句以向数据库添加新行。

你已经了解了如何将QueryQueryRow函数与返回数据的SQL语句一起使用。如果要执行不返回数据的SQL语句,你可以使用Exec函数。

写代码

1. 在albumByID函数下方,粘贴以下addAlbum函数以在数据库中插入新专辑的数据:

// 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
}

在此代码中,你:

Query函数一样,Exec函数接受SQL语句,后跟SQL语句的参数值。

  • 检查尝试INSERT时是否有错误发生。
  • 使用Result.LastInsertId函数检索插入到数据库的新行的ID。
  • 检查尝试检索ID时是否有错误发生。

2. 更新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 GoHow to write Go code中找到有用的最佳实践。
  • Go Tour是对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() {
    // 设置连接参数
    cfg := mysql.Config{
        User:   os.Getenv("DBUSER"),
        Passwd: os.Getenv("DBPASS"),
        Net:    "tcp",
        Addr:   "127.0.0.1:3306",
        DBName: "recordings",
    }
    // 获取一个数据库句柄
    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)

    // 获取ID为2的相册的信息,测试查询语句
    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函数查询具有指定艺术家(Artist)姓名的专辑。
func albumsByArtist(name string) ([]Album, error) {
    // 一个切片用于保存返回行数据的相册(Album)信息。
    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()
    // 遍历每一行,使用Scan函数将列数据分配给结构体的对应字段。
    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函数使用指定ID来查询相册(album)信息。
func albumByID(id int64) (Album, error) {
    // 声明一个用来存储返回行数据的Album结构体变量。
    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函数添加指定专辑的数据到数据库,返回这条新记录的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
}