由於最近在學習Go,因此找了revel這個框架來學習,感受和php的面向對象有很大不一樣。revel沒有提供db mapping的組件,因此在github上搜了不少ORM來學習,在
jmoiron/sqlx
中發現了一篇比較詳細介紹database/sql
這個包的文章,拿來和你們分享。本文並非按字句的翻譯,若是哪裏表述不清楚建議閱讀原文 原文地址php
sql.DB
不是一個鏈接,它是數據庫的抽象接口。它能夠根據driver打開關閉數據庫鏈接,管理鏈接池。正在使用的鏈接被標記爲繁忙,用完後回到鏈接池等待下次使用。因此,若是你沒有把鏈接釋放回鏈接池,會致使過多鏈接使系統資源耗盡。html
這裏使用的是MySQL driversmysql
import ( "database/sql" _ "github.com/go-sql-driver/mysql" )
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。程序員
若是方法包含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
rows.Close()
,可是若是提早退出循環,rows不會關閉,鏈接不會回到鏈接池中,鏈接也不會關閉。因此手動關閉很是重要。rows.Close()
能夠屢次調用,是無害操做。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是和單個數據庫鏈接綁定的。客戶端發送一個有佔位符的statement到服務端,服務器返回一個statement ID,而後客戶端發送ID和參數來執行statement。
在GO中,鏈接不直接暴露,你不能爲鏈接綁定statement,而是隻能爲DB或Tx綁定。database/sql
包有自動重試等功能。當你生成一個Prepared Statement
Stmt
對象記住綁定了哪一個鏈接Stmt
時,嘗試使用該鏈接。若是不可用,例如鏈接被關閉或繁忙中,會自動re-prepare,綁定到另外一個鏈接。這就致使在高併發的場景,過分使用statement可能致使statement泄漏,statement持續重複prepare和re-prepare的過程,甚至會達到服務器端statement數量上限。
某些操做使用了PS,例如db.Query(sql, param1, param2)
, 並在最後自動關閉statement。
有些場景不適合用statement:
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。
若是循環中發生錯誤會自動運行rows.Close()
,用rows.Err()
接收這個錯誤,Close方法能夠屢次調用。循環以後判斷error是很是必要的。
for rows.Next() { // ... } if err = rows.Err(); err != nil { // handle the error here }
若是你在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) }
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處理是爲了強行讓程序員處理結果爲空的狀況
各個數據庫處理方式不太同樣,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的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 } }
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. }
db.SetMaxIdleConns(N)
設置最大空閒鏈接數db.SetMaxOpenConns(N)
設置最大打開鏈接數