關於Golang中database/sql包的學習筆記

由於最近在學習Go,因此找了revel這個框架來學習,感受和php的面向對象有很大不一樣。revel沒有提供db mapping的組件,因此在github上搜了不少ORM來學習,在jmoiron/sqlx中發現了一篇比較詳細介紹database/sql這個包的文章,拿來和你們分享。本文並非按字句的翻譯,若是哪裏表述不清楚建議閱讀原文 原文地址php

概述

sql.DB不是一個鏈接,它是數據庫的抽象接口。它能夠根據driver打開關閉數據庫鏈接,管理鏈接池。正在使用的鏈接被標記爲繁忙,用完後回到鏈接池等待下次使用。因此,若是你沒有把鏈接釋放回鏈接池,會致使過多鏈接使系統資源耗盡。html

使用DB

導入driver

這裏使用的是MySQL driversmysql

import (
    "database/sql"
    _ "github.com/go-sql-driver/mysql"
)

鏈接DB

func main() {
    db, err := sql.Open("mysql",
        "user:password@tcp(127.0.0.1:3306)/hello")
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()
}

sql.Open的第一個參數是driver名稱,第二個參數是driver鏈接數據庫的信息,各個driver可能不一樣。DB不是鏈接,而且只有當須要使用時纔會建立鏈接,若是想當即驗證鏈接,須要用Ping()方法,以下:git

err = db.Ping()
if err != nil {
    // do something here
}

sql.DB的設計就是用來做爲長鏈接使用的。不要頻繁Open, Close。比較好的作法是,爲每一個不一樣的datastore建一個DB對象,保持這些對象Open。若是須要短鏈接,那麼把DB做爲參數傳入function,而不要在function中Open, Close。程序員

讀取DB

若是方法包含Query,那麼這個方法是用於查詢並返回rows的。其餘狀況應該用Exec()github

var (
    id int
    name string
)
rows, err := db.Query("select id, name from users where id = ?", 1)
if err != nil {
    log.Fatal(err)
}
defer rows.Close()
for rows.Next() {
    err := rows.Scan(&id, &name)
    if err != nil {
        log.Fatal(err)
    }
    log.Println(id, name)
}
err = rows.Err()
if err != nil {
    log.Fatal(err)
}

上面代碼的過程爲:db.Query()表示向數據庫發送一個query,defer rows.Close()很是重要,遍歷rows使用rows.Next(), 把遍歷到的數據存入變量使用rows.Scan(), 遍歷完成後檢查error。有幾點須要注意:sql

  1. 檢查遍歷是否有error
  2. 結果集(rows)未關閉前,底層的鏈接處於繁忙狀態。當遍歷讀到最後一條記錄時,會發生一個內部EOF錯誤,自動調用rows.Close(),可是若是提早退出循環,rows不會關閉,鏈接不會回到鏈接池中,鏈接也不會關閉。因此手動關閉很是重要。rows.Close()能夠屢次調用,是無害操做。

單行Query

err在Scan後才產生,因此能夠以下寫:數據庫

var name string
err = db.QueryRow("select name from users where id = ?", 1).Scan(&name)
if err != nil {
    log.Fatal(err)
}
fmt.Println(name)

修改數據,事務

通常用Prepared Statements和Exec()完成INSERT, UPDATE, DELETE操做。安全

stmt, err := db.Prepare("INSERT INTO users(name) VALUES(?)")
if err != nil {
    log.Fatal(err)
}
res, err := stmt.Exec("Dolly")
if err != nil {
    log.Fatal(err)
}
lastId, err := res.LastInsertId()
if err != nil {
    log.Fatal(err)
}
rowCnt, err := res.RowsAffected()
if err != nil {
    log.Fatal(err)
}
log.Printf("ID = %d, affected = %d\n", lastId, rowCnt)

事務

db.Begin()開始事務,Commit()Rollback()關閉事務。Tx從鏈接池中取出一個鏈接,在關閉以前都是使用這個鏈接。Tx不能和DB層的BEGIN, COMMIT混合使用。服務器

若是你須要經過多條語句修改鏈接狀態,你必須使用Tx,例如:

  • 建立僅對單個鏈接可見的臨時表
  • 設置變量,例如SET @var := somevalue
  • 改變鏈接選項,例如字符集,超時

Prepared Statements

Prepared Statements and Connection

在數據庫層面,Prepared Statements是和單個數據庫鏈接綁定的。客戶端發送一個有佔位符的statement到服務端,服務器返回一個statement ID,而後客戶端發送ID和參數來執行statement。

在GO中,鏈接不直接暴露,你不能爲鏈接綁定statement,而是隻能爲DB或Tx綁定。database/sql包有自動重試等功能。當你生成一個Prepared Statement

  1. 自動在鏈接池中綁定到一個空閒鏈接
  2. Stmt對象記住綁定了哪一個鏈接
  3. 執行Stmt時,嘗試使用該鏈接。若是不可用,例如鏈接被關閉或繁忙中,會自動re-prepare,綁定到另外一個鏈接。

這就致使在高併發的場景,過分使用statement可能致使statement泄漏,statement持續重複prepare和re-prepare的過程,甚至會達到服務器端statement數量上限。

某些操做使用了PS,例如db.Query(sql, param1, param2), 並在最後自動關閉statement。

