Go原生sql操作MySQL


MySQL是業界常用的關系型數據庫,Go語言如何操作MySQL數據庫之一的方法。

連接

Go語言中的database/sql包提供了保證SQL或類SQL數據庫的泛用接口,並不提供具體的數據庫驅動。使用database/sql包時必須注入(至少)一個數據庫驅動:github.com/go-sql-driver/mysql

下載依賴

go get -u database/sql
go get -u github.com/go-sql-driver/mysql

配置mysql驅動

func Open(driverName, dataSourceName string) (*DB, error)

Open打開一個dirverName指定的數據庫,dataSourceName指定數據源,一般至少包括數據庫文件名和其它連接地址端口等必要信息。
import (
	"database/sql"

	_ "github.com/go-sql-driver/mysql" // 需要匿名加載初始化驅動
)

func main() {
   // dsn:dataSourceName指定數據源
	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對象,可以讓Open函數應該僅被調用一次,很少關閉DB對象,這樣可以有效goroutine並發使用,並且維護其自己的空閑連接池。

package main

import (
	"database/sql"
	"fmt"

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

// 定義一個全局對象db
var db *sql.DB

// 定義一個初始化數據庫的函數
func initDB() (err error) {
	dsn := "root:root1234@tcp(127.0.0.1:13306)/sql_demo"
	// 不會校驗賬號密碼是否正確
	// 注意!!!這里不要使用:=,我們是給全局變量賦值,然后在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:%v\n", err)
		return
	}
}

sql.DB是表示連接的數據庫對象(結構體實例),它保存了連接數據庫相關的所有信息。它內部維護着一個具有零到多個底層連接的連接池,它可以安全地被多個goroutine同時使用。

SetConnMaxLifetime最長連接時間

SetMaxOpenConns最大開啟連接數

設置與數據庫建立連接的最大數目。

如果大於0且小於最大空閑連接數,會將最大空閑連接數減小到匹配最大開啟連接數的限制。

如果<=0,不會限制最大開啟連接數,默認為0(無限制)。

SetMaxIdleConns 最大空閑連接數

設置連接池中的最大空閑連接數。

如果大於最大開啟連接數,則新的最大空閑連接數會減小到匹配最大開啟連接數的限制。

如果<=0,不會保留空閑連接。

package main

import (
	"database/sql"
	"fmt"

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

var db *sql.DB

func initDB() (err error) {
	dsn := "root:root1234@tcp(127.0.0.1:13306)/sql_demo?charset=utf8mb4&parseTime=True"
	db, err = sql.Open("mysql", dsn)
	if err != nil {
		return err
	}
	// 數值需要業務具體情況來確定
	//db.SetConnMaxLifetime(time.Second*10)
	db.SetMaxOpenConns(100) // 最大連接數
	db.SetMaxIdleConns(10)  // 最大空閑連接數
	return nil
}

func main() {
	err := initDB()
	if err != nil {
		fmt.Printf("init db failed,err:%v\n", err)
		return
	}
}

CURD

增改查刪(增刪改查)

建庫建表

CREATE DATABASE demo;

use demo;

CREATE TABLE `user` (
    `id` BIGINT(20) NOT NULL AUTO_INCREMENT,
    `name` VARCHAR(20) DEFAULT '',
    `age` INT(11) DEFAULT '0',
    PRIMARY KEY(`id`)
)ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;

增-插入數據

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

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

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

// 插入數據
func insertRowDemo() {
	sqlStr := "insert into user(name, age) values (?,?)"
	ret, err := db.Exec(sqlStr, "xx", 23)
	if err != nil {
		fmt.Printf("insert failed, err:%v\n", err)
		return
	}
	var theID int64
	theID, err = ret.LastInsertId() // 新插入數據的id
	if err != nil {
		fmt.Printf("get lastinsert ID failed, err:%v\n", err)
		return
	}
	fmt.Printf("insert success, the id is %d.\n", theID)
}

刪-刪除數據

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

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

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

// 刪除數據
func deleteRowDemo() {
	sqlStr := "delete from user where id = ?"
	ret, err := db.Exec(sqlStr, 0)
	if err != nil {
		fmt.Printf("delete failed, err:%v\n", err)
		return
	}
	var n int64
	n, err = ret.RowsAffected() // 操作影響的行數
	if err != nil {
		fmt.Printf("get RowsAffected failed, err:%v\n", err)
		return
	}
	fmt.Printf("delete success, affected rows:%d\n", n)
}

改-更新數據

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

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

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

// 更新數據
func updateRowDemo() {
	sqlStr := "update user set age=? where id = ?"
	ret, err := db.Exec(sqlStr, 39, 3)
	if err != nil {
		fmt.Printf("update failed, err:%v\n", err)
		return
	}
	var n int64
	n, err = ret.RowsAffected() // 操作影響的行數
	if err != nil {
		fmt.Printf("get RowsAffected failed, err:%v\n", err)
		return
	}
	fmt.Printf("update success, affected rows:%d\n", n)
}

查-查詢數據

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

type user struct {
   id   int // 同類型放一起對使用內存地址更好,更有順序
   age  int
   name string
}

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

func (db *DB) QueryRow(query string, args ...interface{}) *Row
// 查詢單條數據示例
func queryRowDemo() {
   sqlStr := "select id, name, age from user where id=?"
   var u user
   // 非常重要:確保QueryRow之后調用Scan方法,否則持有的數據庫鏈接不會被釋放
   // row := db.QueryRow(sqlStr, 1) // 並沒有對其做scan操作

   err := db.QueryRow(sqlStr, 2).Scan(&u.id, &u.name, &u.age)
   if err != nil {
      fmt.Printf("scan failed, err:%v\n", err)
      return
   }
   fmt.Printf("id:%d name:%s age:%d\n", u.id, u.name, u.age)
}

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

func (db *DB) Query(query string, args ...interface{}) (*Rows, error)
// 查詢多條數據示例
func queryMultiRowDemo() {
   sqlStr := "select id, name, age from user where id > ?"
   rows, err := db.Query(sqlStr, 0)
   if err != nil {
      fmt.Printf("query failed, err:%v\n", err)
      return
   }
   // 非常重要:關閉rows釋放持有的數據庫鏈接
   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:%v\n", err)
         return
      }
      fmt.Printf("id:%d name:%s age:%d\n", u.id, u.name, u.age)
   }
}

