教學課程:存取關聯式資料庫
本教學課程將介紹使用 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
-
在您的文字編輯器中,在資料存取資料夾中,建立一個稱為 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 中,貼上以下 Go 程式碼,以建立資料庫控制代碼,貼在您剛才新增的
import
程式碼下方。var db *sql.DB 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", } // 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 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
宣告。如需瞭解更多關於相依性追蹤的資訊,請參閱 新增相依性。 -
在命令提示字元中,設定
DBUSER
和DBPASS
環境變數,供 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
-
在包含 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 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() {
// Capture connection properties.
cfg := mysql.Config{
User: os.Getenv("DBUSER"),
Passwd: os.Getenv("DBPASS"),
Net: "tcp",
Addr: "127.0.0.1:3306",
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
}