## prepare
前面我們已經(jīng)學(xué)習(xí)了sql的基本curd操作??傮w而言,有兩類操作Query和Exec。前者返回數(shù)據(jù)庫記錄,后者返回數(shù)據(jù)庫影響或插入相關(guān)的結(jié)果。上面兩種操作,多數(shù)是針對單次操作的查詢。如果需要批量插入一堆數(shù)據(jù),就可以使用Prepared語句。golang處理prepared語句有其獨特的行為,了解其底層的實現(xiàn),對于用好它十分重要。
查詢
我們可以使用Query方式查詢記錄,Query函數(shù)提供了兩種選擇,第一種情況下參數(shù)是拼接好的sql,另外一種情況,第一參數(shù)是帶有占位符的sql,第二個參數(shù)為sql的實際參數(shù)。
rows, err := db.Query("SELECT * FROM user WHERE gid = 1")
rows, err := db.Query("SELECT * FROM user WHERE gid = ?", 1)
上面兩種方式都能獲取數(shù)據(jù),那么他們的底層實現(xiàn)是一樣的么?實際上,上面兩種方式的底層通信不完全一樣。一種你是plaintext方式,另外一種是prepared方式。
prepared
所謂prepared,即帶有占位符的sql語句,客戶端將該語句和參數(shù)發(fā)給mysql服務(wù)器。mysql服務(wù)器編譯成一個prepared語句,這個語句可以根據(jù)不同的參數(shù)多次調(diào)用。prepared語句執(zhí)行的方式如下:
- 準(zhǔn)備prepare語句
- 執(zhí)行prepared語句和參數(shù)
- 關(guān)閉prepared語句
之所以會出現(xiàn)prepare語句方式,主要因為這樣有下面的兩個好處:
- 避免通過引號組裝拼接sql語句。避免sql注入帶來的安全風(fēng)險
- 可以多次執(zhí)行的sql語句。
單純的看prepared語句發(fā)好處,會下意識的覺得既然如此,都使用prepared語句查詢不就好了么?其實不然。關(guān)于prepared語句注意事項,稍后再討論。
golang的pliantext和prepare查詢方式
現(xiàn)在我們再回顧上面調(diào)用Qeury函數(shù)的兩個操作。對于第一個操作,執(zhí)行pliantext的sql語句。先看db.Query方法:
// Query executes a query that returns rows, typically a SELECT.
// The args are for any placeholder parameters in the query.
func (db *DB) Query(query string, args ...interface{}) (*Rows, error) {
var rows *Rows
var err error
for i := 0; i < maxBadConnRetries; i++ {
rows, err = db.query(query, args, cachedOrNewConn) // 查詢
if err != driver.ErrBadConn {
break
}
}
if err == driver.ErrBadConn {
return db.query(query, args, alwaysNewConn)
}
return rows, err
}
Query方法我們很熟悉了,它的內(nèi)部調(diào)用了db.query方法,并且根據(jù)連接重連的狀況選擇是cachedOrNewConn模式還是alwaysNewConn模式。前者會從返回一個cached連接或者等待一個可用連接,甚至也可能建立一個新的連接;后者表示打開連接時的策略為每次建立一個新的連接。這就是簽名所說的retry10次連接。
func (db *DB) query(query string, args []interface{}, strategy connReuseStrategy) (*Rows, error) {
ci, err := db.conn(strategy)
if err != nil {
return nil, err
}
return db.queryConn(ci, ci.releaseConn, query, args)
}
query方法邏輯很簡單,通過db.conn方法返回一個新創(chuàng)建或者緩存的空閑連接。driverConn。隨機調(diào)用queryConn方法。
// queryConn executes a query on the given connection.
// The connection gets released by the releaseConn function.
func (db *DB) queryConn(dc *driverConn, releaseConn func(error), query string, args []interface{}) (*Rows, error) {
// 判斷驅(qū)動是否實現(xiàn)了Queryer
if queryer, ok := dc.ci.(driver.Queryer); ok {
dargs, err := driverArgs(nil, args)
if err != nil {
releaseConn(err)
return nil, err
}
dc.Lock()
rowsi, err := queryer.Query(query, dargs) // 調(diào)用驅(qū)動的查詢方法 connection.go 第305行
dc.Unlock()
if err != driver.ErrSkip { // 不帶參數(shù)的返回
if err != nil {
releaseConn(err)
return nil, err
}
// Note: ownership of dc passes to the *Rows, to be freed
// with releaseConn.
rows := &Rows{
dc: dc,
releaseConn: releaseConn,
rowsi: rowsi,
}
return rows, nil
}
}
dc.Lock()
si, err := dc.ci.Prepare(query) // 帶參數(shù)的返回,創(chuàng)建prepare對象
dc.Unlock()
if err != nil {
releaseConn(err)
return nil, err
}
ds := driverStmt{dc, si}
rowsi, err := rowsiFromStatement(ds, args...) // 執(zhí)行語句
if err != nil {
dc.Lock()
si.Close()
dc.Unlock()
releaseConn(err)
return nil, err
}
// Note: ownership of ci passes to the *Rows, to be freed
// with releaseConn.
rows := &Rows{
dc: dc,
releaseConn: releaseConn,
rowsi: rowsi,
closeStmt: si,
}
return rows, nil
}
queryConn函數(shù)內(nèi)容比較多。先判斷驅(qū)動是否實現(xiàn)了Queryer,如果實現(xiàn)了即調(diào)用其Query方法。方法會針對sql查詢語句做查詢。例如mysql的驅(qū)動如下,connection.go 第305行左右,即:
func (mc *mysqlConn) Query(query string, args []driver.Value) (driver.Rows, error) {
if mc.netConn == nil {
errLog.Print(ErrInvalidConn)
return nil, driver.ErrBadConn
}
if len(args) != 0 {
if !mc.cfg.InterpolateParams {
return nil, driver.ErrSkip
}
// try client-side prepare to reduce roundtrip
prepared, err := mc.interpolateParams(query, args)
if err != nil {
return nil, err
}
query = prepared
args = nil
}
// Send command
err := mc.writeCommandPacketStr(comQuery, query)
if err == nil {
// Read Result
var resLen int
resLen, err = mc.readResultSetHeaderPacket()
if err == nil {
rows := new(textRows)
rows.mc = mc
if resLen == 0 {
// no columns, no more data
return emptyRows{}, nil
}
// Columns
rows.columns, err = mc.readColumns(resLen)
return rows, err
}
}
return nil, err
}
Query先檢查參數(shù)是否為0,然后調(diào)用writeCommandPacketStr方法執(zhí)行sql并通過readResultSetHeaderPacket讀取數(shù)據(jù)庫服務(wù)返回的結(jié)果。
如果參數(shù)不為0,會先判斷是否是prepared語句。這里會返回一個driver.ErrSkip錯誤。把函數(shù)執(zhí)行權(quán)再返回到queryConn函數(shù)中。然后再調(diào)用si, err := dc.ci.Prepare(query)創(chuàng)建Stmt對象,接下來調(diào)用rowsiFromStatement執(zhí)行查詢:
func rowsiFromStatement(ds driverStmt, args ...interface{}) (driver.Rows, error) {
ds.Lock()
want := ds.si.NumInput()
ds.Unlock()
// -1 means the driver doesn't know how to count the number of
// placeholders, so we won't sanity check input here and instead let the
// driver deal with errors.
if want != -1 && len(args) != want {
return nil, fmt.Errorf("sql: statement expects %d inputs; got %d", want, len(args))
}
dargs, err := driverArgs(&ds, args)
if err != nil {
return nil, err
}
ds.Lock()
rowsi, err := ds.si.Query(dargs)
ds.Unlock()
if err != nil {
return nil, err
}
return rowsi, nil
}
rowsiFromStatement方法會調(diào)用驅(qū)動的ds.si.Query(dargs)方法,執(zhí)行最后的查詢。大概再statement.go的第84行
func (stmt *mysqlStmt) Query(args []driver.Value) (driver.Rows, error) {
if stmt.mc.netConn == nil {
errLog.Print(ErrInvalidConn)
return nil, driver.ErrBadConn
}
// Send command
err := stmt.writeExecutePacket(args)
if err != nil {
return nil, err
}
mc := stmt.mc
// Read Result
resLen, err := mc.readResultSetHeaderPacket()
if err != nil {
return nil, err
}
rows := new(binaryRows)
if resLen > 0 {
rows.mc = mc
// Columns
// If not cached, read them and cache them
if stmt.columns == nil {
rows.columns, err = mc.readColumns(resLen)
stmt.columns = rows.columns
} else {
rows.columns = stmt.columns
err = mc.readUntilEOF()
}
}
return rows, err
}
調(diào)用 stmt和參數(shù)執(zhí)行sql查詢。查詢完畢之后,返回到queryConn方法中,使用releaseConn釋放查詢的數(shù)據(jù)庫連接。
自定義prepare 查詢
從query查詢可以看到,對于占位符的prepare語句,go內(nèi)部通過的dc.ci.Prepare(query)會自動創(chuàng)建一個 stmt對象。其實我們也可以自定義stmt語句,使用方式如下:
stmt, err := db.Prepare("SELECT * FROM user WHERE gid = ?")
if err != nil {
log.Fatalln(err)
}
defer stmt.Close()
rows, err := stmt.Query(1)
if err != nil{
log.Fatalln(err)
}
即通過Prepare方法創(chuàng)建一個stmt對象,然后執(zhí)行stmt對象的Query(Exec)方法得到sql.Rows結(jié)果集。最后關(guān)閉stmt.Close。這個過程就和之前所說的prepare三步驟匹配了。
創(chuàng)建stmt的preprea方式是golang的一個設(shè)計,其目的是Prepare once, execute many times。為了批量執(zhí)行sql語句。但是通常會造成所謂的三次網(wǎng)絡(luò)請求( three network round-trips)。即preparing executing和closing三次請求。
對于大多數(shù)數(shù)據(jù)庫,prepread的過程都是,先發(fā)送一個帶占位符的sql語句到服務(wù)器,服務(wù)器返回一個statement id,然后再把這個id和參數(shù)發(fā)送給服務(wù)器執(zhí)行,最后再發(fā)送關(guān)閉statement命令。
golang的實現(xiàn)了連接池,處理prepare方式也需要特別注意。調(diào)用Prepare方法返回stmt的時候,golang會在某個空閑的連接上進行prepare語句,然后就把連接釋放回到連接池,可是golang會記住這個連接,當(dāng)需要執(zhí)行參數(shù)的時候,就再次找到之前記住的連接進行執(zhí)行,等到stmt.Close調(diào)用的時候,再釋放該連接。
在執(zhí)行參數(shù)的時候,如果記住的連接正處于忙碌階段,此時golang將會從新選一個新的空閑連接進行prepare(re-prepare)。當(dāng)然,即使是重新reprepare,同樣也會遇到剛才的問題。那么將會一而再再而三的進行reprepare。直到找到空閑連接進行查詢的時候。
這種情況將會導(dǎo)致leak連接的情況,尤其是再高并發(fā)的情景。將會導(dǎo)致大量的prepare過程。因此使用stmt的情況需要仔細考慮應(yīng)用場景,通常在應(yīng)用程序中。多次執(zhí)行同一個sql語句的情況并不多,因此減少prepare語句的使用。
之前有一個疑問,是不是所有sql語句都不能帶占位符,因為這是prepare語句。只要看了一遍database/sql和驅(qū)動的源碼才恍然大悟,對于query(prepare, args)的方式,正如我們前面所分析的,database/sql會使用ds.si.Query(dargs)創(chuàng)建stmt的,然后就立即執(zhí)行prepare和參數(shù),最后關(guān)閉stmt。整個過程都是同一個連接上完成,因此不存在reprepare的情況。當(dāng)然也無法使用所謂的創(chuàng)建一次,執(zhí)行多次的目。
對于prepare的使用方式,基于其好處和缺點,我們將會再后面的最佳實踐再討論。目前需要注意的大致就是:
單次查詢不需要使用prepared,每次使用stmt語句都是三次網(wǎng)絡(luò)請求次數(shù),prepared execute close
不要循環(huán)中創(chuàng)建prepare語句
注意關(guān)閉 stmt
盡管會有reprepare過程,這些操作依然是database/sql幫我們所做的,與連接retry10次一樣,開發(fā)者無需擔(dān)心。
對于Qeruy操作如此,同理Exec操作也一樣。
總結(jié)
目前我們學(xué)習(xí)database/sql提供兩類查詢操作,Query和Exec方法。他們都可以使用plaintext和preprea方式查詢。對于后者,可以有效的避免數(shù)據(jù)庫注入。而prepare方式又可以有顯示的聲明stmt對象,也有隱藏的方式。顯示的創(chuàng)建stmt會有3次網(wǎng)絡(luò)請求,創(chuàng)建->執(zhí)行->關(guān)閉,再批量操作可以考慮這種做法,另外一種方式創(chuàng)建prepare后就執(zhí)行,因此不會因為reprepare導(dǎo)致高并發(fā)下的leak連接問題。
具體使用那種方式,還得基于應(yīng)用場景,安全過濾和連接管理等考慮。至此,關(guān)于查詢和執(zhí)行操作已經(jīng)介紹了很多。關(guān)系型數(shù)據(jù)庫的另外一個特性就是關(guān)系和事務(wù)處理。下一節(jié),我們將會討論database/sql的數(shù)據(jù)庫事務(wù)功能。