這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
最近看到不少朋友對 Golang 中操作 MySQL 資料庫有疑問,那麼就此內容給大家分享一下吧。
MyMySQL 的原作者是來自波蘭的 ziutek,他根據 MySQL 的協議標準使用 Golang 實現了 MyMySQL 包。根據他的介紹,這個包可以用在 MySQL 4.1 或更高版本上,並且在 5.0、5.1 版本上經過項目的實際驗證。
只要用 MySQL 做過項目的朋友一定遇到過編碼問題,set names 幾乎成為了中文環境下使用 MySQL 的標配。那麼 Golang 也不能例外。不過由於 database/sql 使用了隨機的串連池,且未提供任何方法讓所有串連都執行某個操作,於是這個麻煩事兒就落到了驅動的肩上。對於中文什麼的,大老外一向不怎麼在意。我提交了這個 issue 給 ziutek,不過對於他最終實現的方案不怎麼滿意。需要額外編寫godrv.Register 來對編碼進行設定。
同時當串連空閑時間超出伺服器的 wait_timeout 時,會出現 broken pipe。對於這個問題,ziutek 的建議是使用 MyMySQL 的 autorc,但是 godrv 並沒有基於 autorc 實現 database/sql。換句話說,要解決這個錯誤,只能使用 MyMySQL 原始介面。
鑒於這些問題,我只能 fork 了 ziutek 的項目,對 MyMySQL 打上相應的補丁。主要是針對編碼設定進行了改進,並順便實現了 keepalive 功能。
本文主要介紹 fork 的版本的使用。
在 fork 的版本中,還有一個改進就是統一了 go get 的安裝介面,無需再逐一安裝 MyMySQl 的子包,只需要
go get github.com/mikespook/mymysql
即可完成安裝。
關於使用,還是用代碼說話吧,完整代碼看這裡。為了通用,這裡只介紹使用 database/sql 介面的使用。在實際項目中,我也建議大家盡量使用該介面,這樣在最大可用性保障下保持相容性。
import( "log" "database/sql" _ "github.com/mikespook/mymysql/godrv")
使用 MyMySQL 的 database/sql 介面要匯入 godrv 包。在這個包的 init 函數中自動註冊了 database/sql 的驅動,所以匯入後無需再使用。
db, err := sql.Open("mymysql", "tcp://127.0.0.1:3306/test/root/xxiyy?charset=utf8&keepalive=1200")if err != nil { log.Fatal(err)}defer db.Close()
使用 mymysql 驅動開啟一個 sql.DB 串連。dsn 串連串是我對 ziutek 的 MyMySQL 的主要改進。支援多種串連方式:
- 使用 tcp 協議:[tcp://addr/]dbname/user/password[?params]
- 使用 unix sock:[unix://sockpath/]dbname/user/password[?params]
其中 tcp 協議的 addr 必須是含有主機名稱或 ip,且包含又分號分隔的連接埠號碼的字串。如 localhost:3306、192.168.3.2:3307。unix sock 協議中的 sockpath 必須是 MySQL 的 sock 檔案的絕對路徑。
在上述 dsn 中,方括弧內的是可選的內容,可省略。當協議資訊(第一個方括弧內)未指定時,使用 tcp://127.0.0.1:3306/ 作為預設值。
params 部分是用於當前資料庫驅動的參數設定,當前來說,只有兩個可設定的參數:
- charset:用於 ‘set names’ 設定串連編碼。
- keepalive:每 keepalive 秒向伺服器發送 PING。
需要特彆強調的是,如果密碼含有斜線(/),由於解析規則的緣故,需要用星號(*)代替。如果密碼含有星號(*),則需要用兩個星號(**)代替。例如:
- 原密碼 [pass/wd],在 dsn 中應寫為 [pass*wd]。
- 原密碼 [pass*wd],在 dsn 中應寫為 [pass**wd]。
stmt, err := db.Prepare("insert into `test` (`key`, `value`) values (?, ?)")if err != nil { log.Fatal(err)}defer stmt.Close()rslt, err := stmt.Exec("name", "foobar")if err != nil { log.Fatal(err)}if a, err := rslt.RowsAffected(); err != nil { log.Print(err)} else { log.Printf("[INS]Affected rows=%d", a)}if id, err := rslt.LastInsertId(); err != nil { log.Print(err)} else { log.Printf("[INS]Last insert id=%d", id)}
在 database/sql 介面裡,有多種辦法執行某個 SQL 陳述式。出於安全考慮,強烈建議大家不要使用字串方式直接拼接 SQL 陳述式進行執行,如果可能,在大多數其他介面中都會出現的 Prepare 方法是首選。執行完 SQL 陳述式後,可以通過 sql.Result 得到這次執行所影響的行數,和最後插入的 Id 值。如果表中沒有使用 autoincrement 作為主鍵,那麼這個 Id 值永遠為 0。
rows, err := db.Query("select * from `test`")if err != nil { log.Fatal(err)}defer rows.Close()for rows.Next() { var k, v string rows.Scan(&k, &v) log.Printf("[ROWS]key=%s, value=%s", k, v)}
查詢一個二維表,從中取得多行資料使用 Query 方法。需要注意的是 sql.Rows 的 Scan 方法接收的參數必須是指標。也就是說存放資料的地址要在 Scan 調用前準備好。Scan 內部使用了反射來識別參數類型並進行賦值,也可以傳遞一個 []interface{}。這時必須這樣調用:rows.Scan(a…) 才能得到預期結果。
row := db.QueryRow("select * from `test` where `key` = ?", "name")var k, v stringrow.Scan(&k, &v)log.Printf("[ROW]key=%s, value=%s", k, v)
對於有些如用主鍵進行的查詢,僅僅會讀出一行資料。那麼可以用 QueryRow 進行更加簡化的讀取。
rslt, err = db.Exec("delete from `test`")if a, err := rslt.RowsAffected(); err != nil { log.Print(err)} else { log.Printf("[DEL]Affected rows=%d", a)}if id, err := rslt.LastInsertId(); err != nil { log.Print(err)} else { log.Printf("[DEL]Last insert id=%d", id)}
對於沒有拼接 SQL 需要的操作,可以直接在 sql.DB 對象上調用 Exec 方法。返回的 sql.Result 與 sql.Stmt 的 Exec 方法一致。
關於基礎的使用,大致就這些內容。database/sql 也支援事物,這部分資訊大家可以參考其文檔進行學習。