有些場景不適合用statement:

  1. 數據庫不支持。例如Sphinx,MemSQL。他們支持MySQL wire protocol, 但不支持"binary" protocol。
  2. statement不須要重用不少次,而且有其餘方法保證安全。例子

在Transaction中使用PS

PS在Tx中惟一綁定一個鏈接,不會re-prepare。

Tx和statement不能分離,在DB中建立的statement也不能在Tx中使用,由於他們一定不是使用同一個鏈接使用Tx必須十分當心,例以下面的代碼:

tx, err := db.Begin()
if err != nil {
    log.Fatal(err)
}
defer tx.Rollback()
stmt, err := tx.Prepare("INSERT INTO foo VALUES (?)")
if err != nil {
    log.Fatal(err)
}
defer stmt.Close() // danger!
for i := 0; i < 10; i++ {
    _, err = stmt.Exec(i)
    if err != nil {
        log.Fatal(err)
    }
}
err = tx.Commit()
if err != nil {
    log.Fatal(err)
}
// stmt.Close() runs here!

*sql.Tx一旦釋放,鏈接就回到鏈接池中,這裏stmt在關閉時就沒法找到鏈接。因此必須在Tx commit或rollback以前關閉statement。

處理Error

循環Rows的Error

若是循環中發生錯誤會自動運行rows.Close(),用rows.Err()接收這個錯誤,Close方法能夠屢次調用。循環以後判斷error是很是必要的。

for rows.Next() {
    // ...
}
if err = rows.Err(); err != nil {
    // handle the error here
}

關閉Resultsets時的error

若是你在rows遍歷結束以前退出循環,必須手動關閉Resultset,而且接收error。

for rows.Next() {
    // ...
    break; // whoops, rows is not closed! memory leak...
}
// do the usual "if err = rows.Err()" [omitted here]...
// it's always safe to [re?]close here:
if err = rows.Close(); err != nil {
    // but what should we do if there's an error?
    log.Println(err)
}

QueryRow()的error

var name string
err = db.QueryRow("select name from users where id = ?", 1).Scan(&name)
if err != nil {
    log.Fatal(err)
}
fmt.Println(name)

若是id爲1的不存在,err爲sql.ErrNoRows,通常應用中不存在的狀況都須要單獨處理。此外,Query返回的錯誤都會延遲到Scan被調用,因此應該寫成以下代碼:

var name string
err = db.QueryRow("select name from users where id = ?", 1).Scan(&name)
if err != nil {
    if err == sql.ErrNoRows {
        // there were no rows, but otherwise no error occurred
    } else {
        log.Fatal(err)
    }
}
fmt.Println(name)

把空結果當作Error處理是爲了強行讓程序員處理結果爲空的狀況

分析數據庫Error

各個數據庫處理方式不太同樣,mysql爲例:

if driverErr, ok := err.(*mysql.MySQLError); ok { 
    // Now the error number is accessible directly
    if driverErr.Number == 1045 {
        // Handle the permission-denied error
    }
}

MySQLError, Number都是DB特異的,別的數據庫多是別的類型或字段。這裏的數字能夠替換爲常量,例如這個包 MySQL error numbers maintained by VividCortex

鏈接錯誤

NULL值處理

簡單說就是設計數據庫的時候不要出現null,處理起來很是費力。Null的type頗有限,例如沒有sql.NullUint64; null值沒有默認零值。

for rows.Next() {
    var s sql.NullString
    err := rows.Scan(&s)
    // check err
    if s.Valid {
       // use s.String
    } else {
       // NULL value
    }
}

未知Column

rows.Columns()的使用,用於處理不能得知結果字段個數或類型的狀況,例如:

cols, err := rows.Columns()
if err != nil {
    // handle the error
} else {
    dest := []interface{}{ // Standard MySQL columns
        new(uint64), // id
        new(string), // host
        new(string), // user
        new(string), // db
        new(string), // command
        new(uint32), // time
        new(string), // state
        new(string), // info
    }
    if len(cols) == 11 {
        // Percona Server
    } else if len(cols) > 8 {
        // Handle this case
    }
    err = rows.Scan(dest...)
    // Work with the values in dest
}
cols, err := rows.Columns() // Remember to check err afterwards
vals := make([]interface{}, len(cols))
for i, _ := range cols {
    vals[i] = new(sql.RawBytes)
}
for rows.Next() {
    err = rows.Scan(vals...)
    // Now you can check each element of vals for nil-ness,
    // and you can use type introspection and type assertions
    // to fetch the column into a typed variable.
}

關於鏈接池

  1. 避免錯誤操做,例如LOCK TABLE後用 INSERT會死鎖,由於兩個操做不是同一個鏈接,insert的鏈接沒有table lock。
  2. 當須要鏈接,且鏈接池中沒有可用鏈接時,新的鏈接就會被建立。
  3. 默認沒有鏈接上限,你能夠設置一個,但這可能會致使數據庫產生錯誤「too many connections」
  4. db.SetMaxIdleConns(N)設置最大空閒鏈接數
  5. db.SetMaxOpenConns(N)設置最大打開鏈接數
  6. 長時間保持空閒鏈接可能會致使db timeout
相關文章
相關標籤/搜索