Go基礎之--操作Mysql(一)
關於標准庫database/sql
database/sql是golang的標准庫之一,它提供了一系列接口方法,用於訪問關系數據庫。它並不會提供數據庫特有的方法,那些特有的方法交給數據庫驅動去實現。
database/sql庫提供了一些type。這些類型對掌握它的用法非常重要。
DB
數據庫對象。 sql.DB類型代表了數據庫。和其他語言不一樣,它並是數據庫連接。golang中的連接來自內部實現的連接池,連接的建立是惰性的,當你需要連接的時候,連接池會自動幫你創建。通常你不需要操作連接池。一切都有go來幫你完成。
Results
結果集。數據庫查詢的時候,都會有結果集。sql.Rows類型表示查詢返回多行數據的結果集。sql.Row則表示單行查詢結果的結果集。當然,對於插入更新和刪除,返回的結果集類型為sql.Result。
Statements
語句。sql.Stmt類型表示sql查詢語句,例如DDL,DML等類似的sql語句。可以把當成prepare語句構造查詢,也可以直接使用sql.DB的函數對其操作。
而通常工作中我們可能更多的是用https://github.com/jmoiron/sqlx包來操作數據庫
sqlx是基於標准庫database/sql的擴展,並且我們可以通過sqlx操作各種類型的數據如
和其他語言不通的是,查詢數據庫的時候需要創建一個連接,對於go而言則是需要創建一個數據庫對象,連接將會在查詢需要的時候,由連接池創建並維護,使用sql.Open函數創建數據庫對象,第一個參數是數據庫驅動名,第二個參數是一個連接字符串
關於數據庫的增刪查改
增加數據
關於增加數據幾個小知識點:
- 關於插入數據的時候占位符是通過問號:?
- 插入數據的后可以通過LastInsertId可以獲取插入數據的id
- 通過RowsAffected可以獲取受影響的行數
- 執行sql語句是通過exec
一個簡單的使用例子:
package main import ( "github.com/jmoiron/sqlx" _ "github.com/go-sql-driver/mysql" "fmt" ) func main() { Db,err:=sqlx.Open("mysql","root:123456@tcp(192.168.14.7:3306)/godb") if err != nil{ fmt.Println("connect to mysql failed,",err) return } defer Db.Close() fmt.Println("connect to mysql success") //執行sql語句,切記這里的占位符是? result,err := Db.Exec("INSERT INTO user_info(username,sex,email)VALUES (?,?,?)","user01","男","8989@qq.com") if err != nil{ fmt.Println("insert failed,",err) } // 通過LastInsertId可以獲取插入數據的id userId,err:= result.LastInsertId() // 通過RowsAffected可以獲取受影響的行數 rowCount,err:=result.RowsAffected() fmt.Println("user_id:",userId) fmt.Println("rowCount:",rowCount) }
通過Exec方法插入數據,返回的結果是一個sql.Result類型
查詢數據
下面是一個查詢的例子代碼:
//執行查詢操作 rows,err := Db.Query("SELECT email FROM user_info WHERE user_id>=5") if err != nil{ fmt.Println("select db failed,err:",err) return } // 這里獲取的rows是從數據庫查的滿足user_id>=5的所有行的email信息,rows.Next(),用於循環獲取所有 for rows.Next(){ var s string err = rows.Scan(&s) if err != nil{ fmt.Println(err) return } fmt.Println(s) } rows.Close()
使用了Query方法執行select查詢語句,返回的是一個sql.Rows類型的結果集
迭代后者的Next方法,然后使用Scan方法給變量s賦值,以便取出結果。最后再把結果集關閉(釋放連接)。
同樣的我們還可以通過Exec方式執行查詢語句
但是因為Exec返回的是一個sql.Result類型,從官網這里:
https://golang.google.cn/pkg/database/sql/#type Result
我們可以直接這個接口里只有兩個方法:LastInsertId(),RowsAffected()
我們還可以通過Db.Get()方法獲取查詢的數據,將查詢的數據保存到一個結構體中
//Get執行查詢操作 type user_info struct { Username string `db:"username"` Email string `db:"email"` } var userInfo user_info err = Db.Get(&userInfo,"SELECT username,email FROM user_info WHERE user_id=5") if err != nil{ fmt.Println(err) return } fmt.Println(userInfo)
這樣獲取的一個數據,如果我們需要獲取多行數據信息還可以通過Db.Select方法獲取數據,代碼例子為:
var userList []*user_info err = Db.Select(&userList,"SELECT username,email FROM user_info WHERE user_id>5") if err != nil{ fmt.Println(err) return } fmt.Println(userList) for _,v:= range userList{ fmt.Println(v) }
通過Db.Select方法將查詢的多行數據保存在一個切片中,然后就可以通過循環的方式獲取每行數據
更新數據
下面是一個更新的例子,這里是通過Exec的方式執行的
//更新數據 results,err := Db.Exec("UPDATE user_info SET username=? where user_id=?","golang",5) if err != nil{ fmt.Println("update data fail,err:",err) return } fmt.Println(results.RowsAffected())
刪除數據
下面是一個刪除的例子,同樣是通過Exec的方式執行的
//刪除數據 results,err := Db.Exec("DELETE from user_info where user_id=?",5) if err != nil{ fmt.Println("delete data fail,err:",err) return } fmt.Println(results.RowsAffected())
通過上面的簡單例子,對golang操作mysql的增刪查改,有了一個基本的了解,下面整理一下重點內容
sql.DB
當我們調用sqlx.Open()可以獲取一個sql.DB對象,sql.DB是數據庫的抽象,切記它不是數據庫連接,sqlx.Open()只是驗證數據庫參數,並沒不創建數據庫連接。sql.DB提供了和數據庫交互的函數,同時也管理維護一個數據庫連接池,並且對於多gegoroutines也是安全的
sql.DB表示是數據庫抽象,因此你有幾個數據庫就需要為每一個數據庫創建一個sql.DB對象。因為它維護了一個連接池,因此不需要頻繁的創建和銷毀。
連接池
只用sql.Open函數創建連接池,可是此時只是初始化了連接池,並沒有創建任何連接。連接創建都是惰性的,只有當真正使用到連接的時候,連接池才會創建連接。連接池很重要,它直接影響着你的程序行為。
連接池的工作原來卻相當簡單。當你的函數(例如Exec,Query)調用需要訪問底層數據庫的時候,函數首先會向連接池請求一個連接。如果連接池有空閑的連接,則返回給函數。否則連接池將會創建一個新的連接給函數。一旦連接給了函數,連接則歸屬於函數。函數執行完畢后,要不把連接所屬權歸還給連接池,要么傳遞給下一個需要連接的(Rows)對象,最后使用完連接的對象也會把連接釋放回到連接池。
請求連接的函數有幾個,執行完畢處理連接的方式也不同:
- db.Ping() 調用完畢后會馬上把連接返回給連接池。
- db.Exec() 調用完畢后會馬上把連接返回給連接池,但是它返回的Result對象還保留這連接的引用,當后面的代碼需要處理結果集的時候連接將會被重用。
- db.Query() 調用完畢后會將連接傳遞給sql.Rows類型,當然后者迭代完畢或者顯示的調用.Clonse()方法后,連接將會被釋放回到連接池。
- db.QueryRow()調用完畢后會將連接傳遞給sql.Row類型,當.Scan()方法調用之后把連接釋放回到連接池。
- db.Begin() 調用完畢后將連接傳遞給sql.Tx類型對象,當.Commit()或.Rollback()方法調用后釋放連接。
每個連接都是惰性的,如何驗證sql.Open調用之后,sql.DB對象可用,通過db.Ping()初始化
代碼例子:
package main import ( "github.com/jmoiron/sqlx" _ "github.com/go-sql-driver/mysql" "fmt" ) func main() { Db, err := sqlx.Open("mysql", "root:123456@tcp(192.168.50.166:3306)/godb") if err != nil { fmt.Println("connect to mysql failed,", err) return } defer Db.Close() fmt.Println("connect to mysql success") err = Db.Ping() if err != nil{ fmt.Println(err) return } fmt.Println("ping success") }
需要知道:當調用了ping之后,連接池一定會初始化一個數據連接
連接失敗
database/sql 其實幫我們做了很多事情,我們不用見擦汗連接失敗的情況,當我們進行數據庫操作的時候,如果連接失敗,database/sql 會幫我們處理,它會自動連接2次,這個如果查看源碼中我們可以看到如下的代碼:
// ExecContext executes a query without returning any rows. // The args are for any placeholder parameters in the query. func (db *DB) ExecContext(ctx context.Context, query string, args ...interface{}) (Result, error) { var res Result var err error for i := 0; i < maxBadConnRetries; i++ { res, err = db.exec(ctx, query, args, cachedOrNewConn) if err != driver.ErrBadConn { break } } if err == driver.ErrBadConn { return db.exec(ctx, query, args, alwaysNewConn) } return res, err }
上述代碼中變量maxBadConnRetries小時如果連接失敗嘗試的次數,默認是2
關於連接池配置
db.SetMaxIdleConns(n int) 設置連接池中的保持連接的最大連接數。默認也是0,表示連接池不會保持釋放會連接池中的連接的連接狀態:即當連接釋放回到連接池的時候,連接將會被關閉。這會導致連接再連接池中頻繁的關閉和創建。
db.SetMaxOpenConns(n int) 設置打開數據庫的最大連接數。包含正在使用的連接和連接池的連接。
如果你的函數調用需要申請一個連接,並且連接池已經沒有了連接或者連接數達到了最大連接數。此時的函數調用將會被block,直到有可用的連接才會返回。
設置這個值可以避免並發太高導致連接mysql出現too many connections的錯誤。該函數的默認設置是0,表示無限制。
db.SetConnMaxLifetime(d time.Duration) 設置連接可以被使用的最長有效時間,如果過期,連接將被拒絕
讀取數據
在上一篇文章中整理查詢數據的時候,使用了Query的方法查詢,其實database/sql還提供了QueryRow方法查詢數據,就像之前說的database/sql連接創建都是惰性的,所以當我們通過Query查詢數據的時候主要分為三個步驟:
- 從連接池中請求一個連接
- 執行查詢的sql語句
- 將數據庫連接的所屬權傳遞給Result結果集
Query返回的結果集是sql.Rows類型。它有一個Next方法,可以迭代數據庫的游標,進而獲取每一行的數據,使用方法如下:
//執行查詢操作 rows,err := Db.Query("SELECT email FROM user_info WHERE user_id>=5") if err != nil{ fmt.Println("select db failed,err:",err) return } // 這里獲取的rows是從數據庫查的滿足user_id>=5的所有行的email信息,rows.Next(),用於循環獲取所有 for rows.Next(){ var s string err = rows.Scan(&s) if err != nil{ fmt.Println(err) return } fmt.Println(s) } rows.Close()
其實當我們通過for循環迭代數據庫的時候,當迭代到最后一樣數據的時候,會出發一個io.EOF的信號,引發一個錯誤,同時go會自動調用rows.Close方法釋放連接,然后返回false,此時循環將會結束退出。
通常你會正常迭代完數據然后退出循環。可是如果並沒有正常的循環而因其他錯誤導致退出了循環。此時rows.Next處理結果集的過程並沒有完成,歸屬於rows的連接不會被釋放回到連接池。因此十分有必要正確的處理rows.Close事件。如果沒有關閉rows連接,將導致大量的連接並且不會被其他函數重用,就像溢出了一樣。最終將導致數據庫無法使用。
所以為了避免這種情況的發生,最好的辦法就是顯示的調用rows.Close方法,確保連接釋放,又或者使用defer指令在函數退出的時候釋放連接,即使連接已經釋放了,rows.Close仍然可以調用多次,是無害的。
rows.Next循環迭代的時候,因為觸發了io.EOF而退出循環。為了檢查是否是迭代正常退出還是異常退出,需要檢查rows.Err。例如上面的代碼應該改成:
//Query執行查詢操作 rows,err := Db.Query("SELECT email FROM user_info WHERE user_id>=5") if err != nil{ fmt.Println("select db failed,err:",err) return } // 這里獲取的rows是從數據庫查的滿足user_id>=5的所有行的email信息,rows.Next(),用於循環獲取所有 for rows.Next(){ var s string err = rows.Scan(&s) if err != nil{ fmt.Println(err) return } fmt.Println(s) } rows.Close() if err = rows.Err();err != nil{ fmt.Println(err) return }
讀取單條數據
Query方法是讀取多行結果集,實際開發中,很多查詢只需要單條記錄,不需要再通過Next迭代。golang提供了QueryRow方法用於查詢單條記錄的結果集。
QueryRow方法的使用很簡單,它要么返回sql.Row類型,要么返回一個error,如果是發送了錯誤,則會延遲到Scan調用結束后返回,如果沒有錯誤,則Scan正常執行。只有當查詢的結果為空的時候,會觸發一個sql.ErrNoRows錯誤。你可以選擇先檢查錯誤再調用Scan方法,或者先調用Scan再檢查錯誤。
在之前的代碼中我們都用到了Scan方法,下面說說關於這個方法
結果集方法Scan可以把數據庫取出的字段值賦值給指定的數據結構。它的參數是一個空接口的切片,這就意味着可以傳入任何值。通常把需要賦值的目標變量的指針當成參數傳入,它能將數據庫取出的值賦值到指針值對象上。
代碼例子如:
// 查詢數據 var username string var email string rows := Db.QueryRow("SELECT username,email FROM user_info WHERE user_id=6") err = rows.Scan(&username,&email) if err != nil{ fmt.Println("scan err:",err) return } fmt.Println(username,email)
Scan還會幫我們自動推斷除數據字段匹配目標變量。比如有個數據庫字段的類型是VARCHAR,而他的值是一個數字串,例如"1"。如果我們定義目標變量是string,則scan賦值后目標變量是數字string。如果聲明的目標變量是一個數字類型,那么scan會自動調用strconv.ParseInt()或者strconv.ParseInt()方法將字段轉換成和聲明的目標變量一致的類型。當然如果有些字段無法轉換成功,則會返回錯誤。因此在調用scan后都需要檢查錯誤。
空值處理
數據庫有一個特殊的類型,NULL空值。可是NULL不能通過scan直接跟普遍變量賦值,甚至也不能將null賦值給nil。對於null必須指定特殊的類型,這些類型定義在database/sql庫中。例如sql.NullFloat64,sql.NullString,sql.NullBool,sql.NullInt64。如果在標准庫中找不到匹配的類型,可以嘗試在驅動中尋找。下面是一個簡單的例子:
下面代碼,數據庫中create_time為Null這個時候,如果直接這樣查詢,會提示錯誤:
// 查詢數據 var username string var email string var createTime string rows := Db.QueryRow("SELECT username,email,create_time FROM user_info WHERE user_id=6") err = rows.Scan(&username,&email,&createTime) if err != nil{ fmt.Println("scan err:",err) return } fmt.Println(username,email,createTime)
錯誤內容如下:
scan err: sql: Scan error on column index 2: unsupported Scan, storing driver.Value type <nil> into type *string
所以需要將代碼更改為:
// 查詢數據 var username string var email string var createTime sql.NullString rows := Db.QueryRow("SELECT username,email,create_time FROM user_info WHERE user_id=6") err = rows.Scan(&username,&email,&createTime) if err != nil{ fmt.Println("scan err:",err) return } fmt.Println(username,email,createTime)
執行結果為:
user01 8989@qq.com { false}
我將數據庫中添加了一列,是int類型,同樣的默認值是Null,代碼為:
// 查詢數據 var username string var email string var createTime string var score int rows := Db.QueryRow("SELECT username,email,create_time,socre FROM user_info WHERE user_id=6") rows.Scan(&username,&email,&createTime,&score) fmt.Println(username,email,createTime,score)
其實但我們忽略錯誤直接輸出的時候,也可以輸出,當然Null的字段都被轉換為了零值
而當我們按照上面的方式處理后,代碼為:
// 查詢數據 var username string var email string var createTime sql.NullString var score sql.NullInt64 rows := Db.QueryRow("SELECT username,email,create_time,socre FROM user_info WHERE user_id=6") err = rows.Scan(&username,&email,&createTime,&score) if err != nil{ fmt.Println("scan fail,err:",err) return } fmt.Println(username,email,createTime,score)
輸出的結果為:
user01 8989@qq.com { false} {0 false}
對Null的操作,一般還是需要驗證的,代碼如下:
// 查詢數據 var score sql.NullInt64 rows := Db.QueryRow("SELECT socre FROM user_info WHERE user_id=6") err = rows.Scan(&score) if err != nil{ fmt.Println("scan fail,err:",err) return } if score.Valid{ fmt.Println("res:",score.Int64) }else{ fmt.Println("err",score.Int64) }
這里我已經在數據庫給字段添加內容了,所以這里默認輸出10,但是當還是Null的時候輸出的則是零值
但是有時候我們如果不關心是不是Null的時候,只是想把它當做空字符串處理就行,我們也可以使用[]byte,代碼如下:
// 查詢數據 var score []byte var modifyTime []byte rows := Db.QueryRow("SELECT modify_time,socre FROM user_info WHERE user_id=6") err = rows.Scan(&modifyTime,&score) if err != nil{ fmt.Println("scan fail,err:",err) return } fmt.Println(string(modifyTime),string(score))
這樣處理后,如果有值則可以獲取值,如果沒有則獲取的為空字符串
自動匹配字段
上面查詢的例子中,我們都自己定義了變量,同時查詢的時候也寫明了字段,如果不指名字段,或者字段的順序和查詢的不一樣,都有可能出錯。因此如果能夠自動匹配查詢的字段值,將會十分節省代碼,同時也易於維護。
go提供了Columns方法用獲取字段名,與大多數函數一樣,讀取失敗將會返回一個err,因此需要檢查錯誤。
代碼例子如下:
// 查詢數據 rows,err:= Db.Query("SELECT * FROM user_info WHERE user_id>6") if err != nil{ fmt.Println("select fail,err:",err) return } cols,err := rows.Columns() if err != nil{ fmt.Println("get columns fail,err:",err) return } fmt.Println(cols) vals := make([][]byte, len(cols)) scans := make([]interface{},len(cols)) for i := range vals{ scans[i] = &vals[i] } fmt.Println(scans) var results []map[string]string for rows.Next(){ err = rows.Scan(scans...) if err != nil{ fmt.Println("scan fail,err:",err) return } row := make(map[string]string) for k,v:=range vals{ key := cols[k] row[key] =string(v) } results = append(results,row) } for k,v:=range results{ fmt.Println(k,v) }
因為查詢的時候是語句是:
SELECT * FROM user_info WHERE user_id>6
這樣就會獲取每行數據的所有的字段
使用rows.Columns()獲取字段名,是一個string的數組
然后創建一個切片vals,用來存放所取出來的數據結果,類似是byte的切片。接下來還需要定義一個切片,這個切片用來scan,將數據庫的值復制到給它
vals則得到了scan復制給他的值,因為是byte的切片,因此在循環一次,將其轉換成string即可。
轉換后的row即我們取出的數據行值,最后組裝到result切片中。
上面代碼的執行結果為:
[user_id username sex email create_time modify_time socre] [0xc4200c6000 0xc4200c6018 0xc4200c6030 0xc4200c6048 0xc4200c6060 0xc4200c6078 0xc4200c6090] 0 map[user_id:7 username:user01 sex:男 email:23333222@qq.com create_time:2018-03-05 14:10:08 modify_time: socre:] 1 map[username:user11 sex:男 email:1231313@qq.com create_time:2018-03-05 14:10:11 modify_time: socre: user_id:8] 2 map[sex:男 email:65656@qq.com create_time:2018-03-05 14:10:15 modify_time: socre: user_id:9 username:user12]
通過上面例子的整理以及上面文章的整理,我們基本可以知道:
Exec的時候通常用於執行插入和更新操作
Query以及QueryRow通常用於執行查詢操作
Exec執行完畢之后,連接會立即釋放回到連接池中,因此不需要像query那樣再手動調用row的close方法。
事務是數據庫的一個非常重要的特性,尤其對於銀行,支付系統,等等。
database/sql提供了事務處理的功能。通過Tx對象實現。db.Begin會創建tx對象,后者的Exec和Query執行事務的數據庫操作,最后在tx的Commit和Rollback中完成數據庫事務的提交和回滾,同時釋放連接。
tx對象
我們在之前查詢以及操作數據庫都是用的db對象,而事務則是使用另外一個對象.
使用db.Begin 方法可以創建tx對象,tx對象也可以對數據庫交互的Query,Exec方法
用法和我們之前操作基本一樣,但是需要在查詢或者操作完畢之后執行tx對象的Commit提交或者Rollback方法回滾。
一旦創建了tx對象,事務處理都依賴於tx對象,這個對象會從連接池中取出一個空閑的連接,接下來的sql執行都基於這個連接,知道commit或者Roolback調用之后,才會把這個連接釋放到連接池。
在事務處理的時候,不能使用db的查詢方法,當然你如果使用也能執行語句成功,但是這和你事務里執行的操作將不是一個事務,將不會接受commit和rollback的改變,如下面操作時:
tx,err := Db.Begin() Db.Exec() tx.Exec() tx.Commit()
上面這個偽代碼中,調用Db.Exec方法的時候,和tx執行Exec方法時候是不同的,只有tx的會綁定到事務中,db則是額外的一個連接,兩者不是同一個事務。
事務與連接
創建Tx對象的時候,會從連接池中取出連接,然后調用相關的Exec方法的時候,連接仍然會綁定在該事務處理中。
事務的連接生命周期從Beigin函數調用起,直到Commit和Rollback函數的調用結束。
事務並發
對於sql.Tx對象,因為事務過程只有一個連接,事務內的操作都是順序執行的,在開始下一個數據庫交互之前,必須先完成上一個數據庫交互。
rows, _ := db.Query("SELECT id FROM user") for rows.Next() { var mid, did int rows.Scan(&mid) db.QueryRow("SELECT id FROM detail_user WHERE master = ?", mid).Scan(&did) }
調用了Query方法之后,在Next方法中取結果的時候,rows是維護了一個連接,再次調用QueryRow的時候,db會再從連接池取出一個新的連接。rows和db的連接兩者可以並存,並且相互不影響。
但是如果邏輯在事務處理中會失效,如下代碼:
rows, _ := tx.Query("SELECT id FROM user") for rows.Next() { var mid, did int rows.Scan(&mid) tx.QueryRow("SELECT id FROM detail_user WHERE master = ?", mid).Scan(&did) }
tx執行了Query方法后,連接轉移到rows上,在Next方法中,tx.QueryRow將嘗試獲取該連接進行數據庫操作。因為還沒有調用rows.Close,因此底層的連接屬於busy狀態,tx是無法再進行查詢的。
完整的小結
通過下面一個完整的例子就行更好的理解:
func doSomething(){ panic("A Panic Running Error") } func clearTransaction(tx *sql.Tx){ err := tx.Rollback() if err != sql.ErrTxDone && err != nil{ log.Fatalln(err) } } func main() { db, err := sql.Open("mysql", "root:@tcp(127.0.0.1:3306)/test?parseTime=true") if err != nil { log.Fatalln(err) } defer db.Close() tx, err := db.Begin() if err != nil { log.Fatalln(err) } defer clearTransaction(tx) rs, err := tx.Exec("UPDATE user SET gold=50 WHERE real_name='vanyarpy'") if err != nil { log.Fatalln(err) } rowAffected, err := rs.RowsAffected() if err != nil { log.Fatalln(err) } fmt.Println(rowAffected) rs, err = tx.Exec("UPDATE user SET gold=150 WHERE real_name='noldorpy'") if err != nil { log.Fatalln(err) } rowAffected, err = rs.RowsAffected() if err != nil { log.Fatalln(err) } fmt.Println(rowAffected) doSomething() if err := tx.Commit(); err != nil { // tx.Rollback() 此時處理錯誤,會忽略doSomthing的異常 log.Fatalln(err) } }
這里定義了一個clearTransaction(tx)函數,該函數會執行rollback操作。因為我們事務處理過程中,任何一個錯誤都會導致main函數退出,因此在main函數退出執行defer的rollback操作,回滾事務和釋放連接。
如果不添加defer,只在最后Commit后check錯誤err后再rollback,那么當doSomething發生異常的時候,函數就退出了,此時還沒有執行到tx.Commit。這樣就導致事務的連接沒有關閉,事務也沒有回滾。
tx事務環境中,只有一個數據庫連接,事務內的Eexc都是依次執行的,事務中也可以使用db進行查詢,但是db查詢的過程會新建連接,這個連接的操作不屬於該事務。