sqlx: 功能強大的數據庫訪問庫
sqlx[1] 是一個用於擴展標準庫 database/sql 的庫,它提供了一些額外的功能,使得在 Go 中使用 sql 更加方便。sqlx 的目標是保持 database/sql 的簡單性,同時提供更多的功能。
sqlx
爲 Go 的標準 database/sql
庫提供了一組擴展。sqlx 中的 sql.Conn
、sql.DB
、sql.TX
、sql.Stmt
、sql.Rows
、sql.Row
等版本都保留了底層接口不變,因此它們的接口是標準接口的超集。這使得將使用 database/sql
的現有代碼庫與 sqlx
集成相對容易。
主要的額外概念有:
-
將行映射到結構體(支持嵌入結構體)、Map 和切片
-
支持命名參數,包括預編譯語句
-
使用
Get
和Select
快速從查詢到結構體 / 切片
sqlx
的目的是無縫地封裝 database/sql,並提供在開發數據庫驅動的應用程序時有用的便捷方法。它不會改變任何底層的 database/sql
方法。相反,所有擴展行爲都是通過在包裝類型上定義的新方法來實現的。 比如:
type Conn struct {
*sql.Conn
driverName string
unsafe bool
Mapper *reflectx.Mapper
}
type DB struct {
*sql.DB
driverName string
unsafe bool
Mapper *reflectx.Mapper
}
type Stmt struct {
*sql.Stmt
unsafe bool
Mapper *reflectx.Mapper
}
type Tx struct {
*sql.Tx
driverName string
unsafe bool
Mapper *reflectx.Mapper
}
type Rows struct {
*sql.Rows // 嵌入
unsafe bool
Mapper *reflectx.Mapper
// these fields cache memory use for a rows during iteration w/ structScan
started bool
fields [][]int
values []interface{}
}
可以看到,它的核心類型都是對標準庫的封裝,然後在此基礎上提供了更多的功能。
它是 2013 年發佈,已經有 11 年的歷史了,也許爲了保持兼容,它沒有對泛型提供支持,甚至interface{}
也沒有改爲any
,還支持 Go 1.10 的版本。
本文假定你已經有了 Go 開發數據庫程序的基礎。如果你還不瞭解,建議你閱讀下面的材料:
-
database/sql documentation[2]
-
go-database-sql tutorial[3]
本文是編譯自作者寫的 sqlx 圖解指南 [4]。
引入sqlx
庫以及 sqlite3 驅動:
$ go get github.com/jmoiron/sqlx
$ go get github.com/mattn/go-sqlite3
sqlx
的設計初衷是讓用戶感覺與 database/sql
一樣。它主要有 5 種 handler 類型:
-
sqlx.Conn
- 類似於sql.Conn
,表示一個數據庫連接 -
sqlx.DB
- 類似於sql.DB
,表示一個數據庫連接池 -
sqlx.Tx
- 類似於sql.Tx
,表示一個事務 -
sqlx.Stmt
- 類似於sql.Stmt
,表示一個預處理語句 -
sqlx.NamedStmt
- 表示一個支持命名參數的預處理語句
這些 handler 類型都嵌入了它們的 database/sql
對應類型,這意味着當你調用 sqlx.DB.Query
時,你實際上調用的是與 sql.DB.Query
相同的代碼。這使得它易於引入現有的代碼庫中。
除了這些,還有 2 種遊標類型:
-
sqlx.Rows
- 類似於sql.Rows
,是從Queryx
返回的遊標,多行結果 -
sqlx.Row
- 類似於sql.Row
,是從QueryRowx
返回的結果, 單行結果
與 handler 類型一樣,sqlx.Rows
嵌入了 sql.Rows
。由於無法訪問底層實現,sqlx.Row
是對 sql.Row
的部分重新實現,同時保留了標準接口。
連接數據庫
一個 DB
實例並不是連接,而是一個表示數據庫的抽象。這就是爲什麼創建 DB
時不會返回錯誤也不會引發恐慌(panic
)。它在內部維護了一個連接池,並會在首次需要連接時嘗試連接。你可以通過 Open
方法創建一個 sqlx.DB
,或者通過 NewDb
方法從現有的 sql.DB
創建一個新的sqlx.DB
handler :
var db *sqlx.DB
// 完全與內置的一樣
db = sqlx.Open("sqlite3", ":memory:")
// 從現有的sql.DB創建一個新的sqlx.DB
db = sqlx.NewDb(sql.Open("sqlite3", ":memory:"), "sqlite3")
// 強制連接並測試是否成功
err = db.Ping()
在某些情況下,你可能希望同時打開數據庫並建立連接,例如,在初始化階段捕獲配置問題。你可以使用 Connect
方法一次性完成這個操作,它會打開一個新的數據庫並嘗試進行 Ping
操作。MustConnect
變種在遇到錯誤時會觸發 panic
,適合在你的包的模塊級別使用:
var err error
// 打開並連接數據庫
db, err = sqlx.Connect("sqlite3", ":memory:")
// 打開並連接數據庫,遇到錯誤時觸發panic
db = sqlx.MustConnect("sqlite3", ":memory:")
基本查詢
sqlx
中的 handler 類型實現了與 database/sql
相同的基本動詞來查詢你的數據庫:
-
Exec(...) (sql.Result, error)
- 與database/sql
中的方法沒有變化 -
Query(...) (*sql.Rows, error)
- 與database/sql
中的方法沒有變化 -
QueryRow(...) *sql.Row
- 與database/sql
中的方法沒有變化
以下是內置方法的擴展:
-
MustExec() sql.Result
-- 執行Exec
,但遇到錯誤時會觸發panic
-
Queryx(...) (*sqlx.Rows, error)
- 執行Query
,但返回一個sqlx.Rows
-
QueryRowx(...) *sqlx.Row
-- 執行QueryRow
,但返回一個sqlx.Row
還有以下新的語義:
-
Get(dest interface{}, ...) error
-
Select(dest interface{}, ...) error
現在,我們從未改變的接口開始,一直介紹到新的語義,並解釋它們的使用方法。
執行 Exec
Exec
和 MustExec
從連接池中獲取一個連接,並在服務器上執行提供的語句。對於不支持即席(ad-hoc)查詢執行的驅動程序,可能會在幕後創建一個預處理語句來執行。在返回結果之前,連接會被返回到連接池中。
schema := `CREATE TABLE place (
country text,
city text NULL,
telcode integer);`
// 執行一個查詢
result, err := db.Exec(schema)
// 或者,你可以使用MustExec,在錯誤時會觸發panic
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()
或 RowsAffected()
,這些數據的可用性取決於驅動程序。例如,在 MySQL 中,如果插入的表有自增主鍵,則 LastInsertId()
將可用,但在 PostgreSQL 中,這些信息只能通過使用 RETURNING
子句從普通行遊標中檢索。
綁定變量 bindvars
內部稱爲綁定變量的 ?
查詢佔位符非常重要;您應該始終使用這些佔位符向數據庫發送值,因爲它們可以防止 SQL 注入攻擊。database/sql
不會對查詢文本進行任何驗證;它會原樣發送到服務器,同時發送編碼後的參數。除非驅動程序實現了特殊接口,否則查詢會在執行之前先在服務器上準備。因此,綁定變量是特定於數據庫的:
-
MySQL 使用上面展示的
?
變體 -
PostgreSQL 使用枚舉的
$1
,$2
等綁定變量語法 -
SQLite 接受
?
和$1
語法 -
Oracle 使用
:name
語法
其他數據庫可能有所不同。您可以使用 sqlx.DB.Rebind(string) string
函數和 ?
綁定變量語法來獲取適合在當前數據庫類型上執行的查詢。 關於綁定變量的一個常見誤解是它們用於插值。它們僅用於參數化,並且不允許 [5] 更改 SQL 語句的結構。例如,使用綁定變量來嘗試參數化列名或表名將無法工作:
// 無法工作
db.Query("SELECT * FROM ?", "mytable")
// 也無法工作
db.Query("SELECT ?, ? FROM people", "name", "location")
查詢 Query
Query
是使用 database/sql
執行查詢並返回行結果的主要方法。Query
返回一個 sql.Rows
對象和一個錯誤:
// 從數據庫獲取所有地點
rows, err := db.Query("SELECT country, city, telcode FROM place")
// 遍歷每一行
for rows.Next() {
var country string
// 注意city可能爲NULL,所以我們使用NullString類型
var city sql.NullString
var telcode int
err = rows.Scan(&country, &city, &telcode)
}
// 檢查錯誤
err = rows.Err()
你應該把 Rows
當作數據庫遊標來處理,而不是一個具體化的結果列表。儘管驅動程序緩衝行爲可能有所不同,但通過 Next()
進行迭代是限制大型結果集內存使用量的好方法,因爲你一次只掃描一行。Scan()
使用反射將 SQL 列返回類型映射到 Go 類型,如 string
、[]byte
等。如果你沒有遍歷完整個結果集,請確保調用 rows.Close()
將連接返回給連接池!
Query
返回的錯誤是可能在服務器準備或執行期間發生的任何錯誤。這可能包括從連接池中獲取了有問題的連接,儘管 database/sql
會重試 10 次 [6] 以嘗試找到或創建一個工作連接。一般來說,錯誤會由於錯誤的 SQL 語法、類型不匹配或不正確的字段和表名導致。
在大多數情況下,Rows.Scan
會複製它從驅動程序獲取的數據,因爲它不知道驅動程序如何重用其緩衝區。可以使用特殊類型 sql.RawBytes
來從驅動程序實際返回的數據中獲取零拷貝的字節切片。在下次調用 Next()
之後,這樣的值將不再有效,因爲驅動程序可能已經覆蓋了那段內存。
Query
使用的連接在通過 Next 迭代完所有行之前或調用 rows.Close()
之後一直保持活動狀態,之後該連接將被釋放。有關更多信息,請參閱關於連接池 [7] 的部分。
sqlx
擴展的 Queryx
行爲與 Query
完全一樣,但返回的是 sqlx.Rows
,它具有擴展的掃描行爲:
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.Rows
的主要擴展方法是 StructScan()
,它可以自動將查詢結果掃描到結構體的字段中。請注意,爲了讓 sqlx
能夠寫入這些字段,這些字段必須是導出的(即首字母大寫),這是 Go 中所有序列化器(marshaller
)的共同要求。你可以使用 db
結構標籤來指定哪個列名映射到結構體的哪個字段,或者使用 db.MapperFunc()
設置新的默認映射規則。默認行爲是使用 strings.ToLower
對字段名進行小寫轉換以匹配列名。有關 StructScan
、SliceScan
和 MapScan
的更多信息,請參閱高級掃描部分。
查詢單行 QueryRow
QueryRow
從服務器獲取一行數據。它從連接池中獲取一個連接,並使用 Query
執行查詢,返回一個 Row
對象,該對象具有自己的內部 Rows
對象:
row := db.QueryRow("SELECT * FROM place WHERE telcode=?", 852)
var telcode int
err = row.Scan(&telcode)
與 Query
不同,QueryRow
返回一個 Row
類型的結果而不返回錯誤,這使得可以安全地從返回結果中鏈式調用 Scan
方法。如果執行查詢時發生錯誤,該錯誤將由 Scan
返回。如果沒有行,Scan
會返回 sql.ErrNoRows
。如果掃描本身失敗(例如,由於類型不匹配),也會返回該錯誤。
Row
結果內部的 Rows
結構在 Scan
時會被關閉,這意味着 QueryRow
使用的連接會在結果被掃描之前一直保持打開狀態。這也意味着 sql.RawBytes
在這裏不可用,因爲引用的內存屬於驅動程序,在控制權返回給調用者時可能已經無效。
sqlx
擴展的 QueryRowx
將返回一個sqlx.Row
而不是 sql.Row
,它實現了與 Rows
相同的掃描擴展,上面已經說過,並在高級掃描部分有詳細解釋:
var p Place
err := db.QueryRowx("SELECT city, telcode FROM place LIMIT 1").StructScan(&p)
Get 和 Select
Get
和 Select
是針對 handler 類型的省時的擴展,它們將查詢執行與靈活的掃描語義結合起來。爲了清楚地解釋它們,我們需要談談什麼是可掃描的:
-
如果一個值不是結構體,比如字符串(
string
)、整數(int
),那麼它就是可掃描的。 -
如果一個值實現了
sql.Scanner
接口,那麼它就是可掃描的。 -
如果一個值是結構體,但沒有導出的字段(例如
time.Time
),那麼它也是可掃描的。
Get
和 Select
在可掃描類型上使用 rows.Scan
,在非可掃描類型上使用 rows.StructScan
。它們大致分別對應於 QueryRow
和 Query
,其中 Get
用於獲取單個結果並進行掃描,而 Select
用於獲取結果的切片:
p := Place{}
pp := []Place{}
//這將直接將第一個地點拉取到p中
err = db.Get(&p, "SELECT * FROM place LIMIT 1")
// 這將把telcode大於50的地點拉取到切片pp中
err = db.Select(&pp, "SELECT * FROM place WHERE telcode > ?", 50)
// 也可以使用普通類型
var id int
err = db.Get(&id, "SELECT count(*) FROM place")
// 獲取最多10個地點名稱
var names []string
err = db.Select(&names, "SELECT name FROM place LIMIT 10")
這兩個方法基本可以把我前一篇封裝的 helper 函數替代掉了。
Get
和 Select
都會在查詢執行過程中關閉它們創建的 Rows
,並返回在此過程中任何步驟遇到的錯誤。由於它們內部使用 StructScan
,因此高級掃描部分中的細節也適用於 Get
和 Select
。
Select
可以爲您節省大量輸入,但要小心!它在語義上與 Queryx
不同,因爲它會一次性將整個結果集加載到內存中。如果查詢沒有將結果集限制在合理的大小,那麼最好使用經典的 Queryx/StructScan
迭代方式。
試想你要處理幾千萬行的數據,一條一條的拉取和處理,比一次性讀入到內存中處理,資源使用更友好。
事務 Transaction
要使用事務,您必須使用 DB.Begin()
創建一個事務 handler 。像這樣的代碼將不會工作:
// 這將不會工作,如果連接池>1
db.MustExec("BEGIN;")
db.MustExec(...)
db.MustExec("COMMIT;")
請記住,Exec
和其他所有查詢動詞每次都會向數據庫請求一個連接,並在使用後將其返回給連接池。因此,無法保證您會收到執行 BEGIN
語句時使用的同一個連接。要使用事務,您必須使用DB.Begin()
。
tx, err := db.Begin()
err = tx.Exec(...)
err = tx.Commit()
DB
handler 還有 Beginx()
和 MustBegin()
擴展方法,它們返回一個 sqlx.Tx
而不是 sql.Tx
:
tx := db.MustBegin()
tx.MustExec(...)
err = tx.Commit()
sqlx.Tx
擁有 sqlx.DB
的所有 handler 擴展。
由於事務是連接狀態,Tx
對象必須從連接池中綁定並控制一個單一的連接。在整個生命週期中,Tx
將維持這個單一的連接,只有在調用 Commit()
或 Rollback()
時纔會釋放它。你應該至少調用這兩個函數之一,否則連接將一直被佔用,直到垃圾收集器回收。
因爲在一個事務中你只能使用一個連接,所以你一次只能執行一個語句;在執行另一個查詢之前,必須分別掃描或關閉 Row
和 Rows
類型的遊標。如果你嘗試在服務器向你發送結果時向服務器發送數據,它可能會破壞連接。
最後,Tx
對象並不實際上在服務器上執行任何行爲;它們只是執行一個 BEGIN
語句並綁定一個單一的連接。事務的實際行爲,包括鎖定和隔離等,完全是未指定的,並且依賴於數據庫。
預編譯語句 Prepared Statement
在大多數數據庫中,當執行查詢時,實際上會在幕後準備語句。但是,您也可以使用 sqlx.DB.Prepare()
明確地準備語句以便在其他地方重用。
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
實際上是在數據庫上執行準備操作的,因此它需要一個連接和連接狀態。database/sql
爲你抽象了這些,允許你通過在新連接上自動創建語句,從單個 Stmt
對象在多個連接上併發執行。Preparex()
返回一個 sqlx.Stmt
,它擁有 sqlx.DB
和 sqlx.Tx
的所有 handler 擴展功能:
stmt, err := db.Preparex(`SELECT * FROM place WHERE telcode=?`)
var p Place
err = stmt.Get(&p, 852)
標準的 sql.Tx
對象還有一個 Stmt()
方法,該方法可以從預先存在的語句中返回一個特定於事務的語句。sqlx.Tx
有一個 Stmtx
版本,可以從現有的 sql.Stmt
或 sqlx.Stmt
創建一個新的特定於事務的 sqlx.Stmt
。
查詢輔助方法 Query Helper
database/sql
包不會對您的實際查詢文本進行任何處理。這使得在您的代碼中使用特定於後端的特性變得輕而易舉;您可以像在數據庫提示符中一樣編寫查詢。雖然這非常靈活,但它使得編寫某些類型的查詢變得困難。
"In" 子句
由於 database/sql
不會檢查您的查詢,而是直接將參數傳遞給驅動程序,因此處理帶有 IN
子句的查詢會變得困難:
SELECT * FROM users WHERE level IN (?);
當在後端將其準備爲語句時,綁定變量 ? 只會對應一個參數,但通常我們希望它根據某個切片的長度來對應可變數量的參數,例如:
var levels = []int{4, 6, 7}
rows, err := db.Query("SELECT * FROM users WHERE level IN (?);", levels)
通過使用 sqlx.In 預先處理查詢語句,可以實現這種模式:
var levels = []int{4, 6, 7}
query, args, err := sqlx.In("SELECT * FROM users WHERE level IN (?);", levels)
// sqlx.In返回帶有`?`綁定變量的查詢,我們可以重新綁定它以適應我們的後端
query = db.Rebind(query)
rows, err := db.Query(query, args...)
使用 sqlx.In
預先處理查詢語句可以實現這種模式:sqlx.In
會擴展傳遞給它的查詢中的任何綁定變量(bindvars
),這些綁定變量對應於參數中的切片,並擴展到與切片長度相同數量的佔位符,然後將這些切片元素追加到一個新的參數列表中。它僅對 ?
綁定變量執行此操作;您可以使用 db.Rebind
來獲取適合您後端的查詢語句。
命名查詢 Named Query
命名查詢在許多其他數據庫包中都很常見。它們允許您使用綁定變量語法,該語法通過結構體字段的名稱或映射鍵來綁定查詢中的變量,而不是按位置引用所有內容。結構體字段的命名約定遵循 StructScan
的規則,使用 NameMapper
和 db
結構體標籤。與命名查詢相關的有兩個額外的查詢動詞:
-
NamedQuery(...) (*sqlx.Rows, error)
- 類似於 Queryx,但使用命名綁定變量 -
NamedExec(...) (sql.Result, error)
- 類似於 Exec,但使用命名綁定變量
還有一個額外的 handler 類型:
NamedStmt
- 一個sqlx.Stmt
,可以使用命名綁定變量進行準備
// 使用結構體的命名查詢
p := Place{Country: "South Africa"}
rows, err := db.NamedQuery(`SELECT * FROM place WHERE country=:country`, p)
// 使用map的命名查詢
m := map[string]interface{}{"city": "Johannesburg"}
result, err := db.NamedExec(`SELECT * FROM place WHERE city=:city`, m)
命名查詢的執行和準備適用於結構體和 Map。如果你想要完整的查詢操作集,可以準備一個命名語句並使用它:
p := Place{TelephoneCode: 50}
pp := []Place{}
// 查詢所有telcode大於50的地點
nstmt, err := db.PrepareNamed(`SELECT * FROM place WHERE telcode > :telcode`)
err = nstmt.Select(&pp, p)
命名查詢支持是通過解析查詢中的 :param
語法,並將其替換爲底層數據庫支持的綁定變量來實現的,然後在執行時執行映射,因此它可以在 sqlx
支持的任何數據庫上使用。你還可以使用 sqlx.Named
,它使用 ?
綁定變量,並且可以與 sqlx.In
組合使用:
arg := map[string]interface{}{
"published": true,
"authors": []{8, 19, 32, 44},
}
query, args, err := sqlx.Named("SELECT * FROM articles WHERE published=:published AND author_id IN (:authors)", arg)
query, args, err := sqlx.In(query, args...)
query = db.Rebind(query)
db.Query(query, args...)
高級掃描 Advanced Scanning
StructScan
相當複雜但具有欺騙性。它支持嵌入的結構體,並使用與 Go 用於嵌入屬性和方法訪問相同的優先級規則爲字段賦值。這種用法的一個常見例子是在多個表之間共享表模型的公共部分,例如:
type AutoIncr struct {
ID uint64
Created time.Time
}
type Place struct {
Address string
AutoIncr
}
type Person struct {
Name string
AutoIncr
}
使用上述結構體,Person
和 Place
都能夠從 StructScan
中接收 id
和 created
列,因爲它們都嵌入了定義了這些列的 AutoIncr
結構體。這個功能可以讓你快速地爲連接操作創建一個臨時的表。它還可以遞歸地工作;以下結構體可以通過 Go 的點運算符和 StructScan
訪問 Person
的 Name
字段、AutoIncr
的 ID
和 Created
字段:
type Employee struct {
BossID uint64
EmployeeID uint64
Person
}
請注意,sqlx
歷史上一度支持此功能用於非嵌入結構體,但這最終變得令人困惑,因爲用戶使用此功能來定義關係,並兩次嵌入相同的結構體:
type Child struct {
Father Person
Mother Person
}
這會引起一些問題。在 Go 語言中,隱藏後代字段是合法的;如果嵌入示例中的 Employee
定義了一個 Name
字段,那麼它會優先於 Person
的 Name
字段。但是,模糊的選擇器是非法的,並且會導致運行時錯誤。如果我們想爲 Person
和 Place
創建一個快速的 JOIN
類型,那麼我們應該在哪裏放置 id
列,這兩個類型都通過嵌入的 AutoIncr
定義了 id
列?是否會出現錯誤?
由於 sqlx
構建字段名到字段地址映射的方式,在將結果掃描到結構體時,它不再知道在遍歷結構體樹時是否遇到過兩次相同的字段名。因此,與 Go 語言不同,StructScan
會選擇遇到的第一個具有該名稱的字段。由於 Go 語言的結構體字段是從上到下排序的,而 sqlx
爲了保持優先級規則,採用廣度優先遍歷,因此會選擇最淺、最頂部的定義。例如,在以下類型中:
type PersonPlace struct {
Person
Place
}
StructScan
會將 id
列的結果設置在 Person.AutoIncr.ID
中,也可以通過 Person.ID
訪問。爲了避免混淆,建議你在 SQL
中使用 AS
來創建列別名。
安全掃描目的字段
默認情況下,如果某一列無法映射到目標結構體中的字段,StructScan
將返回一個錯誤。這模仿了 Go 中對未使用變量的處理方式,但與標準庫中的序列化器(如 encoding/json
)的行爲不符。由於 SQL 通常以比解析 JSON 更受控的方式執行,並且這些錯誤通常是編碼錯誤,因此決定默認返回錯誤。
與未使用的變量類似,忽略的列會浪費網絡和數據庫資源,而且在沒有映射器通知未找到某些內容的情況下,很難在早期檢測到不兼容的映射或結構標籤中的拼寫錯誤。
儘管如此,在某些情況下,可能希望忽略沒有目標字段的列。爲此,每種 Handle 類型都有一個 Unsafe 方法,它返回該 handler 的新副本,並關閉此安全檢查:
var p Person
// 由於place列沒有字段目標,所以這裏的err不是nil
err = db.Get(&p, "SELECT * FROM person, place LIMIT 1;")
// 這不會返回錯誤,即使place列沒有目標
udb := db.Unsafe()
err = udb.Get(&p, "SELECT * FROM person, place LIMIT 1;")
控制命名映射
用作 StructScan
目標的結構體字段必須大寫以便 sqlx
能夠訪問。因此,sqlx
使用了一個 NameMapper
,該映射器將字段名應用 strings.ToLower
函數以將它們映射到行結果中的列。但是,這並不總是符合需求的,這取決於你的數據庫模式,因此 sqlx
允許以多種方式自定義映射。
最簡單的方式是通過使用 sqlx.DB.MapperFunc
爲數據庫 handler 設置映射器,該方法接收一個類型爲 func(string) string
的參數。如果你的庫需要特定的映射器,並且你不想污染你接收到的 sqlx.DB
,你可以爲庫創建一個副本以確保使用特定的默認映射:
// 如果我們的數據庫模式使用大寫列,我們可以使用普通字段
db.MapperFunc(strings.ToUpper)
// 假定一個庫使用小寫列,我們可以創建一個副本
copy := sqlx.NewDb(db.DB, db.DriverName())
copy.MapperFunc(strings.ToLower)
每個 sqlx.DB
使用 sqlx/reflectx
包的 Mapper
來實現這種映射,並將活動的映射器公開爲 sqlx.DB.Mapper
。你可以通過直接設置來進一步自定義 DB 上的映射:
import "github.com/jmoiron/sqlx/reflectx"
// 創建一個新的映射器,它將使用結構字段標籤“json”而不是“db”
db.Mapper = reflectx.NewMapperFunc("json", strings.ToLower)
替代掃描類型
除了使用 Scan
和 StructScan``,sqlx
的 Row
或 Rows
還可以用於自動返回結果切片或 Map:
rows, err := db.Queryx("SELECT * FROM place")
for rows.Next() {
// cols 代表所有列結果的[]interface{}
cols, err := rows.SliceScan()
}
rows, err := db.Queryx("SELECT * FROM place")
for rows.Next() {
results := make(map[string]interface{})
err = rows.MapScan(results)
}
SliceScan
返回一個 []interface{}
,其中包含所有列的數據,這在你代表第三方執行查詢且無法知道可能會返回哪些列的情況下非常有用。MapScan
的行爲類似,但它將列名映射到 interface{}
類型的值上。這裏有一個重要的注意事項:rows.Columns()
返回的結果不包括完全限定的名稱,因此執行如 SELECT a.id, b.id FROM a NATURAL JOIN b
的查詢時,Columns
的結果將是 []string{"id", "id"}
,這會導致你的 Map 中其中一個結果會被覆蓋。
自定義類型
上面的例子都使用了內置類型來進行掃描和查詢,但 database/sql
提供了接口,允許你使用任何自定義類型:
-
sql.Scanner
允許你在Scan()
中使用自定義類型 -
driver.Valuer
允許你在Query/QueryRow/Exec
中使用自定義類型
這些是標準接口,使用它們可以確保與任何可能在 database/sql
之上提供服務的庫的兼容性。要詳細瞭解如何使用它們,請閱讀這篇博客文章 [8] 或查看 sqlx/types[9] 包,該包實現了一些標準的有用類型。
連接池
語句準備和查詢執行需要一個連接,DB 對象將管理一個連接池,以便它可以安全地用於併發查詢。在 Go 1.2 及更高版本中,有兩種方式控制連接池的大小:
-
DB.SetMaxIdleConns(n int)
-
DB.SetMaxOpenConns(n int)
默認情況下,連接池會無限制地增長,並且當池中沒有空閒連接可用時,就會創建新的連接。你可以使用 DB.SetMaxOpenConns
來設置池的最大大小。未被使用的連接會被標記爲空閒狀態,如果不再需要,它們就會被關閉。爲了避免頻繁地創建和關閉連接,請使用 DB.SetMaxIdleConns
將最大空閒大小設置爲適合你的查詢負載的大小。
如果不小心持有連接,很容易遇到麻煩。爲了避免這種情況:
-
確保你使用
Scan()
掃描每個Row
對象 -
確保你通過
Close()
或完全迭代Next()
來處理每個Rows
對象 -
確保每個事務都通過
Commit()
或Rollback()
返回其連接
如果你忽略了這些操作中的任何一個,它們所使用的連接可能會被保持到垃圾回收,而你的數據庫將最終創建大量連接以補償正在使用的連接。請注意,Rows.Close()
可以安全地多次調用,因此不必擔心在可能不必要的地方調用它。
參考資料
[1]
sqlx: https://github.com/jmoiron/sqlx
[2]
database/sql documentation: https://golang.org/pkg/database/sql/
[3]
go-database-sql tutorial: http://go-database-sql.org/
[4]
sqlx 圖解指南: https://jmoiron.github.io/sqlx/
[5]
不允許: https://use-the-index-luke.com/sql/where-clause/bind-parameters
[6]
10 次: https://golang.org/src/pkg/database/sql/sql.go?s=23888:23957#L885
[7]
連接池: https://jmoiron.github.io/sqlx/#connectionPool
[8]
這篇博客文章: http://jmoiron.net/blog/built-in-interfaces
[9]
sqlx/types: https://github.com/jmoiron/sqlx/blob/master/types/types.go
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/EV-c6Ytuo9dJo0b_TAxt2Q