## prepare
前面我們已經(jīng)學(xué)習(xí)了sql的基本curd操作抗悍〗稍ǎ總體而言,有兩類操作Query和Exec。前者返回?cái)?shù)據(jù)庫記錄田柔,后者返回?cái)?shù)據(jù)庫影響或插入相關(guān)的結(jié)果俐巴。上面兩種操作,多數(shù)是針對單次操作的查詢硬爆。如果需要批量插入一堆數(shù)據(jù)欣舵,就可以使用Prepared語句。golang處理prepared語句有其獨(dú)特的行為缀磕,了解其底層的實(shí)現(xiàn)缘圈,對于用好它十分重要。
查詢
我們可以使用Query方式查詢記錄袜蚕,Query函數(shù)提供了兩種選擇糟把,第一種情況下參數(shù)是拼接好的sql牲剃,另外一種情況遣疯,第一參數(shù)是帶有占位符的sql,第二個(gè)參數(shù)為sql的實(shí)際參數(shù)凿傅。
rows, err := db.Query("SELECT * FROM user WHERE gid = 1")
rows, err := db.Query("SELECT * FROM user WHERE gid = ?", 1)
上面兩種方式都能獲取數(shù)據(jù)缠犀,那么他們的底層實(shí)現(xiàn)是一樣的么?實(shí)際上聪舒,上面兩種方式的底層通信不完全一樣辨液。一種你是plaintext方式,另外一種是prepared方式箱残。
prepared
所謂prepared滔迈,即帶有占位符的sql語句,客戶端將該語句和參數(shù)發(fā)給mysql服務(wù)器。mysql服務(wù)器編譯成一個(gè)prepared語句亡鼠,這個(gè)語句可以根據(jù)不同的參數(shù)多次調(diào)用赏殃。prepared語句執(zhí)行的方式如下:
- 準(zhǔn)備prepare語句
- 執(zhí)行prepared語句和參數(shù)
- 關(guān)閉prepared語句
之所以會出現(xiàn)prepare語句方式,主要因?yàn)檫@樣有下面的兩個(gè)好處:
- 避免通過引號組裝拼接sql語句间涵。避免sql注入帶來的安全風(fēng)險(xiǎn)
- 可以多次執(zhí)行的sql語句仁热。
單純的看prepared語句發(fā)好處,會下意識的覺得既然如此勾哩,都使用prepared語句查詢不就好了么抗蠢?其實(shí)不然。關(guān)于prepared語句注意事項(xiàng)思劳,稍后再討論迅矛。
golang的pliantext和prepare查詢方式
現(xiàn)在我們再回顧上面調(diào)用Qeury函數(shù)的兩個(gè)操作。對于第一個(gè)操作潜叛,執(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
模式销斟。前者會從返回一個(gè)cached連接或者等待一個(gè)可用連接,甚至也可能建立一個(gè)新的連接椒舵;后者表示打開連接時(shí)的策略為每次建立一個(gè)新的連接蚂踊。這就是簽名所說的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方法返回一個(gè)新創(chuàng)建或者緩存的空閑連接犁钟。driverConn。隨機(jī)調(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ū)動是否實(shí)現(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ū)動是否實(shí)現(xiàn)了Queryer侥加,如果實(shí)現(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語句。這里會返回一個(gè)driver.ErrSkip錯(cuò)誤狈网。把函數(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)建一個(gè) stmt對象肥照。其實(shí)我們也可以自定義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)建一個(gè)stmt對象勤众,然后執(zhí)行stmt對象的Query(Exec)方法得到sql.Rows結(jié)果集舆绎。最后關(guān)閉stmt.Close。這個(gè)過程就和之前所說的prepare三步驟匹配了们颜。
創(chuàng)建stmt的preprea方式是golang的一個(gè)設(shè)計(jì)吕朵,其目的是Prepare once, execute many times
。為了批量執(zhí)行sql語句窥突。但是通常會造成所謂的三次網(wǎng)絡(luò)請求( three network round-trips)边锁。即preparing executing和closing三次請求。
對于大多數(shù)數(shù)據(jù)庫波岛,prepread的過程都是,先發(fā)送一個(gè)帶占位符的sql語句到服務(wù)器音半,服務(wù)器返回一個(gè)statement id则拷,然后再把這個(gè)id和參數(shù)發(fā)送給服務(wù)器執(zhí)行,最后再發(fā)送關(guān)閉statement命令曹鸠。
golang的實(shí)現(xiàn)了連接池煌茬,處理prepare方式也需要特別注意。調(diào)用Prepare方法返回stmt的時(shí)候彻桃,golang會在某個(gè)空閑的連接上進(jìn)行prepare語句坛善,然后就把連接釋放回到連接池,可是golang會記住這個(gè)連接邻眷,當(dāng)需要執(zhí)行參數(shù)的時(shí)候眠屎,就再次找到之前記住的連接進(jìn)行執(zhí)行,等到stmt.Close調(diào)用的時(shí)候肆饶,再釋放該連接改衩。
在執(zhí)行參數(shù)的時(shí)候,如果記住的連接正處于忙碌階段驯镊,此時(shí)golang將會從新選一個(gè)新的空閑連接進(jìn)行prepare(re-prepare)葫督。當(dāng)然竭鞍,即使是重新reprepare,同樣也會遇到剛才的問題橄镜。那么將會一而再再而三的進(jìn)行reprepare偎快。直到找到空閑連接進(jìn)行查詢的時(shí)候。
這種情況將會導(dǎo)致leak連接的情況洽胶,尤其是再高并發(fā)的情景晒夹。將會導(dǎo)致大量的prepare過程。因此使用stmt的情況需要仔細(xì)考慮應(yīng)用場景妖异,通常在應(yīng)用程序中惋戏。多次執(zhí)行同一個(gè)sql語句的情況并不多,因此減少prepare語句的使用他膳。
之前有一個(gè)疑問响逢,是不是所有sql語句都不能帶占位符,因?yàn)檫@是prepare語句棕孙。只要看了一遍database/sql和驅(qū)動的源碼才恍然大悟舔亭,對于query(prepare, args)的方式,正如我們前面所分析的蟀俊,database/sql會使用ds.si.Query(dargs)創(chuàng)建stmt的钦铺,然后就立即執(zhí)行prepare和參數(shù),最后關(guān)閉stmt肢预。整個(gè)過程都是同一個(gè)連接上完成矛洞,因此不存在reprepare的情況。當(dāng)然也無法使用所謂的創(chuàng)建一次烫映,執(zhí)行多次的目沼本。
對于prepare的使用方式,基于其好處和缺點(diǎn)锭沟,我們將會再后面的最佳實(shí)踐再討論抽兆。目前需要注意的大致就是:
單次查詢不需要使用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í)行阁苞,因此不會因?yàn)閞eprepare導(dǎo)致高并發(fā)下的leak連接問題。
具體使用那種方式祠挫,還得基于應(yīng)用場景那槽,安全過濾和連接管理等考慮。至此等舔,關(guān)于查詢和執(zhí)行操作已經(jīng)介紹了很多骚灸。關(guān)系型數(shù)據(jù)庫的另外一個(gè)特性就是關(guān)系和事務(wù)處理。下一節(jié)慌植,我們將會討論database/sql的數(shù)據(jù)庫事務(wù)功能甚牲。