Mysql預處理

普通SQL語句執行過程:

  1. 客戶端對SQL語句進行占位符替換得到完整的SQL語句。
  2. 客戶端發送完整SQL語句到MySQL服務端
  3. MySQL服務端執行完整的SQL語句並將結果返回給客戶端。

預處理執行過程:

  1. 把SQL語句分成兩部分,命令部分與數據部分。
  2. 先把命令部分發送給MySQL服務端,MySQL服務端進行SQL預處理。
  3. 然后把數據部分發送給MySQL服務端,MySQL服務端對SQL語句進行占位符替換。
  4. MySQL服務端執行完整的SQL語句並將結果返回給客戶端。

為什么要預處理?

1. 優化MySQL服務器重復執行SQL的方法,既可以提升服務器性能,提前讓服務器編譯,一次編譯多次執行,節省后續編譯的成本.
2. 避免SQL注入問題.

Go實現Mysql預處理

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

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

Prepare方法過程: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:%v\n", err)
		return
	}
	defer stmt.Close()
	rows, err := stmt.Query(3)
	if err != nil {
		fmt.Printf("query failed, err:%v\n", 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:%v\n", err)
			return
		}
		fmt.Printf("id:%d name:%s age:%d\n", 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:%v\n", err)
		return
	}
	defer stmt.Close()
	_, err = stmt.Exec("kk", 18)
	if err != nil {
		fmt.Printf("insert failed, err:%v\n", err)
		return
	}
	_, err = stmt.Exec("xx", 23)
	if err != nil {
		fmt.Printf("insert failed, err:%v\n", err)
		return
	}
	fmt.Println("insert success.")
}

