database/sql
database/sql是golang的標(biāo)準(zhǔn)庫(kù)之一姓迅,它提供了一系列接口方法,用于訪問(wèn)關(guān)系數(shù)據(jù)庫(kù)它掂。它并不會(huì)提供數(shù)據(jù)庫(kù)特有的方法棺弊,那些特有的方法交給數(shù)據(jù)庫(kù)驅(qū)動(dòng)去實(shí)現(xiàn)。
database/sql庫(kù)提供了一些type鱼喉。這些類型對(duì)掌握它的用法非常重要秀鞭。
****DB**** 數(shù)據(jù)庫(kù)對(duì)象。 sql.DB類型代表了數(shù)據(jù)庫(kù)扛禽。和其他語(yǔ)言不一樣锋边,它并是數(shù)據(jù)庫(kù)連接。golang中的連接來(lái)自內(nèi)部實(shí)現(xiàn)的連接池编曼,連接的建立是惰性的豆巨,當(dāng)你需要連接的時(shí)候,連接池會(huì)自動(dòng)幫你創(chuàng)建掐场。通常你不需要操作連接池往扔。一切都有g(shù)o來(lái)幫你完成。
****Results**** 結(jié)果集刻肄。數(shù)據(jù)庫(kù)查詢的時(shí)候瓤球,都會(huì)有結(jié)果集。sql.Rows類型表示查詢返回多行數(shù)據(jù)的結(jié)果集敏弃。sql.Row則表示單行查詢結(jié)果的結(jié)果集卦羡。當(dāng)然,對(duì)于插入更新和刪除麦到,返回的結(jié)果集類型為sql.Result绿饵。
****Statements**** 語(yǔ)句。sql.Stmt類型表示sql查詢語(yǔ)句瓶颠,例如DDL拟赊,DML等類似的sql語(yǔ)句〈饬埽可以把當(dāng)成prepare語(yǔ)句構(gòu)造查詢吸祟,也可以直接使用sql.DB的函數(shù)對(duì)其操作瑟慈。
warming up
下面就開(kāi)始我們的sql數(shù)據(jù)庫(kù)之旅,我們使用mysql數(shù)據(jù)庫(kù)為例子屋匕,驅(qū)動(dòng)使用go-sql-driver/mysql
葛碧。
對(duì)于其他語(yǔ)言,查詢數(shù)據(jù)的時(shí)候需要?jiǎng)?chuàng)建一個(gè)連接过吻,對(duì)于go而言則是需要?jiǎng)?chuàng)建一個(gè)數(shù)據(jù)庫(kù)抽象對(duì)象进泼。連接將會(huì)在查詢需要的時(shí)候,由連接池創(chuàng)建并維護(hù)纤虽。使用sql.Open函數(shù)創(chuàng)建數(shù)據(jù)庫(kù)對(duì)象乳绕。它的第一個(gè)參數(shù)是數(shù)據(jù)庫(kù)驅(qū)動(dòng)名,第二個(gè)參數(shù)是一個(gè)連接字串(符合DSN風(fēng)格逼纸,可以是一個(gè)tcp連接洋措,一個(gè)unix socket等)。
import (
"database/sql"
"log"
_ "github.com/go-sql-driver/mysql"
)
func main() {
db, err := sql.Open("mysql", "root:@tcp(127.0.0.1:3306)/test?parseTime=true")
if err != nil{
log.Fatal(err)
}
defer db.Close()
}
創(chuàng)建了數(shù)據(jù)庫(kù)對(duì)象之后樊展,在函數(shù)退出的時(shí)候呻纹,需要釋放連接,即調(diào)用sql.Close方法专缠。例子使用了defer語(yǔ)句設(shè)置釋放連接雷酪。
接下來(lái)進(jìn)行一些基本的數(shù)據(jù)庫(kù)操作,首先我們使用Exec方法執(zhí)行一條sql涝婉,創(chuàng)建一個(gè)數(shù)據(jù)表:
func main() {
db, err := sql.Open("mysql", "root:@tcp(127.0.0.1:3306)/test?parseTime=true")
if err != nil{
log.Fatal(err)
}
defer db.Close()
_, err = db.Exec("CREATE TABLE IF NOT EXISTS test.hello(world varchar(50))")
if err != nil{
log.Fatalln(err)
}
}
此時(shí)可以看見(jiàn)哥力,數(shù)據(jù)庫(kù)生成了一個(gè)新的表。接下來(lái)再插入一些數(shù)據(jù)墩弯。
func main() {
db, err := sql.Open("mysql", "root:@tcp(127.0.0.1:3306)/test?parseTime=true")
...
rs, err := db.Exec("INSERT INTO test.hello(world) VALUES ('hello world')")
if err != nil{
log.Fatalln(err)
}
rowCount, err := rs.RowsAffected()
if err != nil{
log.Fatalln(err)
}
log.Printf("inserted %d rows", rowCount)
}
同樣使用Exec方法即可插入數(shù)據(jù)吩跋,返回的結(jié)果集對(duì)象是是一個(gè)sql.Result類型,它有一個(gè)LastInsertId
方法渔工,返回插入數(shù)據(jù)后的id锌钮。當(dāng)然此例的數(shù)據(jù)表并沒(méi)有id字段,就返回一個(gè)0.
插入了一些數(shù)據(jù)引矩,接下來(lái)再簡(jiǎn)單的查詢一下數(shù)據(jù):
func main() {
db, err := sql.Open("mysql", "root:@tcp(127.0.0.1:3306)/test?parseTime=true")
...
rows, err := db.Query("SELECT world FROM test.hello")
if err != nil{
log.Fatalln(err)
}
for rows.Next(){
var s string
err = rows.Scan(&s)
if err !=nil{
log.Fatalln(err)
}
log.Printf("found row containing %q", s)
}
rows.Close()
}
我們使用了Query方法執(zhí)行select查詢語(yǔ)句梁丘,返回的是一個(gè)sql.Rows類型的結(jié)果集。迭代后者的Next方法旺韭,然后使用Scan方法給變量s賦值氛谜,以便取出結(jié)果。最后再把結(jié)果集關(guān)閉(釋放連接)区端。
通過(guò)上面一個(gè)簡(jiǎn)單的例子值漫,介紹了database/sql的基本數(shù)據(jù)查詢操作。而對(duì)于開(kāi)篇所說(shuō)的幾個(gè)結(jié)構(gòu)類型尚未進(jìn)行詳細(xì)的介紹织盼。下面我們?cè)籴槍?duì)database/sql庫(kù)的類型和數(shù)據(jù)庫(kù)交互做更深的探究杨何。
sql.DB
正如上文所言酱塔,sql.DB是數(shù)據(jù)庫(kù)的抽象,雖然通常它容易被誤以為是數(shù)據(jù)庫(kù)連接晚吞。它提供了一些跟數(shù)據(jù)庫(kù)交互的函數(shù)延旧,同時(shí)管理維護(hù)一個(gè)數(shù)據(jù)庫(kù)連接池谋国,幫你處理了單調(diào)而重復(fù)的管理工作槽地,并且在多個(gè)goroutines也是十分安全。
sql.DB表示是數(shù)據(jù)庫(kù)抽象芦瘾,因此你有幾個(gè)數(shù)據(jù)庫(kù)就需要為每一個(gè)數(shù)據(jù)庫(kù)創(chuàng)建一個(gè)sql.DB對(duì)象捌蚊。因?yàn)樗S護(hù)了一個(gè)連接池,因此不需要頻繁的創(chuàng)建和銷毀近弟。它需要長(zhǎng)時(shí)間保持缅糟,因此最好是設(shè)置成一個(gè)全局變量以便其他代碼可以訪問(wèn)。
創(chuàng)建數(shù)據(jù)庫(kù)對(duì)象需要引入標(biāo)準(zhǔn)庫(kù)database/sql祷愉,同時(shí)還需要引入驅(qū)動(dòng)go-sql-driver/mysql窗宦。使用_
表示引入驅(qū)動(dòng)的變量,這樣做的目的是為了在你的代碼中不至于和標(biāo)注庫(kù)的函數(shù)變量namespace沖突二鳄。
連接池
只用sql.Open函數(shù)創(chuàng)建連接池赴涵,可是此時(shí)只是初始化了連接池,并沒(méi)有創(chuàng)建任何連接订讼。連接創(chuàng)建都是惰性的髓窜,只有當(dāng)你真正使用到連接的時(shí)候,連接池才會(huì)創(chuàng)建連接欺殿。連接池很重要寄纵,它直接影響著你的程序行為。
連接池的工作原來(lái)卻相當(dāng)簡(jiǎn)單脖苏。當(dāng)你的函數(shù)(例如Exec程拭,Query)調(diào)用需要訪問(wèn)底層數(shù)據(jù)庫(kù)的時(shí)候,函數(shù)首先會(huì)向連接池請(qǐng)求一個(gè)連接棍潘。如果連接池有空閑的連接恃鞋,則返回給函數(shù)。否則連接池將會(huì)創(chuàng)建一個(gè)新的連接給函數(shù)蜒谤。一旦連接給了函數(shù)山宾,連接則歸屬于函數(shù)。函數(shù)執(zhí)行完畢后鳍徽,要不把連接所屬權(quán)歸還給連接池资锰,要么傳遞給下一個(gè)需要連接的(Rows)對(duì)象,最后使用完連接的對(duì)象也會(huì)把連接釋放回到連接池阶祭。
請(qǐng)求一個(gè)連接的函數(shù)有好幾種绷杜,執(zhí)行完畢處理連接的方式稍有差別直秆,大致如下:
- db.Ping() 調(diào)用完畢后會(huì)馬上把連接返回給連接池。
- db.Exec() 調(diào)用完畢后會(huì)馬上把連接返回給連接池鞭盟,但是它返回的Result對(duì)象還保留這連接的引用圾结,當(dāng)后面的代碼需要處理結(jié)果集的時(shí)候連接將會(huì)被重用。
- db.Query() 調(diào)用完畢后會(huì)將連接傳遞給sql.Rows類型齿诉,當(dāng)然后者迭代完畢或者顯示的調(diào)用.Clonse()方法后筝野,連接將會(huì)被釋放回到連接池。
- db.QueryRow()調(diào)用完畢后會(huì)將連接傳遞給sql.Row類型粤剧,當(dāng).Scan()方法調(diào)用之后把連接釋放回到連接池歇竟。
- db.Begin() 調(diào)用完畢后將連接傳遞給sql.Tx類型對(duì)象,當(dāng).Commit()或.Rollback()方法調(diào)用后釋放連接抵恋。
因?yàn)槊恳粋€(gè)連接都是惰性創(chuàng)建的焕议,如何驗(yàn)證sql.Open調(diào)用之后,sql.DB對(duì)象可用呢弧关?通常使用db.Ping()方法初始化:
db, err := sql.Open("driverName", "dataSourceName")
if err != nil{
log.Fatalln(err)
}
defer db.Close()
err = db.Ping()
if err != nil{
log.Fatalln(err)
}
調(diào)用了Ping之后盅安,連接池一定會(huì)初始化一個(gè)數(shù)據(jù)庫(kù)連接。當(dāng)然世囊,實(shí)際上對(duì)于失敗的處理别瞭,應(yīng)該定義一個(gè)符合自己需要的方式,現(xiàn)在為了演示茸习,簡(jiǎn)單的使用log.Fatalln(err)
表示了畜隶。
連接失敗
關(guān)于連接池另外一個(gè)知識(shí)點(diǎn)就是你不必檢查或者嘗試處理連接失敗的情況。當(dāng)你進(jìn)行數(shù)據(jù)庫(kù)操作的時(shí)候号胚,如果連接失敗了籽慢,database/sql會(huì)幫你處理。實(shí)際上猫胁,當(dāng)從連接池取出的連接斷開(kāi)的時(shí)候箱亿,database/sql會(huì)自動(dòng)嘗試重連10次。仍然無(wú)法重連的情況下會(huì)自動(dòng)從連接池再獲取一個(gè)或者新建另外一個(gè)弃秆。好比去買雞蛋届惋,售貨員會(huì)從箱子里掏出雞蛋,如果發(fā)現(xiàn)是壞蛋則連續(xù)掏10次菠赚,仍然找不到合適的就會(huì)換一個(gè)箱子招脑豹,或者從別的庫(kù)房再拿一個(gè)給你。
連接池配置
無(wú)論哪一個(gè)版本的go都不會(huì)提供很多控制連接池的接口衡查。知道1.2版本以后才有一些簡(jiǎn)單的配置瘩欺。可是1.2版本的連接池有一個(gè)bug,請(qǐng)升級(jí)更高的版本俱饿。
配置連接池有兩個(gè)的方法:
- db.SetMaxOpenConns(n int) 設(shè)置打開(kāi)數(shù)據(jù)庫(kù)的最大連接數(shù)歌粥。包含正在使用的連接和連接池的連接。如果你的函數(shù)調(diào)用需要申請(qǐng)一個(gè)連接拍埠,并且連接池已經(jīng)沒(méi)有了連接或者連接數(shù)達(dá)到了最大連接數(shù)失驶。此時(shí)的函數(shù)調(diào)用將會(huì)被block,直到有可用的連接才會(huì)返回。設(shè)置這個(gè)值可以避免并發(fā)太高導(dǎo)致連接mysql出現(xiàn)too many connections的錯(cuò)誤。該函數(shù)的默認(rèn)設(shè)置是0,表示無(wú)限制。
- db.SetMaxIdleConns(n int) 設(shè)置連接池中的保持連接的最大連接數(shù)范嘱。默認(rèn)也是0,表示連接池不會(huì)保持釋放會(huì)連接池中的連接的連接狀態(tài):即當(dāng)連接釋放回到連接池的時(shí)候若贮,連接將會(huì)被關(guān)閉奈应。這會(huì)導(dǎo)致連接再連接池中頻繁的關(guān)閉和創(chuàng)建。
對(duì)于連接池的使用依賴于你是如何配置連接池账磺,如果使用不當(dāng)會(huì)導(dǎo)致下面問(wèn)題:
- 大量的連接空閑芹敌,導(dǎo)致額外的工作和延遲。
- 連接數(shù)據(jù)庫(kù)的連接過(guò)多導(dǎo)致錯(cuò)誤垮抗。
- 連接阻塞氏捞。
- 連接池有超過(guò)十個(gè)或者更多的死連接,限制就是10次重連冒版。
大多數(shù)時(shí)候液茎,如何使用sql.DB對(duì)連接的影響大過(guò)連接池配置的影響。這些具體問(wèn)題我們會(huì)再使用sql.DB的時(shí)候逐一介紹辞嗡。
掌握了database/sql關(guān)于數(shù)據(jù)庫(kù)連接池管理內(nèi)容捆等,下一步則是使用這些連接,進(jìn)行數(shù)據(jù)的交互操作啦续室。