Go 操作 Mysql(一)


關於 Go 的標准庫 database/sql 和 sqlx

database/sql 是 Go 操作數據庫的標准庫之一,它提供了一系列接口方法,用於訪問數據庫(mysql,sqllite,oralce,postgresql),它並不會提供數據庫特有的方法,那些特有的方法交給數據庫驅動去實現

而通常在工作中,我們更多的是用  https://github.com/jmoiron/sqlx 包來操作數據庫,sqlx 是基於標准庫 sql 的擴展,並且我們可以通過 sqlx 操作各種類型的數據,如將查詢的數據轉為結構體等

github 地址:

  • https://github.com/go-sql-driver/mysql
  • https://github.com/jmoiron/sqlx

安裝:

go get "github.com/go-sql-driver/mysql"
go get "github.com/jmoiron/sqlx"

 

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", "技術部", "123456@163.com")
		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)
}

運行結果:
1 2019-07-06 11:45:20 anson 123456 技術部 123456@163.com
View Code

 

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)
}

運行結果:
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
View Code

 

重點內容回顧

sql.DB

  1. 當我們調用 sqlx.Open() 可以獲取一個 sql.DB 類型對象,sqlx.DB 是數據庫的抽象,切記它不是數據庫連接,sqlx.Open() 只是驗證數據庫參數,並沒有創建數據庫連接
  2. sqlx.DB 擁有一系列與數據庫交互的方法(Exec,Query,Get,Select ...),同時也管理維護着一個數據庫連接池,並且對於多個 goroutine 也是安全的
  3. 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/zhaof/p/8509164.html

ending ~

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM