首頁 > 軟體

golang操作連線資料庫實現mysql事務範例

2022-04-14 16:02:51

MySQL是業界常用的關係型資料庫,本文介紹了database/sql庫以及Go語言如何操作MySQL資料庫。

mysql驅動

_ "github.com/go-sql-driver/mysql"

posgre驅動

_ "github.com/lib/pq"

連線postgres

Go語言中的database/sql包提供了保證SQL或類SQL資料庫的泛用介面,並不提供具體的資料庫驅動。使用database/sql包時必須注入(至少)一個資料庫驅動。

我們常用的資料庫基本上都有完整的第三方實現。例如:MySQL驅動

func Open(driverName, dataSourceName string) (*DB, error)
import (
	"database/sql"
	"fmt"
	_ "github.com/lib/pq"
)
Db, _ := sql.Open("postgres", "postgres://postgres:iLoveShark@127.0.0.1:3432/master?sslmode=disable&fallback_application_name=bikesvc")

連線mysql

Open開啟一個dirverName指定的資料庫,dataSourceName指定資料來源,一般至少包括資料庫檔名和其它連線必要的資訊。

import (
	"database/sql"
	_ "github.com/go-sql-driver/mysql"
)
func main() {
   // DSN:Data Source Name
	dsn := "user:password@tcp(127.0.0.1:3306)/dbname"
	db, err := sql.Open("mysql", dsn)
	if err != nil {
		panic(err)
	}
	defer db.Close()  // 注意這行程式碼要寫在上面err判斷的下面
}

初始化連線

Open函數可能只是驗證其引數格式是否正確,實際上並不建立與資料庫的連線。如果要檢查資料來源的名稱是否真實有效,應該呼叫Ping方法。

返回的DB物件可以安全地被多個goroutine並行使用,並且維護其自己的空閒連線池。因此,Open函數應該僅被呼叫一次,很少需要關閉這個DB物件。

// 定義一個全域性物件db
var db *sql.DB
// 定義一個初始化資料庫的函數
func initDB() (err error) {
	// DSN:Data Source Name
	dsn := "user:password@tcp(127.0.0.1:3306)/sql_test?charset=utf8mb4&parseTime=True"
	// 不會校驗賬號密碼是否正確
	// 注意!!!這裡不要使用:=,我們是給全域性變數賦值,然後在main函數中使用全域性變數db
	db, err = sql.Open("mysql", dsn)
	if err != nil {
		return err
	}
	// 嘗試與資料庫建立連線(校驗dsn是否正確)
	err = db.Ping()
	if err != nil {
		return err
	}
	return nil
}
func main() {
	err := initDB() // 呼叫輸出化資料庫的函數
	if err != nil {
		fmt.Printf("init db failed,err:%vn", err)
		return
	}
}

其中sql.DB是表示連線的資料庫物件(結構體範例),它儲存了連線資料庫相關的所有資訊。它內部維護著一個具有零到多個底層連線的連線池,它可以安全地被多個goroutine同時使用。

SetMaxOpenConns

func (db *DB) SetMaxOpenConns(n int)

SetMaxOpenConns設定與資料庫建立連線的最大數目。 如果n大於0且小於最大閒置連線數,會將最大閒置連線數減小到匹配最大開啟連線數的限制。 如果n<=0,不會限制最大開啟連線數,預設為0(無限制)。

SetMaxIdleConns

func (db *DB) SetMaxIdleConns(n int)

SetMaxIdleConns設定連線池中的最大閒置連線數。 如果n大於最大開啟連線數,則新的最大閒置連線數會減小到匹配最大開啟連線數的限制。 如果n<=0,不會保留閒置連線。

CRUD

查詢

為了方便查詢,我們事先定義好一個結構體來儲存user表的資料。

type Info struct {
	Id      int    `json:"id"` 
	Code    string `json:"code"`
	HwCode  string `json:"hw_code"` 
	Name    string `json:"name"` 
	Des     string `json:"des"`
	Created int64  `json:"created"`
	Updated int64  `json:"updated"`
	BrandId int64  `json:"brand_id"`
}

單行查詢QueryRow

單行查詢db.QueryRow()執行一次查詢,並期望返回最多一行結果(即Row)。QueryRow總是返回非nil的值,直到返回值的Scan方法被呼叫時,才會返回被延遲的錯誤。(如:未找到結果)

func (db *DB) QueryRow(query string, args ...interface{}) *Row

具體範例程式碼:

