前言
Golang 提供了database/sql
包用於對SQL數據庫
的訪問, 作為操作數據庫的入口對象sql.DB
, 主要為我們提供了兩個重要的功能:
- sql.DB 通過數據庫驅動為我們提供管理底層數據庫連接的打開和關閉操作.
- sql.DB 為我們管理數據庫連接池
需要注意的是,sql.DB表示操作數據庫的抽象訪問接口,而非一個數據庫連接對象;它可以根據driver打開關閉數據庫連接,管理連接池。正在使用的連接被標記為繁忙,用完后回到連接池等待下次使用。所以,如果你沒有把連接釋放回連接池,會導致過多連接使系統資源耗盡。
操作mysql
1.導入mysql數據庫驅動
1 |
import ( |
通常來說, 不應該直接使用驅動所提供的方法, 而是應該使用 sql.DB, 因此在導入 mysql 驅動時, 這里使用了匿名導入的方式(在包路徑前添加 _), 當導入了一個數據庫驅動后, 此驅動會自行初始化並注冊自己到Golang的database/sql上下文中, 因此我們就可以通過 database/sql 包提供的方法訪問數據庫了.
2.連接數據庫
1 |
|
通過調用sql.Open函數返回一個sql.DB指針
; sql.Open函數原型如下:
1 |
func Open(driverName, dataSourceName string) (*DB, error) |
driverName
: 使用的驅動名. 這個名字其實就是數據庫驅動注冊到 database/sql 時所使用的名字.dataSourceName
: 數據庫連接信息,這個連接包含了數據庫的用戶名, 密碼, 數據庫主機以及需要連接的數據庫名等信息.
- sql.Open並不會立即建立一個數據庫的網絡連接, 也不會對數據庫鏈接參數的合法性做檢驗, 它僅僅是初始化一個sql.DB對象. 當真正進行第一次數據庫查詢操作時, 此時才會真正建立網絡連接;
- sql.DB表示操作數據庫的抽象接口的對象,但不是所謂的數據庫連接對象,sql.DB對象只有當需要使用時才會創建連接,如果想立即驗證連接,需要用Ping()方法;
- sql.Open返回的sql.DB對象是協程並發安全的.
- sql.DB的設計就是用來作為長連接使用的。不要頻繁Open, Close。比較好的做法是,為每個不同的datastore建一個DB對象,保持這些對象Open。如果需要短連接,那么把DB作為參數傳入function,而不要在function中Open, Close。
3.數據庫基本操作
數據庫查詢的一般步驟如下:
- 調用 db.Query 執行 SQL 語句, 此方法會返回一個 Rows 作為查詢的結果
- 通過 rows.Next() 迭代查詢數據.
- 通過 rows.Scan() 讀取每一行的值
- 調用 db.Close() 關閉查詢
現有user
數據庫表如下:
1 |
CREATE TABLE `user` ( |
MySQL 5.5 之前, UTF8 編碼只支持1-3個字節,從MYSQL5.5開始,可支持4個字節UTF編碼utf8mb4,一個字符最多能有4字節,utf8mb4兼容utf8,所以能支持更多的字符集;關於emoji表情的話mysql的utf8是不支持,需要修改設置為utf8mb4,才能支持。
查詢數據
1 |
func (dbw *DbWorker) QueryData() { |
- rows.Scan 參數的順序很重要, 需要和查詢的結果的column對應. 例如 “SELECT * From user where age >=20 AND age < 30” 查詢的行的 column 順序是 “id, name, age” 和插入操作順序相同, 因此 rows.Scan 也需要按照此順序 rows.Scan(&id, &name, &age), 不然會造成數據讀取的錯位.
- 因為golang是強類型語言,所以查詢數據時先定義數據類型,但是查詢數據庫中的數據存在三種可能:存在值,存在零值,未賦值NULL 三種狀態, 因為可以將待查詢的數據類型定義為sql.Nullxxx類型,可以通過判斷Valid值來判斷查詢到的值是否為賦值狀態還是未賦值NULL狀態.
- 每次db.Query操作后, 都建議調用rows.Close(). 因為 db.Query() 會從數據庫連接池中獲取一個連接, 這個底層連接在結果集(rows)未關閉前會被標記為處於繁忙狀態。當遍歷讀到最后一條記錄時,會發生一個內部EOF錯誤,自動調用rows.Close(),但如果提前退出循環,rows不會關閉,連接不會回到連接池中,連接也不會關閉, 則此連接會一直被占用. 因此通常我們使用 defer rows.Close() 來確保數據庫連接可以正確放回到連接池中; 不過閱讀源碼發現rows.Close()操作是冪等操作,即一個冪等操作的特點是其任意多次執行所產生的影響均與一次執行的影響相同, 所以即便對已關閉的rows再執行close()也沒關系.
單行查詢
1 |
var name string |
- err在Scan后才產生,上述鏈式寫法是對的
- 需要注意Scan()中變量和順序要和前面Query語句中的順序一致,否則查出的數據會映射不一致.
插入數據
1 |
func (dbw *DbWorker) insertData() { |
通過db.Exec()
插入數據,通過返回的err
可知插入失敗的原因,通過返回的ret
可以進一步查詢本次插入數據影響的行數RowsAffected
和最后插入的Id(如果數據庫支持查詢最后插入Id).
4.預編譯語句(Prepared Statement)
預編譯語句(PreparedStatement)提供了諸多好處, 因此我們在開發中盡量使用它. 下面列出了使用預編譯語句所提供的功能:
- PreparedStatement 可以實現自定義參數的查詢
- PreparedStatement 通常來說, 比手動拼接字符串 SQL 語句高效.
- PreparedStatement 可以防止SQL注入攻擊
一般用Prepared Statements
和Exec()
完成INSERT
, UPDATE
, DELETE
操作。
下面是將上述案例用Prepared Statement 修改之后的完整代碼
1 |
package main |
db.Prepare()返回的statement使用完之后需要手動關閉,即defer stmt.Close()