Golang 如何優雅連接 MYSQL 數據庫
Go 原生提供了連接數據庫操作的支持,在用 Golang 進行開發的時候,如果需要在和數據庫交互,則可以使用 database/sql 包。這是一個對關係型數據庫的通用抽象,它提供了標準的、輕量的、面向行的接口。
在 Go 中訪問數據庫需要用到sql.DB
接口:它可以創建語句 (statement) 和事務(transaction),執行查詢,獲取結果。
使用數據庫時,除了database/sql
包本身,還需要引入想使用的特定數據庫驅動。官方不提供實現,先下載第三方的實現,點擊這裏查看各種各樣的實現版本。
本文測試數據庫爲 mysql,使用的驅動爲:github.com/go-sql-driver/mysql
, 需要引入的包爲:
"database/sql"
_ "github.com/go-sql-driver/mysql"
解釋一下導入包名前面的 "_" 作用:
import 下劃線(如:import _ github/demo)的作用:當導入一個包時,該包下的文件裏所有 init() 函數都會被執行,然而,有些時候我們並不需要把整個包都導入進來,僅僅是是希望它執行 init() 函數而已。這個時候就可以使用 import _ 引用該包。
上面的 mysql 驅動中引入的就是 mysql 包中各個 init() 方法,你無法通過包名來調用包中的其他函數。導入時,驅動的初始化函數會調用 sql.Register 將自己註冊在 database/sql 包的全局變量 sql.drivers 中,以便以後通過 sql.Open 訪問。
執行數據庫操作之前我們準備一張表:
CREATE TABLE `user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(45) DEFAULT '',
`age` int(11) NOT NULL DEFAULT '0',
`sex` tinyint(3) NOT NULL DEFAULT '0',
`phone` varchar(45) NOT NULL DEFAULT '',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
1. 初始化數據庫連接:#
DB, _ := sql.Open("mysql", "root:123456@tcp(127.0.0.1:3306)/test")
//設置數據庫最大連接數
DB.SetConnMaxLifetime(100)
//設置上數據庫最大閒置連接數
DB.SetMaxIdleConns(10)
//驗證連接
if err := DB.Ping(); err != nil {
fmt.Println("open database fail")
return
}
fmt.Println("connnect success")
sql.Open() 中的數據庫連接串格式爲:"用戶名:密碼@tcp(IP:端口)/數據庫?charset=utf8"
。
DB 的類型爲:*sql.DB
,有了 DB 之後我們就可以執行 CRUD 操作。Go 將數據庫操作分爲兩類:Query
與Exec
。兩者的區別在於前者會返回結果,而後者不會。
-
Query
表示查詢,它會從數據庫獲取查詢結果(一系列行,可能爲空)。 -
Exec
表示執行語句,它不會返回行。
此外還有兩種常見的數據庫操作模式:
-
QueryRow
表示只返回一行的查詢,作爲Query
的一個常見特例。 -
Prepare
表示準備一個需要多次使用的語句,供後續執行用。
2. 查詢操作#
var user User
rows, e := DB.Query("select * from user where id in (1,2,3)")
if e == nil {
errors.New("query incur error")
}
for rows.Next(){
e := rows.Scan(user.sex, user.phone, user.name, user.id, user.age)
if e != nil{
fmt.Println(json.Marshal(user))
}
}
rows.Close()
//單行查詢操作
DB.QueryRow("select * from user where id=1").Scan(user.age, user.id, user.name, user.phone, user.sex)
整體工作流程如下:
-
使用
db.Query()
來發送查詢到數據庫,獲取結果集Rows
,並檢查錯誤。 -
使用
rows.Next()
作爲循環條件,迭代讀取結果集。 -
使用
rows.Scan
從結果集中獲取一行結果。 -
使用
rows.Err()
在退出迭代後檢查錯誤。 -
使用
rows.Close()
關閉結果集,釋放連接。
3. 增刪改和 Exec#
通常不會約束你查詢必須用 Query,只是 Query 會返回結果集,而 Exec 不會返回。所以如果你執行的是增刪改操作一般用 Exec 會好一些。Exec 返回的結果是Result
,Result
接口允許獲取執行結果的元數據:
type Result interface {
// 用於返回自增ID,並不是所有的關係型數據庫都有這個功能。
LastInsertId() (int64, error)
// 返回受影響的行數。
RowsAffected() (int64, error)
}
4. 準備查詢#
如果你現在想使用佔位符的功能,where 的條件想以參數的形式傳入,Go 提供了db.Prepare
語句來幫你綁定。準備查詢的結果是一個準備好的語句(prepared statement),語句中可以包含執行時所需參數的佔位符(即綁定值)。準備查詢比拼字符串的方式好很多,它可以轉義參數,避免 SQL 注入。同時,準備查詢對於一些數據庫也省去了解析和生成執行計劃的開銷,有利於性能。
佔位符
PostgreSQL 使用$N
作爲佔位符,N
是一個從 1 開始遞增的整數,代表參數的位置,方便參數的重複使用。MySQL 使用?
作爲佔位符,SQLite 兩種佔位符都可以,而 Oracle 則使用:param1
的形式。
MySQL PostgreSQL Oracle
===== ========== ======
WHERE col = ? WHERE col = $1 WHERE col = :col
VALUES(?, ?, ?) VALUES($1, $2, $3) VALUES(:val1, :val2, :val3)
stmt, e := DB.Prepare("select * from user where id=?")
query, e := stmt.Query(1)
query.Scan()
5. 事務的使用#
通過db.Begin()
來開啓一個事務,Begin
方法會返回一個事務對象Tx
。在結果變量Tx
上調用Commit()
或者Rollback()
方法會提交或回滾變更,並關閉事務。在底層,Tx
會從連接池中獲得一個連接並在事務過程中保持對它的獨佔。事務對象Tx
上的方法與數據庫對象sql.DB
的方法一一對應,例如Query,Exec
等。事務對象也可以準備 (prepare) 查詢,由事務創建的準備語句會顯式綁定到創建它的事務。
//開啓事務
tx, err := DB.Begin()
if err != nil {
fmt.Println("tx fail")
}
//準備sql語句
stmt, err := tx.Prepare("DELETE FROM user WHERE id = ?")
if err != nil {
fmt.Println("Prepare fail")
return false
}
//設置參數以及執行sql語句
res, err := stmt.Exec(user.id)
if err != nil {
fmt.Println("Exec fail")
return false
}
//提交事務
tx.Commit()
我們來一個完整的 sql 操作:
package main
import (
"database/sql"
"encoding/json"
"fmt"
_ "github.com/go-sql-driver/mysql"
"github.com/pkg/errors"
"strings"
)
//數據庫配置
const (
userName = "root"
password = "123456"
ip = "127.0.0.1"
port = "3306"
dbName = "test"
)
//Db數據庫連接池
var DB *sql.DB
type User struct {
id int64
name string
age int8
sex int8
phone string
}
//注意方法名大寫,就是public
func InitDB() {
//構建連接:"用戶名:密碼@tcp(IP:端口)/數據庫?charset=utf8"
path := strings.Join([]string{userName, ":", password, "@tcp(", ip, ":", port, ")/", dbName, "?charset=utf8"}, "")
//打開數據庫,前者是驅動名,所以要導入: _ "github.com/go-sql-driver/mysql"
DB, _ = sql.Open("mysql", path)
//設置數據庫最大連接數
DB.SetConnMaxLifetime(100)
//設置上數據庫最大閒置連接數
DB.SetMaxIdleConns(10)
//驗證連接
if err := DB.Ping(); err != nil {
fmt.Println("open database fail")
return
}
fmt.Println("connnect success")
}
//查詢操作
func Query() {
var user User
rows, e := DB.Query("select * from user where id in (1,2,3)")
if e == nil {
errors.New("query incur error")
}
for rows.Next() {
e := rows.Scan(user.sex, user.phone, user.name, user.id, user.age)
if e != nil {
fmt.Println(json.Marshal(user))
}
}
rows.Close()
DB.QueryRow("select * from user where id=1").Scan(user.age, user.id, user.name, user.phone, user.sex)
stmt, e := DB.Prepare("select * from user where id=?")
query, e := stmt.Query(1)
query.Scan()
}
func DeleteUser(user User) bool {
//開啓事務
tx, err := DB.Begin()
if err != nil {
fmt.Println("tx fail")
}
//準備sql語句
stmt, err := tx.Prepare("DELETE FROM user WHERE id = ?")
if err != nil {
fmt.Println("Prepare fail")
return false
}
//設置參數以及執行sql語句
res, err := stmt.Exec(user.id)
if err != nil {
fmt.Println("Exec fail")
return false
}
//提交事務
tx.Commit()
//獲得上一個insert的id
fmt.Println(res.LastInsertId())
return true
}
func InsertUser(user User) bool {
//開啓事務
tx, err := DB.Begin()
if err != nil {
fmt.Println("tx fail")
return false
}
//準備sql語句
stmt, err := tx.Prepare("INSERT INTO user (`name`, `phone`) VALUES (?, ?)")
if err != nil {
fmt.Println("Prepare fail")
return false
}
//將參數傳遞到sql語句中並且執行
res, err := stmt.Exec(user.name, user.phone)
if err != nil {
fmt.Println("Exec fail")
return false
}
//將事務提交
tx.Commit()
//獲得上一個插入自增的id
fmt.Println(res.LastInsertId())
return true
}
func main() {
InitDB()
Query()
defer DB.Close()
}
轉自:JavaEdge
zhuanlan.zhihu.com/p/305152125
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/rB-mpLEPWazCISpgm1GT3g