func (o object) QueryInfo(id int) *Info {
	r := new(Info)
	var qstr string
	switch {
	case id > 0:
		qstr += fmt.Sprintf(" and id = %d", id)
	default:
		o.Log.Error("invalid param")
		return nil
	}
	sqlstr := `
	select
	coalesce(id,0) as id,
	coalesce(code,'') as code,
	coalesce(hw_code,'') as hw_code,
	coalesce(name,'') as name,
	coalesce(des,'') as des,
	coalesce(created,0) as created,
	coalesce(updated,0) as updated,
	coalesce(brand_id,0) as brand_id
	from test_table
	where 1=1
	`
	sqlstr += qstr
	err := o.DbRo.QueryRow(sqlstr).Scan(&r.Id, &r.Code, &r.HwCode, &r.Name, &r.Des, &r.Created, &r.Updated, &r.BrandId)
	if err != nil {
		fmt.Println(err)
		o.Log.Errorf("param=%d,sql=%s,err=%v", id, sqlstr, err)
		return nil
	}
	return r
}

多行查詢Query-rows

多行查詢db.Query()執行一次查詢,返回多行結果(即Rows),一般用於執行select命令。引數args表示query中的佔位引數。

func (db *DB) Query(query string, args ...interface{}) (*Rows, error)

具體範例程式碼:

func (o object) QueryList(brandId int) (list []Info) {
	list = make([]Info, 0)
	var qstr string
	switch {
	case brandId > 0:
		qstr += fmt.Sprintf(" and brand_id = %d", brandId)
	default:
		o.Log.Error("invalid param")
		return nil
	}
	sqlstr := `
	select
	coalesce(id,0) as id,
	coalesce(code,'') as code,
	coalesce(hw_code,'') as hw_code,
	coalesce(name,'') as name,
	coalesce(des,'') as des,
	coalesce(created,0) as created,
	coalesce(updated,0) as updated,
	coalesce(brand_id,0) as brand_id
	from bike_color
	where 1=1
	`
	sqlstr += qstr
	rows, err := o.DbRo.Query(sqlstr)
	if err != nil {
		o.Log.Error(err)
		return
	}
	defer rows.Close()
  // rows.Next()獲取下一條結果
	for rows.Next() {
		var r = new(Info)
		err = rows.Scan(&r.Id, &r.Code, &r.HwCode, &r.Name, &r.Des, &r.Created, &r.Updated, &r.BrandId)
		if err != nil {
			o.Log.Error(err, brandId)
			continue
		}
		list = append(list, *r)
	}
	return
}

插入和更新和刪除Exec

插入、更新和刪除操作都使用Exec方法。

func (db *DB) Exec(query string, args ...interface{}) (Result, error)

Exec執行一次命令(包括查詢、刪除、更新、插入等),返回的Result是對已執行的SQL命令的總結。引數args表示query中的佔位引數。

影響的行數

sqlStr := "update user set age=? where id = ?"
ret, _:= db.Exec(sqlStr, 39, 3)
n, _ := ret.RowsAffected() // 操作影響的行數

插入

具體插入資料範例程式碼如下:

func (o object) Insert(i *Info) (r int) {
	sqlstr := `insert into bike_model(name,ctrl_tmpl,batt_support,bike_class,pics,code,brand_id,hw_code)
	values('%v',%d,%d,%d,'%v','%v',%d,'%v')
	returning id`
	sqlstr = fmt.Sprintf(sqlstr, i.Name, i.CtrlTmpl, i.BattSupport, i.BikeClass, i.Pics, i.Code, i.BrandId, i.HwCode)
	fmt.Println(sqlstr)
	err := o.Db.QueryRow(sqlstr).Scan(&r)
	if err != nil {
		fmt.Println(err)
		o.Log.Errorf("param=%+v,sql=%s,err=%v", i, sqlstr, err)
		return
	}
	return
}

更新

具體更新資料範例程式碼如下:

func (o object) Update(i *Info) (r int) {
	sqlstr := fmt.Sprintf("update bike_model set ")
	if i.Id <= 0 {
		o.Log.Error("invalid param")
		return
	}
	if len(i.Name) > 0 {
		sqlstr += fmt.Sprintf("name = '%v',", i.Name)
	}
	if i.CtrlTmpl > 0 {
		sqlstr += fmt.Sprintf("ctrl_tmpl = '%d',", i.CtrlTmpl)
	}
	if i.BattSupport > 0 {
		sqlstr += fmt.Sprintf("batt_support = '%d',", i.BattSupport)
	}
	if i.BikeClass > 0 {
		sqlstr += fmt.Sprintf("bike_class = '%d',", i.BikeClass)
	}
	if len(i.Pics) > 0 {
		sqlstr += fmt.Sprintf("hw_code = '%v',", i.Pics)
	}
	if len(i.Code) > 0 {
		sqlstr += fmt.Sprintf("code = '%v',", i.Code)
	}
	if i.BrandId > 0 {
		sqlstr += fmt.Sprintf("brand_id = %d,", i.BrandId)
	}
	if len(i.HwCode) > 0 {
		sqlstr += fmt.Sprintf("hw_code = '%v',", i.HwCode)
	}
  // 拼接sql字串
	sqlstr = strings.TrimRight(sqlstr, ",")
	sqlstr += fmt.Sprintf("where id = %d returning id", i.Id)
	err := o.Db.QueryRow(sqlstr).Scan(&r)
	if err != nil {
		o.Log.Errorf("param=%+v,sql=%s,err=%v", i, sqlstr, err)
		return
	}
	return
}