預處理更新

// 預處理更新示例
func prepareUpdateDemo() {
	sqlStr := "update user set age=? where id = ?"
	stmt, err := db.Prepare(sqlStr)
	if err != nil {
		fmt.Printf("prepare failed err:%v\n", err)
	}
	defer stmt.Close()
	_, err = stmt.Exec(18, 5)
	if err != nil {
		fmt.Printf("update failed, err:%v\n", err)
		return
	}
	fmt.Println("update success.")
}

預處理刪除

// 預處理刪除
func prepareDeleteDemo() {
	sqlStr := "delete from user where id = ?"
	stmt, err := db.Prepare(sqlStr)
	if err != nil {
		fmt.Printf("prepare failed err:%v\n", err)
	}
	defer stmt.Close()
	_, err = stmt.Exec(5)
	if err != nil {
		fmt.Printf("delete failed, err:%v\n", err)
		return
	}
	fmt.Println("delete success.")
}

SQL注入問題

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

SQL注入示例

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

輸入字符串都可以引發SQL注入問題

sqlInjectDemo("xxx' or 1=1#")
sqlInjectDemo("xxx' union select * from user #")

不同數據庫占位符語法

MySQL事務

什么是事務?

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

在MySQL中只有使用了Innodb數據庫引擎的數據庫或表才支持事務。事務處理可以用來維護數據庫的完整性,保證成批的SQL語句要么全部執行,要么全部不執行。
Transaction生命周期.png

事務ACID

A:atomicity原子性;整個事務中的所有操作要么全部成功執行,要么全部失敗后回滾
C:consistency一致性;數據庫總是從一個一致性狀態轉換為另一個一致性狀態
I:Isolation隔離性;一個事務所做出的操作在提交之前,是不能為其它事務所見;隔離有多種隔離級別,實現並發
D:durability持久性;一旦事務提交,其所做的修改會永久保存於數據庫中

事務相關方法

開始事務

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

MySQL:begin;

提交事務

func (tx *Tx) Commit() error

MySQL:commit;

回滾事務

func (tx *Tx) Rollback() error

MySQL:rollback;

事務示例

// 事務操作示例
func transactionDemo() {
	tx, err := db.Begin() // 開啟事務
	if err != nil {
		if tx != nil {
			tx.Rollback() // 回滾
		}
		fmt.Printf("begin trans failed, err:%v\n", err)
		return
	}
	sqlStr1 := "Update user set age=23 where id=?"
	ret1, err := tx.Exec(sqlStr1, 2)
	if err != nil {
		tx.Rollback() // 回滾
		fmt.Printf("exec sql1 failed, err:%v\n", err)
		return
	}
	affRow1, err := ret1.RowsAffected()
	if err != nil {
		tx.Rollback() // 回滾
		fmt.Printf("exec ret1.RowsAffected() failed, err:%v\n", err)
		return
	}

	sqlStr2 := "Update user set age=23 where id=?"
	ret2, err := tx.Exec(sqlStr2, 3)
	if err != nil {
		tx.Rollback() // 回滾
		fmt.Printf("exec sql2 failed, err:%v\n", err)
		return
	}
	affRow2, err := ret2.RowsAffected()
	if err != nil {
		tx.Rollback() // 回滾
		fmt.Printf("exec ret1.RowsAffected() failed, err:%v\n", err)
		return
	}

	// 當affRow1 == 1 && affRow2 == 1, 兩個都成功才提交事務,否則回滾
	fmt.Println(affRow1, affRow2)
	if affRow1 == 1 && affRow2 == 1 {
		fmt.Println("事務提交啦...")
		tx.Commit() // 提交事務
	} else {
		tx.Rollback()
		fmt.Println("事務回滾啦...")
	}

	fmt.Println("exec trans success!")
}


免責聲明!

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



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