Go 的database/sql 和 sqlx

關於 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 個函數,分別是 連接數據庫,插入數據,更新數據,刪除數據
關於 下面數據庫操作的幾個小知識點

  1. 插入數據後可以通過 LastInsertId() 方法獲取插入數據的主鍵 id
  2. 通過 RowsAffected 可以獲取受影響的行數
  3. 通過 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
    的方法時,會首先去向連接池請求要一個數據庫連接,如果連接池有空閒的連接,則返回給方法中使用,否則連接池將創建一個新的連接給到方法中使用;一旦將數據庫連接給到了方法中,連接就屬於方法了。方法執行完畢後,要不把連接所屬權還給連接池,要不傳遞給下一個需要數據庫連接的方法中,最後都使用完將連接釋放回到連接池中

請求數據庫連接的方法有幾個,執行完畢處理連接的方式也不同:

  1. DB.Ping() 使用完畢後會馬上把連接返回給連接池
  2. DB.Exec() 使用完畢後會馬上把連接返回給連接池,但是它返回的 Result 對象還保留着連接的引用,當後面的代碼需要處理結果集的時候,連接將會被重新啓用
  3. DB.Query() 調用完畢後將連接傳遞給 sql.Rows 類型,當後者迭代完畢或者顯示的調用 Close() 方法後,連接將會被釋放到連接池
  4. DB.QueryRow() 調用完畢後將連接傳遞給 sql.Row 類型,當 Scan() 方法調用完成後,連接將會被釋放到連接池
  5. 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
}

原文連接(https://www.cnblogs.com/kaichenkai/p/11140555.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章