這邊文章主要基於Illustrated guide to SQLX翻譯而成。
sqlx是一個go語言包,在內置database/sql包之上增加了很多擴展,簡化數據庫操作代碼的書寫。
資源
如果對於go語言的sql用法不熟悉,可以到下面網站學習:
database/sql documentation
go-database-sql tutorial
如果對於golang語言不熟悉,可以到下面網站學習:
The Go tour
How to write Go code
Effective Go
CSDN
由於database/sql接口是sqlx的子集,當前文檔中所有關於database/sql的用法同樣用於sqlx
開始
安裝sqlx 驅動
$ go get github.com/jmoiron/sqlx
本文訪問sqlite數據庫
$ go get github.com/mattn/go-sqlite3
Handle Types
sqlx設計和database/sql使用方法是一樣的。包含有4中主要的handle types:
- sqlx.DB - 和sql.DB相似,表示數據庫。
- sqlx.Tx - 和sql.Tx相似,表示transacion。
- sqlx.Stmt - 和sql.Stmt相似,表示prepared statement。
- sqlx.NamedStmt - 表示prepared statement(支持named parameters)
所有的handler types都提供了對database/sql的兼容,意味着當你調用sqlx.DB.Query時,可以直接替換為sql.DB.Query.這就使得sqlx可以很容易的加入到已有的數據庫項目中。
此外,sqlx還有兩個cursor類型:
- sqlx.Rows - 和sql.Rows類似,Queryx返回。
- sqlx.Row - 和sql.Row類似,QueryRowx返回。
連級到數據庫
一個DB實例並不是一個鏈接,但是抽象表示了一個數據庫。這就是為什么創建一個DB時並不會返回錯誤和panic。它內部維護了一個連接池,當需要進行連接的時候嘗試連接。你可以通過Open創建一個sqlx.DB或通過NewDb從已存在的sql.DB中創建一個新的sqlx.DB
var db *sqlx.DB // exactly the same as the built-in db = sqlx.Open("sqlite3", ":memory:") // from a pre-existing sql.DB; note the required driverName db = sqlx.NewDb(sql.Open("sqlite3", ":memory:"), "sqlite3") // force a connection and test that it worked err = db.Ping()
在一些環境下,你可能需要同時打開一個DB並鏈接。可以調用connect,這個函數打開一個新的DB並嘗試Ping。MustConnect函數在鏈接出錯時會panic。
var err error // open and connect at the same time: db, err = sqlx.Connect("sqlite3", ":memory:") // open and connect at the same time, panicing on error db = sqlx.MustConnect("sqlite3", ":memory:")
Querying 101 sqlx中的handle types實現了數據庫查詢相同的基本的操作語法。 - Exec(…) (sql.Result, error) - 和 database/sql相比沒有改變 - Query(…) (*sql.Rows, error) - 和 database/sql相比沒有改變 - QueryRow(…) *sql.Row - 和 database/sql相比沒有改變 對內置語法的擴展 - MustExec() sql.Result – Exec, but panic on error - Queryx(…) (*sqlx.Rows, error) - Query, but return an sqlx.Rows - QueryRowx(…) *sqlx.Row – QueryRow, but return an sqlx.Row 還有下面新的語法 Get(dest interface{}, …) error Select(dest interface{}, …) error 下面會詳細介紹這些方法的使用 Exec Exec和MustExec從連接池中獲取一個連接然后只想對應的query操作。對於不支持ad-hoc query execution的驅動,在操作執行的背后會創建一個prepared statement。在結果返回前這個connection會返回到連接池中。
schema := `CREATE TABLE place ( country text, city text NULL, telcode integer);` // execute a query on the server result, err := db.Exec(schema) // or, you can use MustExec, which panics on error cityState := `INSERT INTO place (country, telcode) VALUES (?, ?)` countryCity := `INSERT INTO place (country, city, telcode) VALUES (?, ?, ?)` db.MustExec(cityState, "Hong Kong", 852) db.MustExec(cityState, "Singapore", 65) db.MustExec(countryCity, "South Africa", "Johannesburg", 27)
上面代碼中 result有兩個可能的數據LastInsertId() or RowsAffected(),依賴不同的驅動。 Mysql中,在含有auto-increment key的表中執行插入操作會得到LastInsertId(),在PostgreSQL中這個信息只有在使用RETURNING語句的row cursor中才會返回。 bindvars 代碼中?占位符,稱為bindvars,非常重要,你可以總是使用它們來向數據庫發送數據,可以用來組織SQL Injection攻擊。 database/sql並不會對查詢語句進行任何的校驗,傳入什么就發送到server是什么。 除非driver實現特定的接口,query在數據庫執行之前會准備好。不同的數據庫的bindvars不一樣。 - MySQL 使用? - PostgreSQL 使用1,1,2等等 - SQLite 使用?或$1 - Oracle 使用:name 其他數據庫可能還不一樣。你可以使用sqlx.DB.Rebind(string) string函數利用?語法來得到一個適合在當前數據庫上執行的query語句。 關於bindvars常見的誤解是他們用於插值。他們只用於參數化,不允許改變sql語句的合法接口。例如,下面的用法是會報錯的
// doesn't work db.Query("SELECT * FROM ?", "mytable") // also doesn't work db.Query("SELECT ?, ? FROM people", "name", "location")
Query
Query是database/sql中執行查詢主要使用的方法,該方法返回row結果。Query返回一個sql.Rows對象和一個error對象。
// fetch all places from the db rows, err := db.Query("SELECT country, city, telcode FROM place") // iterate over each row for rows.Next() { var country string // note that city can be NULL, so we use the NullString type var city sql.NullString var telcode int err = rows.Scan(&country, &city, &telcode) }
在使用的時候應該吧Rows當成一個游標而不是一系列的結果。盡管數據庫驅動緩存的方法不一樣,通過Next()迭代每次獲取一列結果,對於查詢結果非常巨大的情況下,可以有效的限制內存的使用,Scan()利用reflect把sql每一列結果映射到go語言的數據類型如string,[]byte等。如果你沒有遍歷完全部的rows結果,一定要記得在把connection返回到連接池之前調用rows.Close()。
Query返回的error有可能是在server准備查詢的時候發生的,也有可能是在執行查詢語句的時候發生的。例如可能從連接池中獲取一個壞的連級(盡管數據庫會嘗試10次去發現或創建一個工作連接)。一般來說,錯誤主要由錯誤的sql語句,錯誤的類似匹配,錯誤的域名或表名等。
在大部分情況下,Rows.Scan()會把從驅動獲取的數據進行拷貝,無論驅動如何使用緩存。特殊類型sql.RawBytes可以用來從驅動返回的數據總獲取一個zero-copy的slice byte。當下一次調用Next的時候,這個值就不在有效了,因為它指向的內存已經被驅動重寫了別的數據。
Query使用的connection在所有的rows通過Next()遍歷完后或者調用rows.Close()后釋放。
Queryx和Query行為很相似,不過返回一個sqlx.Rows對象,支持擴展的scan行為。
type Place struct { Country string City sql.NullString TelephoneCode int `db:"telcode"` } rows, err := db.Queryx("SELECT * FROM place") for rows.Next() { var p Place err = rows.StructScan(&p) }
sqlx.Rowx的主要擴展就是StructScan,可以自動把查下結果掃描到對應結構體中的域(fileld)中。注意結構體中域(field)必須是可導出(exported)的,這樣sqlx才能夠寫入值到結構體中。
正如在上面代碼中所示,可以利用db結構體標簽來指定結構體field映射到數據庫中特定的列名,或者用db.MapperFunc()來指定默認的映射。db默認對結構體的filed名執行strings.Lower后,和數據庫的列名進行匹配。關於StructScan,SliceScan,MapScan更詳細的內容請參見后面章節advanced scanning
QueryRow
QueryRow從數據庫server中獲取一列數據。它從連接池中獲取一個連級,然后執行Query,返回一個Row對象,這個對象有一個自己的內部的Rows對象。
row := db.QueryRow("SELECT * FROM place WHERE telcode=?", 852) var telcode int err = row.Scan(&telcode)
不像Query,QueryRow只返回一個Row類型,並不返回error,如果在執行查詢過程中出錯,則錯誤通過Scan返回,如果查詢結果為空,則返回sql.ErrNoRows。如果Scan本身出錯,error同樣由scan返回。
QueryRow使用的connection當result返回的時候就關閉了,也就意味着使用QueryRow的時候不能夠使用sql.RawByes,因為driver使用sql.RawBytes引用內存,在connection回收后可能也會無效。
QueryRowx返回一個sqlx.Row而不是sql.Row,它實現了跟Rows相同的scan方法如上,同時還有高級的scan方法如下:(更高級的scan方法advanced scanning section)
var p Place err := db.QueryRowx("SELECT city, telcode FROM place LIMIT 1").StructScan(&p)
Get and Select
Get和Select是一個非常省時的擴展。它們把query和非常靈活的scan語法結合起來。為了更加清晰的介紹它們,我們先討論下什么是scannalbe:
- a value is scannable if it is not a struct, eg string, int
- a value is scannable if it implements sql.Scanner
- a value is scannable if it is a struct with no exported fields (eg. time.Time)
Get和Select對scannable的類型使用rows.scan,對non-scannable的類型使用rows.StructScan。Get用來獲取單個結果然后Scan,Select用來獲取結果切片。
p := Place{} pp := []Place{} // this will pull the first place directly into p err = db.Get(&p, "SELECT * FROM place LIMIT 1") // this will pull places with telcode > 50 into the slice pp err = db.Select(&pp, "SELECT * FROM place WHERE telcode > ?", 50) // they work with regular types as well var id int err = db.Get(&id, "SELECT count(*) FROM place") // fetch at most 10 place names var names []string err = db.Select(&names, "SELECT name FROM place LIMIT 10")
Get和Select在執行完查詢后就會關閉Rows,並且在執行階段遇到任何問題都會返回錯誤。由於它們內部使用的StructScan,所以 下文中advanced scanning section講的特征也適用與Get和Select。
Select可以提高編碼小路,但是要注意Select和Queryx是有很大不同的,因為Select會把整個結果一次放入內存。如果查詢結果沒有限制特定的大小,那么最好使用Query/StructScan迭代方法。
Transactions
為了使用transactions,必須使用DB.Begin()來創建,下面的代碼是錯誤的:
db.MustExec("BEGIN;") db.MustExec(...) db.MustExec("COMMIT;")
Exec和其他查詢語句會向DB請求一個connection,執行完后就返回到連接池中,並不能保證每次獲取的connection就是BEGIN執行時使用的那個,所以正確的做法要使用DB.Begin:
tx, err := db.Begin() err = tx.Exec(...) err = tx.Commit()
DB除了Begin之外,還可以使用擴展Beginx()和MustBegin(),返回sqlx.Tx:
tx := db.MustBegin() tx.MustExec(...) err = tx.Commit()
sqlx.Tx擁有sqlx.DB擁有的所有的handle extensions.
由於transaction是一個connection狀態,所以Tx對象必須綁定和控制單個connection。一個Tx會在整個生命周期中保存一個connection,然后在調用commit或Rollback()的時候釋放掉。你在調用這幾個函數的時候必須十分小心,否則connection會一直被占用直到被垃圾回收。
由於在一個transaction中只能有一個connection,所以每次只能執行一條語句。在執行另外的query操作之前,cursor對象Row*和Rows必須被Scanned或Closed。如果在數據庫給你返回數據的時候你嘗試向數據庫發送數據,這個操作可能會中斷connection。
最后,Tx對象僅僅執行了一個BEGIN語句和綁定了一個connection,它其實並沒有在server上執行任何操作。而transaction真實的行為包含locking和isolation,在不同數據庫上實現是不同的。
Prepared Statements
對於大部分的數據庫來說,當一個query執行的時候,在數據庫內部statements其實已經准備好了。然后你可以通過sqlx.DB.Prepare()准備statements,便於后面在別的地方使用。
stmt, err := db.Prepare(`SELECT * FROM place WHERE telcode=?`) row = stmt.QueryRow(65) tx, err := db.Begin() txStmt, err := tx.Prepare(`SELECT * FROM place WHERE telcode=?`) row = txStmt.QueryRow(852)
Prepare實際上在數據庫上執行preparation操作,所以它需要一個connection和它的connection state。
database/sql把這部分進行了抽象,自動在新的connection上創建statement,這樣開發者就能通過stmt對象在多個connection上並發執行操作。
Preparex()返回一個sqlx.Stmt對象,包含sqlx.DB和sqlx.Tx所有的handle 擴展(方法)。
sql.Tx對象含有一個Stmt()方法,從已存在的statement中返回一個特定於改transaction的statement。
sqlx.Tx同樣含有一個Stmtx()方法,從已有的sql.Stmt或sqlx.Stmt中創建一個特定於transaction的sqlx.Stmt。
Query Helpers
“In” Queries
由於database/sql並不會分析你的查詢語句然后直接把參數傳遞給driver,這樣對於IN
轉載自https://studygolang.com/articles/14590