• English
  • 存取關聯式資料庫

    本教學介紹了如何使用 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. 在你的文字編輯器中,在 data-access 資料夾中,建立一個名為 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. 使用你的文字編輯器,在之前建立的 data-access 目錄中建立一個用於撰寫 Go 程式碼的檔案,並將其儲存為 main.go

    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 中,緊接在你剛加入的 import 程式碼下方,貼上以下 Go 程式碼以建立資料庫控制代碼。

      var db *sql.DB
      
      func main() {
      	// 捕獲連接屬性。
      	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"
      
      	// 取得資料庫控制代碼。
      	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.DBdb 變數。這是你的資料庫控制代碼。

        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 getgithub.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 下載了這個依賴項。有關依賴項追蹤的更多資訊,請參閱 加入依賴項

    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 查詢具有指定藝術家名稱的專輯。
      func albumsByArtist(name string) ([]Album, error) {
      	// 一個 albums 切片,用於儲存返回行中的資料。
      	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 類型的 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 查詢具有指定 ID 的專輯。
      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 函數以在資料庫中插入新專輯,然後儲存 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 的錯誤。

    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 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() {
    	// 捕獲連接屬性。
    	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"
    
    	// 取得資料庫控制代碼。
    	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 查詢具有指定藝術家名稱的專輯。
    func albumsByArtist(name string) ([]Album, error) {
    	// 一個 albums 切片,用於儲存返回行中的資料。
    	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 的專輯。
    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
    }