教學課程:存取關聯式資料庫

本教學課程將介紹使用 Go 和其標準函式庫中的 database/sql 套件存取關聯式資料庫的基本知識。

如果您對 Go 及其工具具備基本認識,將能充分利用本教學課程。如果您是第一次接觸 Go,請參閱 教學課程:開始使用 Go 以取得快速入門。

您將使用的 database/sql 套件包含用於連線至資料庫、執行交易、取消進行中的作業等類型和函式。如需有關使用套件的更多詳細資訊,請參閱 存取資料庫

在本教學課程中,您將建立一個資料庫,然後撰寫程式碼來存取資料庫。您的範例專案將會是關於復古爵士唱片資料的儲存庫。

在本教學課程中,您將逐步完成下列各節

  1. 為您的程式碼建立一個資料夾。
  2. 設定資料庫。
  3. 匯入資料庫驅動程式。
  4. 取得資料庫控制代碼並連線。
  5. 查詢多列。
  6. 查詢單一列。
  7. 新增資料。

注意:如需其他教學課程,請參閱 教學課程

先決條件

為您的程式碼建立一個資料夾

首先,為您要撰寫的程式碼建立一個資料夾。

  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 程式碼中,您

    • 刪除 (drop) 名為 album 的表格。首先執行此命令,讓您在稍後想要重新開始使用表格時,可以更輕鬆地重新執行程式碼。

    • 建立一個具有四個欄位的 album 表格:titleartistprice。每個列的 id 值會由 DBMS 自動建立。

    • 新增四列具有值。

  7. mysql 命令提示字元中,執行您剛才建立的程式碼。

    您將使用以下格式的 source 命令

    mysql> source /path/to/create-tables.sql
    
  8. 在您的 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 套件中的函式所提出的要求轉換成資料庫可以理解的要求。

  1. 在您的瀏覽器中,前往 SQLDrivers wiki 頁面,以找出您可以使用的驅動程式。

    使用頁面上的清單來找出您將使用的驅動程式。在本教學課程中存取 MySQL 時,您將使用 Go-MySQL-Driver

  2. 記下驅動程式的套件名稱,在此為 github.com/go-sql-driver/mysql

  3. 使用您的文字編輯器,建立一個檔案來撰寫您的 Go 程式碼,並將檔案儲存為 main.go,位於您先前建立的 data-access 目錄中。

  4. 在 main.go 中,貼上以下程式碼來匯入驅動程式套件。

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

    在此程式碼中,您

    • 將您的程式碼新增到 main 套件,以便您可以獨立執行它。

    • 匯入 MySQL 驅動程式 github.com/go-sql-driver/mysql

匯入驅動程式後,您將開始撰寫程式碼來存取資料庫。

取得資料庫控制代碼並進行連線

現在撰寫一些 Go 程式碼,讓您可以使用資料庫控制代碼存取資料庫。

您將使用指向 sql.DB 結構的指標,它代表對特定資料庫的存取權。

撰寫程式碼

  1. 在 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 連線成功,請列印訊息。

  2. 在 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 宣告。如需瞭解更多關於相依性追蹤的資訊,請參閱 新增相依性

  2. 在命令提示字元中,設定 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 搭配一個點引數來執行程式碼,表示「執行目前目錄中的套件」。

    $ 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 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 檢查整體查詢的錯誤。請注意,如果查詢本身失敗,在此處檢查錯誤是找出結果不完整的唯一方法。

  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 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` 表示查詢沒有傳回列。通常值得用更具體的文字取代該錯誤,例如這裡的「沒有此專輯」。

  2. 更新 `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`。

撰寫程式碼

  1. 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 的錯誤。

  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 對關聯式資料庫執行簡單的動作。

建議的後續主題

已完成的程式碼

此區段包含您使用本教學課程建置的應用程式程式碼。

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
}