Golang 提供了 database/sql 包用於對 SQL 的數據庫的訪問, 在這個包中, 最重要的天然就是 sql.DB 了.
對於 sql.DB, 咱們須要強調的是, 它並不表明一個數據庫鏈接
, 它是一個已存在的數據庫的抽象訪問接口. sql.DB 爲咱們提供了兩個重要的功能:mysql
sql.DB 經過數據庫驅動爲咱們管理底層數據庫鏈接的打開和關閉操做.git
sql.DB 爲咱們管理數據庫鏈接池github
有一點須要注意的是, 正由於 sql.DB 是以鏈接池的方式管理數據庫鏈接, 咱們每次進行數據庫操做時, 都須要從鏈接池中取出一個鏈接, 當操做任務完成時, 咱們須要將此鏈接返回到鏈接池中, 所以若是咱們沒有正確地將鏈接返回給鏈接池, 那麼會形成 db.SQL 打開過多的數據庫鏈接, 使數據庫鏈接資源耗盡.golang
有過數據庫開發經驗的朋友就知道了, 咱們須要藉助於一個數據庫驅動才能和具體的數據庫進行鏈接. 這在 Golang 中也不例外. 例如以 MySQL 數據庫爲例:sql
import ( "database/sql" _ "github.com/go-sql-driver/mysql" )
須要注意的是, 一般來講, 咱們不該該直接使用驅動所提供的方法, 而是應該使用 sql.DB, 所以在導入 mysql 驅動時, 咱們使用了匿名導入的方式(在包路徑前添加 _).
當導入了一個數據庫驅動後, 此驅動會自行初始化並註冊本身到 Golang 的 database/sql 上下文中, 所以咱們就能夠經過 database/sql 包提供的方法訪問數據庫了.數據庫
當導入了 MySQL 驅動後, 咱們打開數據庫鏈接:網絡
func main() { db, err := sql.Open("mysql", "user:password@tcp(127.0.0.1:3306)/test") if err != nil { log.Fatal(err) } defer db.Close() }
經過 sql.Open 函數, 能夠建立一個數據庫抽象操做接口, 若是打開成功的話, 它會返回一個 sql.DB 指針.
sql.Open 函數的簽名以下:tcp
func Open(driverName, dataSourceName string) (*DB, error)
它接收兩個參數:函數
driverName, 使用的驅動名. 這個名字其實就是數據庫驅動註冊到 database/sql 時所使用的名字.指針
dataSourceName, 第二個數據庫鏈接的連接. 這個連接包含了數據庫的用戶名, 密碼, 數據庫主機以及須要鏈接的數據庫名等信息.
須要注意的是, golang 對數據庫的鏈接是延時初始化的(lazy init), 即 sql.Open 並不會當即創建一個數據庫的網絡鏈接, 也不會對數據庫連接參數的合法性作檢驗, 它僅僅是初始化一個 sql.DB 對象.
當咱們進行第一次數據庫查詢操做時, 此時纔會真正創建網絡鏈接.
若是咱們想當即檢查數據庫鏈接是否可用, 那麼能夠利用 sql.DB 的 Ping 方法, 例如:
err = db.Ping() if err != nil { log.Fatal(err) }
sql.DB 的最佳實踐:
sql.DB 對象是做爲長期生存的對象來使用的, 咱們應當避免頻繁地調用 Open() 和 Close(). 即通常來講, 咱們要對一個數據庫進行操做時, 建立一個 sql.DB 並將其保存起來, 每次操做此數據庫時, 傳遞此 sql.DB 對象便可, 最後在須要對此數據庫進行訪問時, 關閉對應的 sql.DB 對象.
數據庫查詢的通常步驟以下:
調用 db.Query 執行 SQL 語句, 此方法會返回一個 Rows 做爲查詢的結果
經過 rows.Next() 迭代查詢數據.
經過 rows.Scan() 讀取每一行的值
調用 db.Close() 關閉查詢
例如咱們有以下一個數據庫表:
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=3 DEFAULT CHARSET=utf8mb4
咱們向其中插入一條記錄:
func insertData(db *sql.DB) { rows, err := db.Query(`INSERT INTO user (id, name, age) VALUES (1, "xys", 20)`) defer rows.Close() if err != nil { log.Fatalf("insert data error: %v\n", err) } var result int rows.Scan(&result) log.Printf("insert result %v\n", result) }
經過調用 db.Query, 咱們執行了一條 INSERT 語句插入了一條數據. 當執行完畢後, 首先須要作的是檢查語句是否執行成功, 當沒有錯誤時, 就經過 rows.Scan 獲取執行的結果. 由於 INSERT 返回的是插入的數據的行數, 所以咱們打印的語句就是 "insert result 0".
接下來如法炮製, 咱們從數據庫中將插入的數據取出:
func selectData(db *sql.DB) { var id int var name string var age int rows, err := db.Query(`SELECT * From user where id = 1`) if err != nil { log.Fatalf("insert data error: %v\n", err) return } for rows.Next() { rows.Scan(&id, &age, &name) if err != nil { log.Fatal(err) } log.Printf("get data, id: %d, name: %s, age: %d", id, name, age) } err = rows.Err() if err != nil { log.Fatal(err) } }
上面的代碼的流程基本上沒有很大的差異, 不過咱們須要注意一點的是 rows.Scan 參數的順序很重要, 須要和查詢的結果的 column 對應. 例如 "SELECT * From user where id = 1" 查詢的行的 column 順序是 "id, name, age", 所以 rows.Scan 也須要按照此順序 rows.Scan(&id, &name, &age), 否則會形成數據讀取的錯位.
注意
:
對於每一個數據庫操做都須要檢查是否有錯誤返回
每次 db.Query 操做後, 都須要調用 rows.Close(). 由於 db.Query() 會從數據庫鏈接池中獲取一個鏈接, 若是咱們沒有調用 rows.Close(), 則此鏈接會一直被佔用. 所以一般咱們使用 defer rows.Close() 來確保數據庫鏈接能夠正確放回到鏈接池中.
屢次調用 rows.Close() 不會有反作用, 所以即便咱們已經顯示地調用了 rows.Close(), 咱們仍是應該使用 defer rows.Close() 來關閉查詢.
完整的例子以下:
func insertData(db *sql.DB) { rows, err := db.Query(`INSERT INTO user (id, name, age) VALUES (1, "xys", 20)`) defer rows.Close() if err != nil { log.Fatalf("insert data error: %v\n", err) } var result int rows.Scan(&result) log.Printf("insert result %v\n", result) } func selectData(db *sql.DB) { var id int var name string var age int rows, err := db.Query(`SELECT id, name, age From user where id = 1`) if err != nil { log.Fatalf("insert data error: %v\n", err) return } for rows.Next() { err = rows.Scan(&id, &name, &age) if err != nil { log.Fatal(err) } log.Printf("get data, id: %d, name: %s, age: %d", id, name, age) } err = rows.Err() if err != nil { log.Fatal(err) } } func main() { db, err := sql.Open("mysql", "root:root@tcp(127.0.0.1:3306)/test") defer db.Close() if err != nil { fmt.Printf("connect to db 127.0.0.1:3306 error: %v\n", err) return } insertData(db) selectData(db) }
預編譯語句(PreparedStatement)提供了諸多好處, 所以咱們在開發中儘可能使用它. 下面列出了使用預編譯語句所提供的功能:
PreparedStatement 能夠實現自定義參數的查詢
PreparedStatement 一般來講, 比手動拼接字符串 SQL 語句高效.
PreparedStatement 能夠防止SQL注入攻擊
下面咱們將上一小節的例子使用 Prepared Statement 來改寫:
func deleteData(db *sql.DB) { stmt, _ := db.Prepare(`DELETE FROM user WHERE id = ?`) rows, err := stmt.Query(1) defer stmt.Close() rows.Close() if err != nil { log.Fatalf("delete data error: %v\n", err) } rows, err = stmt.Query(2) rows.Close() if err != nil { log.Fatalf("delete data error: %v\n", err) } } func insertData(db *sql.DB) { stmt, _ := db.Prepare(`INSERT INTO user (id, name, age) VALUES (?, ?, ?)`) rows, err := stmt.Query(1, "xys", 20) defer stmt.Close() rows.Close() if err != nil { log.Fatalf("insert data error: %v\n", err) } rows, err = stmt.Query(2, "test", 19) var result int rows.Scan(&result) log.Printf("insert result %v\n", result) rows.Close() } func selectData(db *sql.DB) { var id int var name string var age int stmt, _ := db.Prepare(`SELECT * From user where age > ?`) rows, err := stmt.Query(10) defer stmt.Close() defer rows.Close() if err != nil { log.Fatalf("select data error: %v\n", err) return } for rows.Next() { err = rows.Scan(&id, &name, &age) if err != nil { log.Fatal(err) } log.Printf("get data, id: %d, name: %s, age: %d", id, name, age) } err = rows.Err() if err != nil { log.Fatal(err) } } func main() { db, err := sql.Open("mysql", "root:root@tcp(127.0.0.1:3306)/test") defer db.Close() if err != nil { fmt.Printf("connect to db 127.0.0.1:3306 error: %v\n", err) return } deleteData(db) insertData(db) selectData(db) }