刪除

具體刪除資料的範例程式碼如下:

func (o object) Delete(id int) (r int) {
	sqlstr := `delete from bike_model where id = $1`
	res, err := o.Db.Exec(sqlstr, id)
	if err != nil {
		o.Log.Errorf("param=%d,sql=%s,err=%v", id, sqlstr, err)
		return
	}
  // 影響的行數
	ra, _ := res.RowsAffected()
	if ra != 1 {
		o.Log.Warnf("param=%d,sql=%s,rowsAftected=%d", id, sqlstr, ra)
		return int(ra)
	}
	return
}

MySQL預處理

普通SQL語句執行過程:

  • 使用者端對SQL語句進行預留位置替換得到完整的SQL語句。
  • 使用者端傳送完整SQL語句到MySQL伺服器端
  • MySQL伺服器端執行完整的SQL語句並將結果返回給使用者端。

預處理執行過程:

  • 把SQL語句分成兩部分,命令部分與資料部分。
  • 先把命令部分傳送給MySQL伺服器端,MySQL伺服器端進行SQL預處理。
  • 然後把資料部分傳送給MySQL伺服器端,MySQL伺服器端對SQL語句進行預留位置替換。
  • MySQL伺服器端執行完整的SQL語句並將結果返回給使用者端。

為什麼要預處理?

  • 優化MySQL伺服器重複執行SQL的方法,可以提升伺服器效能,提前讓伺服器編譯,一次編譯多次執行,節省後續編譯的成本。
  • 避免SQL隱碼攻擊問題。

Go實現MySQL預處理

database/sql中使用下面的Prepare方法來實現預處理操作。

func (db *DB) Prepare(query string) (*Stmt, error)

Prepare方法會先將sql語句傳送給MySQL伺服器端,返回一個準備好的狀態用於之後的查詢和命令。返回值可以同時執行多個查詢和命令。

查詢操作的預處理範例程式碼如下:

// 預處理查詢範例
func prepareQueryDemo() {
	sqlStr := "select id, name, age from user where id > ?"
	stmt, err := db.Prepare(sqlStr)
	if err != nil {
		fmt.Printf("prepare failed, err:%vn", err)
		return
	}
	defer stmt.Close()
	rows, err := stmt.Query(0)
	if err != nil {
		fmt.Printf("query failed, err:%vn", err)
		return
	}
	defer rows.Close()
	// 迴圈讀取結果集中的資料
	for rows.Next() {
		var u user
		err := rows.Scan(&u.id, &u.name, &u.age)
		if err != nil {
			fmt.Printf("scan failed, err:%vn", err)
			return
		}
		fmt.Printf("id:%d name:%s age:%dn", u.id, u.name, u.age)
	}
}

插入、更新和刪除操作的預處理十分類似,這裡以插入操作的預處理為例:

// 預處理插入範例
func prepareInsertDemo() {
	sqlStr := "insert into user(name, age) values (?,?)"
	stmt, err := db.Prepare(sqlStr)
	if err != nil {
		fmt.Printf("prepare failed, err:%vn", err)
		return
	}
	defer stmt.Close()
	_, err = stmt.Exec("小王子", 18)
	if err != nil {
		fmt.Printf("insert failed, err:%vn", err)
		return
	}
	_, err = stmt.Exec("沙河娜扎", 18)
	if err != nil {
		fmt.Printf("insert failed, err:%vn", err)
		return
	}
	fmt.Println("insert success.")
}

SQL隱碼攻擊問題

我們任何時候都不應該自己拼接SQL語句!

這裡我們演示一個自行拼接SQL語句的範例,編寫一個根據name欄位查詢user表的函數如下:

// sql注入範例
func sqlInjectDemo(name string) {
	sqlStr := fmt.Sprintf("select id, name, age from user where name='%s'", name)
	fmt.Printf("SQL:%sn", sqlStr)
	var u user
	err := db.QueryRow(sqlStr).Scan(&u.id, &u.name, &u.age)
	if err != nil {
		fmt.Printf("exec failed, err:%vn", err)
		return
	}
	fmt.Printf("user:%#vn", u)
}

此時以下輸入字串都可以引發SQL隱碼攻擊問題:

sqlInjectDemo("xxx' or 1=1#")
sqlInjectDemo("xxx' union select * from user #")
sqlInjectDemo("xxx' and (select count(*) from user) <10 #")

補充:不同的資料庫中,SQL語句使用的預留位置語法不盡相同。

資料庫預留位置語法
MySQL?
PostgreSQL$1, ​​​​​​​$2等
SQLite? 和$1
Oracle:name

Go實現MySQL事務

什麼是事務?

事務:一個最小的不可再分的工作單元;通常一個事務對應一個完整的業務(例如銀行賬戶轉賬業務,該業務就是一個最小的工作單元),同時這個完整的業務需要執行多次的DML(insert、update、delete)語句共同聯合完成。A轉賬給B,這裡面就需要執行兩次update操作。

在MySQL中只有使用了Innodb資料庫引擎的資料庫或表才支援事務。事務處理可以用來維護資料庫的完整性,保證成批的SQL語句要麼全部執行,要麼全部不執行。

事務的ACID

通常事務必須滿足4個條件(ACID):原子性(Atomicity,或稱不可分割性)、一致性(Consistency)、隔離性(Isolation,又稱獨立性)、永續性(Durability)。

條件解釋
原子性一個事務(transaction)中的所有操作,要麼全部完成,要麼全部不完成,不會結束在中間某個環節。事務在執行過程中發生錯誤,會被回滾(Rollback)到事務開始前的狀態,就像這個事務從來沒有執行過一樣。
一致性在事務開始之前和事務結束以後,資料庫的完整性沒有被破壞。這表示寫入的資料必須完全符合所有的預設規則,這包含資料的精確度、串聯性以及後續資料庫可以自發性地完成預定的工作。
隔離性資料庫允許多個並行事務同時對其資料進行讀寫和修改的能力,隔離性可以防止多個事務並行執行時由於交叉執行而導致資料的不一致。事務隔離分為不同級別,包括讀未提交(Read uncommitted)、讀提交(read committed)、可重複讀(repeatable read)和序列化(Serializable)。
永續性事務處理結束後,對資料的修改就是永久的,即便系統故障也不會丟失。

事務相關方法

Go語言中使用以下三個方法實現MySQL中的事務操作。 開始事務

func (db *DB) Begin() (*Tx, error)

提交事務

func (tx *Tx) Commit() error

回滾事務

func (tx *Tx) Rollback() error

事務範例

下面的程式碼演示了一個簡單的事務操作,該事物操作能夠確保兩次更新操作要麼同時成功要麼同時失敗,不會存在中間狀態。

// 事務操作範例
func transactionDemo() {
	tx, err := db.Begin() // 開啟事務
	if err != nil {
		if tx != nil {
			tx.Rollback() // 回滾
		}
		fmt.Printf("begin trans failed, err:%vn", err)
		return
	}
	sqlStr1 := "Update user set age=30 where id=?"
	ret1, err := tx.Exec(sqlStr1, 2)
	if err != nil {
		tx.Rollback() // 回滾
		fmt.Printf("exec sql1 failed, err:%vn", err)
		return
	}
	affRow1, err := ret1.RowsAffected()
	if err != nil {
		tx.Rollback() // 回滾
		fmt.Printf("exec ret1.RowsAffected() failed, err:%vn", err)
		return
	}
	sqlStr2 := "Update user set age=40 where id=?"
	ret2, err := tx.Exec(sqlStr2, 3)
	if err != nil {
		tx.Rollback() // 回滾
		fmt.Printf("exec sql2 failed, err:%vn", err)
		return
	}
	affRow2, err := ret2.RowsAffected()
	if err != nil {
		tx.Rollback() // 回滾
		fmt.Printf("exec ret1.RowsAffected() failed, err:%vn", err)
		return
	}
	fmt.Println(affRow1, affRow2)
	if affRow1 == 1 && affRow2 == 1 {
		fmt.Println("事務提交啦...")
		tx.Commit() // 提交事務
	} else {
		tx.Rollback()
		fmt.Println("事務回滾啦...")
	}
	fmt.Println("exec trans success!")
}

以上就是golong操作連線資料庫實現mysql事務範例的詳細內容,更多關於golong操作實現mysql事務的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com