教程:訪問關係型資料庫
本教程介紹使用 Go 及其標準庫中的 database/sql
包訪問關係型資料庫的基礎知識。
如果您對 Go 及其工具鏈有基本瞭解,將能更好地利用本教程。如果您是第一次接觸 Go,請參閱教程:開始使用 Go 進行快速入門。
您將使用的 database/sql
包包含用於連線資料庫、執行事務、取消正在進行的操作等的型別和函式。有關使用此包的更多詳細資訊,請參閱訪問資料庫。
在本教程中,您將建立一個數據庫,然後編寫程式碼來訪問該資料庫。您的示例專案將是一個關於老式爵士樂唱片的資料儲存庫。
在本教程中,您將按以下章節進行學習:
- 為您的程式碼建立一個資料夾。
- 設定資料庫。
- 匯入資料庫驅動程式。
- 獲取資料庫控制代碼並連線。
- 查詢多行資料。
- 查詢單行資料。
- 新增資料。
注意:有關其他教程,請參閱教程。
前提條件
- 安裝 MySQL 關係型資料庫管理系統 (DBMS)。
- 安裝 Go。有關安裝說明,請參閱安裝 Go。
- 一個程式碼編輯工具。您擁有的任何文字編輯器都可以正常工作。
- 一個命令列終端。Go 在 Linux 和 Mac 上的任何終端,以及 Windows 上的 PowerShell 或 cmd 中都能很好地執行。
為您的程式碼建立一個資料夾
首先,為您的程式碼建立一個資料夾。
-
開啟命令列提示符並切換到您的主目錄。
在 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() { // Capture connection properties. cfg := mysql.NewConfig() cfg.User = os.Getenv("DBUSER") cfg.Passwd = os.Getenv("DBPASS") cfg.Net = "tcp" cfg.Addr = "127.0.0.1:3306" cfg.DBName = "recordings" // 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!") }
在此程式碼中,您將執行以下操作:
-
宣告一個型別為
*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 宣告下方,匯入支援您剛編寫的程式碼所需的包。
檔案頂部現在應該如下所示:
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: added filippo.io/edwards25519 v1.1.0 go: added github.com/go-sql-driver/mysql v1.8.1
由於您在上一步中將其新增到了
import
宣告中,Go 下載了這個依賴項。有關依賴項跟蹤的更多資訊,請參閱新增依賴項。 -
在命令列提示符下,為 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 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 }
在此程式碼中,您將執行以下操作:
-
宣告一個您定義的
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 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 }
在此程式碼中,您將執行以下操作:
-
使用
DB.QueryRow
執行SELECT
語句,查詢指定 ID 的專輯。它返回一個
sql.Row
。為了簡化呼叫程式碼(您的程式碼!),QueryRow
不會返回錯誤。相反,它會安排在稍後的Rows.Scan
中返回任何查詢錯誤(例如sql.ErrNoRows
)。 -
使用
Row.Scan
將列值複製到結構體欄位中。 -
檢查
Scan
返回的錯誤。特殊的錯誤
sql.ErrNoRows
表示查詢沒有返回任何行。通常,這個錯誤值得替換為更具體的文字,例如這裡的“沒有這樣的專輯”。
-
-
更新
main
函式以呼叫albumByID
。在
func main
的末尾,新增以下程式碼。// 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)
在新的程式碼中,您現在將執行以下操作:
-
呼叫您新增的
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 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 }
在此程式碼中,您將執行以下操作:
-
使用
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.NewConfig()
cfg.User = os.Getenv("DBUSER")
cfg.Passwd = os.Getenv("DBPASS")
cfg.Net = "tcp"
cfg.Addr = "127.0.0.1:3306"
cfg.DBName = "recordings"
// 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
}