關於 Go 的標準庫 database/sql 和 sqlx
database/sql 是 Go 操作數據庫的標準庫之一,它提供了一系列接口方法,用於訪問數據庫(mysql,sqllite,oralce,postgresql),它並不會提供數據庫特有的方法,那些特有的方法交給數據庫驅動去實現
而通常在工作中,我們更多的是用 https://github.com/jmoiron/sqlx 包來操作數據庫,sqlx 是基於標準庫 sql 的擴展,並且我們可以通過 sqlx 操作各種類型的數據,如將查詢的數據轉爲結構體等
github 地址:
安裝:
- go get “github.com/go-sql-driver/mysql” (mysql驅動連接包)
- go get “github.com/jmoiron/sqlx”(基於Go中的database/sql 進行了一些封裝,方便使用)
sqlx 庫提供了一些類型,掌握這些類型的用法非常的重要
1)DB(數據庫對象)
sql.DB 類型代表了數據庫,其它語言操作數據庫的時候,需要創建一個連接,對於 Go 而言則是需要創建一個數據庫類型,它不是數據庫連接,Go 中的連接來自內部實現的連接池,連接的建立是惰性的,連接將會在操作的時候,由連接池創建並維護使用 sql.Open 函數創建數據庫類型,第一個是數據庫驅動名,第二個是連接信息的字符串
var Db *sqlx.DB
db, err := sqlx.Open("mysql","username:password@tcp(ip:port)/database?charset=utf8")
Db = db
2)Results 和 Result(結果集)
新增、更新、刪除;和查詢所用的方法不一樣,所有返回的類型也不同
- Result 是 新增、更新、刪除時返回的結果集
- Results 是查詢數據庫時的結果集,sql.Rows 類型表示查詢返回多行數據的結果集,sql.Row 則表示單行查詢的結果集
3)Statements(語句)
sql.Stmt 類型表示 sql 語句,例如 DDL,DML 等類似的 sql 語句,可以當成 prepare 語句構造查詢,也可以直接使用 sql.DB 的函數對其操作
實踐部分(數據庫CURD)
數據庫建表
以下所有 demo 都以下表結構作爲基礎
CREATE TABLE `userinfo` (
`uid` INT(10) NOT NULL AUTO_INCREMENT,
`create_time` datetime DEFAULT NULL,
`username` VARCHAR(64) DEFAULT NULL,
`password` VARCHAR(32) DEFAULT NULL,
`department` VARCHAR(64) DEFAULT NULL,
`email` varchar(64) DEFAULT NULL,
PRIMARY KEY (`uid`)
)ENGINE=InnoDB DEFAULT CHARSET=utf8;
Exec() 方法使用(新增、修改、刪除)
func (db *DB) Exec(query string, args ...interface{}) (Result, error)
Exec 和 MustExec 從連接池中獲取一個連接然後指向對應的 query 操作,對於不支持 ad-hoc query execution 的驅動,在操作執行的背後會創建一個 prepared statement,在結果返回前,這個 connection 會返回到連接池中需要注意的是,不同的數據庫,使用的佔位符不同,mysql 採用 ? 作爲佔位符
- Mysql 使用 ?
- PostgreSQL 使用 1,1,2 等等
- SQLLite 使用 ? 或 $1
- Oracle 使用 :name (注意有冒號)
demo:定義了 4 個函數,分別是 連接數據庫,插入數據,更新數據,刪除數據
關於 下面數據庫操作的幾個小知識點
- 插入數據後可以通過 LastInsertId() 方法獲取插入數據的主鍵 id
- 通過 RowsAffected 可以獲取受影響的行數
- 通過 Exec() 方法插入數據,返回的結果是 sql.Result 類型
package main
import (
"fmt"
_ "github.com/go-sql-driver/mysql"
"github.com/jmoiron/sqlx"
)
var (
userName string = "chenkai"
password string = "chenkai"
ipAddrees string = "192.168.0.115"
port int = 3306
dbName string = "test"
charset string = "utf8"
)
func connectMysql() (*sqlx.DB) {
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=%s", userName, password, ipAddrees, port, dbName, charset)
Db, err := sqlx.Open("mysql", dsn)
if err != nil {
fmt.Printf("mysql connect failed, detail is [%v]", err.Error())
}
return Db
}
func addRecord(Db *sqlx.DB) {
for i:=0; i<2; i++ {
result, err := Db.Exec("insert into userinfo values(?,?,?,?,?,?)",0, "2019-07-06 11:45:20", "johny", "123456", "技術部", "[email protected]")
if err != nil {
fmt.Printf("data insert faied, error:[%v]", err.Error())
return
}
id, _ := result.LastInsertId()
fmt.Printf("insert success, last id:[%d]\n", id)
}
}
func updateRecord(Db *sqlx.DB){
//更新uid=1的username
result, err := Db.Exec("update userinfo set username = 'anson' where uid = 1")
if err != nil {
fmt.Printf("update faied, error:[%v]", err.Error())
return
}
num, _ := result.RowsAffected()
fmt.Printf("update success, affected rows:[%d]\n", num)
}
func deleteRecord(Db *sqlx.DB){
//刪除uid=2的數據
result, err := Db.Exec("delete from userinfo where uid = 2")
if err != nil {
fmt.Printf("delete faied, error:[%v]", err.Error())
return
}
num, _ := result.RowsAffected()
fmt.Printf("delete success, affected rows:[%d]\n", num)
}
func main() {
var Db *sqlx.DB = connectMysql()
defer Db.Close()
addRecord(Db)
updateRecord(Db)
deleteRecord(Db)
}
運行結果:
API server listening at: 127.0.0.1:59899
insert success, last id:[1]
insert success, last id:[2]
update success, affected rows:[1]
delete success, affected rows:[1]
Query() 方法使用(查詢單個字段數據)
func (db *DB) Query(query string, args ...interface{}) (*Rows, error)
Query() 方法返回的是一個 sql.Rows 類型的結果集
也可以用來查詢多個字段的數據,不過需要定義多個字段的變量進行接收
迭代後者的 Next() 方法,然後使用 Scan() 方法給對應類型變量賦值,以便取出結果,最後再把結果集關閉(釋放連接)
package main
import (
"fmt"
_ "github.com/go-sql-driver/mysql"
"github.com/jmoiron/sqlx"
)
var (
userName string = "chenkai"
password string = "chenkai"
ipAddrees string = "192.168.0.115"
port int = 3306
dbName string = "test"
charset string = "utf8"
)
func connectMysql() (*sqlx.DB) {
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=%s", userName, password, ipAddrees, port, dbName, charset)
Db, err := sqlx.Open("mysql", dsn)
if err != nil {
fmt.Printf("mysql connect failed, detail is [%v]", err.Error())
}
return Db
}
func queryData(Db *sqlx.DB) {
rows, err := Db.Query("select * from userinfo")
if err != nil {
fmt.Printf("query faied, error:[%v]", err.Error())
return
}
for rows.Next() {
//定義變量接收查詢數據
var uid int
var create_time, username, password, department, email string
err := rows.Scan(&uid, &create_time, &username, &password, &department, &email)
if err != nil {
fmt.Println("get data failed, error:[%v]", err.Error())
}
fmt.Println(uid, create_time, username, password, department, email)
}
//關閉結果集(釋放連接)
rows.Close()
}
func main() {
var Db *sqlx.DB = connectMysql()
defer Db.Close()
queryData(Db)
}
運行結果:
1 2019-07-06 11:45:20 anson 123456 技術部 123456@163.com
3 2019-07-06 11:45:20 johny 123456 技術部 123456@163.com
4 2019-07-06 11:45:20 johny 123456 技術部 123456@163.com
Get() 方法使用
func (db *DB) Get(dest interface{}, query string, args ...interface{}) error
是將查詢到的一條記錄,保存到結構體
結構體的字段名首字母必須大寫,不然無法尋址
package main
import (
"fmt"
_ "github.com/go-sql-driver/mysql"
"github.com/jmoiron/sqlx"
)
var (
userName string = "chenkai"
password string = "chenkai"
ipAddrees string = "192.168.0.115"
port int = 3306
dbName string = "test"
charset string = "utf8"
)
func connectMysql() (*sqlx.DB) {
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=%s", userName, password, ipAddrees, port, dbName, charset)
Db, err := sqlx.Open("mysql", dsn)
if err != nil {
fmt.Printf("mysql connect failed, detail is [%v]", err.Error())
}
return Db
}
func getData(Db *sqlx.DB) {
type userInfo struct {
Uid int `db:"uid"`
UserName string `db:"username"`
CreateTime string `db:"create_time"`
Password string `db:"password"`
Department string `db:"department"`
Email string `db:"email"`
}
//初始化定義結構體,用來存放查詢數據
var userData *userInfo = new(userInfo)
err := Db.Get(userData,"select *from userinfo where uid = 1")
if err != nil {
fmt.Printf("query faied, error:[%v]", err.Error())
return
}
//打印結構體內容
fmt.Println(userData.Uid, userData.CreateTime, userData.UserName,
userData.Password, userData.Department, userData.Email)
}
func main() {
var Db *sqlx.DB = connectMysql()
defer Db.Close()
getData(Db)
}
運行結果:
2019-07-06 11:45:20 anson 123456 技術部 123456@163.com
Select() 方法使用
func (db *DB) Select(dest interface{}, query string, args ...interface{}) error
將查詢的多條記錄,保存到結構體的切片中
結構體的字段名首字母必須大寫,不然無法尋址
package main
import (
"fmt"
_ "github.com/go-sql-driver/mysql"
"github.com/jmoiron/sqlx"
)
var (
userName string = "chenkai"
password string = "chenkai"
ipAddrees string = "192.168.0.115"
port int = 3306
dbName string = "test"
charset string = "utf8"
)
func connectMysql() (*sqlx.DB) {
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=%s", userName, password, ipAddrees, port, dbName, charset)
Db, err := sqlx.Open("mysql", dsn)
if err != nil {
fmt.Printf("mysql connect failed, detail is [%v]", err.Error())
}
return Db
}
func selectData(Db *sqlx.DB) {
type userInfo struct {
Uid int `db:"uid"`
UserName string `db:"username"`
CreateTime string `db:"create_time"`
Password string `db:"password"`
Department string `db:"department"`
Email string `db:"email"`
}
//定義結構體切片,用來存放多條查詢記錄
var userInfoSlice []userInfo
err := Db.Select(&userInfoSlice,"select * from userinfo")
if err != nil {
fmt.Printf("query faied, error:[%v]", err.Error())
return
}
//遍歷結構體切片
for _, userData := range userInfoSlice {
fmt.Println(userData.Uid, userData.CreateTime, userData.UserName,
userData.Password, userData.Department, userData.Email)
}
}
func main() {
var Db *sqlx.DB = connectMysql()
defer Db.Close()
selectData(Db)
}
運行結果:
2019-07-06 11:45:20 anson 123456 技術部 123456@163.com
2019-07-06 11:45:20 johny 123456 技術部 123456@163.com
2019-07-06 11:45:20 johny 123456 技術部 123456@163.com
重點內容回顧
sql.DB
- 當我們調用 sqlx.Open() 可以獲取一個 sql.DB 類型對象,sqlx.DB
是數據庫的抽象,切記它不是數據庫連接,sqlx.Open() 只是驗證數據庫參數,並沒有創建數據庫連接 - sqlx.DB 擁有一系列與數據庫交互的方法(Exec,Query,Get,Select
…),同時也管理維護着一個數據庫連接池,並且對於多個 goroutine 也是安全的 - sqlx.DB 表示是數據庫的抽象,因此有幾個數據庫就要創建幾個 sqlx.DB
類型對象,因爲它要維護一個連接池,因此不需要頻繁的創建和銷燬
連接池
- 只用 sqlx.Open() 函數創建連接池,此時只是初始化了連接池,並沒有連接數據庫,連接都是惰性的,只有調用 sqlx.DB
的方法時,此時才真正用到了連接,連接池纔會去創建連接,連接池很重要,它直接影響着你的程序行爲 - 連接池的工作原理也非常簡單,當調用 sqlx.DB
的方法時,會首先去向連接池請求要一個數據庫連接,如果連接池有空閒的連接,則返回給方法中使用,否則連接池將創建一個新的連接給到方法中使用;一旦將數據庫連接給到了方法中,連接就屬於方法了。方法執行完畢後,要不把連接所屬權還給連接池,要不傳遞給下一個需要數據庫連接的方法中,最後都使用完將連接釋放回到連接池中
請求數據庫連接的方法有幾個,執行完畢處理連接的方式也不同:
- DB.Ping() 使用完畢後會馬上把連接返回給連接池
- DB.Exec() 使用完畢後會馬上把連接返回給連接池,但是它返回的 Result 對象還保留着連接的引用,當後面的代碼需要處理結果集的時候,連接將會被重新啓用
- DB.Query() 調用完畢後將連接傳遞給 sql.Rows 類型,當後者迭代完畢或者顯示的調用 Close() 方法後,連接將會被釋放到連接池
- DB.QueryRow() 調用完畢後將連接傳遞給 sql.Row 類型,當 Scan() 方法調用完成後,連接將會被釋放到連接池
- DB.Begin() 調用完畢後將連接傳遞給 sql.Tx 類型對象,當 Commit() 或 Rollback() 方法調用後釋放連接
每個連接都是惰性的,如果驗證 sqlx.Open() 調用之後,sqlx.DB 類型對象可用呢?通過 DB.Ping() 方法來初始化
func (db *DB) Ping() error
demo:需要知道,當調用了 Ping() 方法後,連接池一定會初始化一個數據庫連接
package main
import (
"fmt"
_ "github.com/go-sql-driver/mysql"
"github.com/jmoiron/sqlx"
)
var (
userName string = "chenkai"
password string = "chenkai"
ipAddrees string = "192.168.0.115"
port int = 3306
dbName string = "test"
charset string = "utf8"
)
func connectMysql() (*sqlx.DB) {
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=%s", userName, password, ipAddrees, port, dbName, charset)
Db, err := sqlx.Open("mysql", dsn)
if err != nil {
fmt.Printf("mysql connect failed, detail is [%v]", err.Error())
}
return Db
}
func ping(Db *sqlx.DB) {
err := Db.Ping()
if err != nil {
fmt.Println("ping failed")
} else {
fmt.Println("ping success")
}
}
func main() {
var Db *sqlx.DB = connectMysql()
defer Db.Close()
ping(Db)
}
運行結果:
ping success
連接池配置
- DB.SetMaxIdleConns(n int)
設置連接池中的保持連接的最大連接數。默認也是0,表示連接池不會保持數據庫連接的狀態:即當連接釋放回到連接池的時候,連接將會被關閉。這會導致連接再連接池中頻繁的關閉和創建,我們可以設置一個合理的值。 - DB.SetMaxOpenConns(n int) 設置打開數據庫的最大連接數。包含正在使用的連接和連接池的連接。如果你的方法調用
需要用到一個連接,並且連接池已經沒有了連接或者連接數達到了最大連接數。此時的方法調用將會被
block,直到有可用的連接纔會返回。設置這個值可以避免併發太高導致連接 mysql 出現 too many connections
的錯誤。該函數的默認設置是0,表示無限制 - DB.SetConnMaxLifetime(d time.Duration) 設置連接可以被使用的最長有效時間,如果過期,連接將被拒絕
數據庫連接重試次數
sqlx 中的方法幫我們做了很多事情,我們不用考慮連接失敗的情況,當調用方法進行數據庫操作的時候,如果連接失敗,sqlx 中的方法會幫我們處理,它會自動連接2次,這個如果查看源碼中我們可以看到如下的代碼:(其它的方法中也有這種處理,代碼中變量maxBadConnRetries小時如果連接失敗嘗試的次數,默認是 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
}