Go - database/sql package 簡介

2015-05-23

前言

用 Go 寫程式,要對 SQL 資料庫存取時,就需要依賴 database/sql 這個 package。這篇文章將針對這個 package 裡的 API 做介紹,順便加深自己的印象。

前置準備

為了方便,這裡使用 sqlite 的 in-memory database,driver 使用的是 go-sqlite3

安裝:

go get github.com/mattn/go-sqlite3

測試用程式:

package main

import (
    "database/sql"
    _ "github.com/mattn/go-sqlite3"
    "log"
)

func main() {
    db := createTestDB()
    defer db.Close()

    // write your code here
}

func createTestDB() *sql.DB {
    db, err := sql.Open("sqlite3", ":memory:")
    if err != nil {
        log.Fatal(err)
    }

    _, err = db.Exec("CREATE TABLE foo (id INTEGER PRIMARY KEY ASC, name TEXT)")
    if err != nil {
        log.Fatal(err)
    }
    return db
}

使用 Driver

要使用 database/sql 裡的 API,一定得要搭配一套實作 database/sql/driver 的 driver,Go 內建並不支援任何資料庫,但官方有整理出來目前可使用的 driver 清單。這些 driver 大都會在自己的 package 實作 init function,並在裡面將自己的 driver 註冊,所以使用者只需要 import 即可。

這次並不會使用 go-sqlite3 自己的 API;為了避免 Go Compiler 抱怨 package 沒在使用,需要加上 blank identifier

import (
    _ "github.com/mattn/go-sqlite3"
)

建立 DB 物件

Go 操作資料庫的 API 都圍繞在 DB 這個物件上,除了 API 以外,它也會在內部管理著 Connection Pool,並且保證能安全地在多執行緒的程式內使用。常見的資料庫 API 不少是以一個連線為一個物件的概念來操作,可是 DB 一旦建立起來,就可以一直使用到程式結束為止再關閉,不需要每次使用時重新開啟與關閉。

建立的方式是透過 sql.Open(),輸入 driver 名字和 DSN 即可。

db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
    log.Fatal(err)
}
// 操作 db...

執行 SQL

執行如 INSERTUPDATE 等不需要回傳查詢資料的 SQL,可以使用 DB.Exec() 這個 method:

_, err = db.Exec("CREATE TABLE foo (id INTEGER PRIMARY KEY ASC, name TEXT)")
if err != nil {
    log.Fatal(err)
}

這個 method 支援 placeholder 語法,自動視需要跳脫字元,例如:

result, err := db.Exec("INSERT INTO foo VALUES (NULL, ?)", "escape'me")
if err != nil {
    log.Fatal(err)
}

除了有錯誤時會回傳 error 以外,DB.Exec() 還會回傳 Result 這個物件。這個物件代表著 SQL 的執行結果,透過它可以知道剛剛執行 SQL 後所產生的 auto increment id,或者是受影響的資料筆數。

id, _ := result.LastInsertId()
log.Printf("ID: %d\n", id)  // "ID: 1"

查詢資料

需要查詢資料時,可以使用 DB.Query()DB.QueryRow() 兩個 method。這兩個 method 的差別在於 DB.Query() 會把所有的結果傳回來,而 DB.QueryRow() 最多只會傳第一筆回來。和 DB.Exec() 相同,它們也都支援 placeholder 語法 :

rows, err := db.Query("SELECT * FROM foo")
// or
row := db.QueryRow("SELECT * FROM foo WHERE id = ?", 1)

讀查詢出來的資料

DB.Query() 回傳的物件 Rows 代表著零至多筆的查詢結果,使用 Rows.Scan() 可以將第一行的資料寫入帶進去的參數指標內,接著再呼叫 Rows.Next() 就可以移至下一行:

for rows.Next() {
    var id int
    var name string
    err = rows.Scan(&id, &name)
    if err != nil {
        log.Fatalf("Scan error: %q\n", err)
    }
    log.Printf("Query(): ID: %d, name %s", id, name)
}

DB.QueryRow() 回傳的物件 Row 代表著零或一筆查詢結果,和 Rows 一樣,可以使用 Row.Scan() 把資料寫進指標內。

var id int
var name string
row := db.QueryRow("SELECT * FROM foo WHERE id = ?", 1)
err := row.Scan(&id, &name)
if err != nil {
    log.Fatal(err)
}
log.Printf("QueryRow(): ID: %d, name %s", id, name)

Prepared Statement

Go 也有支援 Prepared Statement,使用 DB.Prepare() 就可以建立出 Stmt 物件

stmt, err := db.Prepare("INSERT INTO foo VALUES (NULL, ?)")
if err != nil {
    log.Fatal(err)
}

Stmt 物件也提供了 Stmt.Exec(), Stmt.Query(), Stmt.QueryRow() 三個執行 SQL 的 API;和前面不同的是,只需要將 placeholder 的參數帶進去即可。

for i := 0; i < 3; i++ {
    result, err := stmt.Exec("test" + string(i))
    if err != nil {
        log.Fatalf("Prepare error: %q", err)
    }
    id, _ := result.LastInsertId()
    log.Printf("Prepare insert ID: %d", id)
}

Transaction

要使用資料庫 Transaction,就需要靠 DB.Begin() 這個 method 建立出 Tx 物件:

tx, err := db.Begin()

透過同一個 Tx 物件所執行的動作,都會在同一個 Transaction 內。它提供和前面提過的 method 一樣的 API 來執行 SQL: Tx.Exec(), Tx.Query(), Tx.QueryRow(), Tx.Prepare()

result, err := tx.Exec("UPDATE foo SET name = 'abc'")
if err != nil {
    tx.Rollback()
    log.Fatal(err)
}
count, _ := result.RowsAffected()
log.Printf("Tx RowsAffected(): %d", count)
tx.Commit()

當然免不了的要記得呼叫 Tx.Commit(), Tx.Rollback()來決定 Transaction 是否完成,一旦呼叫,這個 Tx 物件就不能再被用來執行 SQL,只能再重新開一個新 Transaction。

感想

database/sql 麻雀雖小,五臟俱全,現代資料庫操作會想要有的基本功能都支援了。除此之外,API 也十分簡潔一致;不同物件要執行 SQL 所用的 API signature 差異很小。上手快,不太需要一直查文件才會記得。

Reference

  1. Package Sql http://golang.org/pkg/database/sql/