Go語(yǔ)言使用sqlx操作數(shù)據(jù)庫(kù)的示例詳解
sqlx 是 Go 語(yǔ)言中一個(gè)流行的第三方包,它提供了對(duì) Go 標(biāo)準(zhǔn)庫(kù) database/sql
的擴(kuò)展,旨在簡(jiǎn)化和改進(jìn) Go 語(yǔ)言中使用 SQL 的體驗(yàn),并提供了更加強(qiáng)大的數(shù)據(jù)庫(kù)交互功能。sqlx
保留了 database/sql
接口不變,是 database/sql
的超集,這使得將現(xiàn)有項(xiàng)目中使用的 database/sql
替換為 sqlx
變得相當(dāng)輕松。
本文重點(diǎn)講解 sqlx
在 database/sql
基礎(chǔ)上擴(kuò)展的功能,對(duì)于 database/sql
已經(jīng)支持的功能則不會(huì)詳細(xì)講解。如果你對(duì) database/sql
不熟悉,可以查看我的另一篇文章《在 Go 中如何使用 database/sql 來操作數(shù)據(jù)庫(kù)》。
安裝
sqlx
安裝方式同 Go 語(yǔ)言中其他第三方包一樣:
$ go get github.com/jmoiron/sqlx
sqlx 類型設(shè)計(jì)
sqlx
的設(shè)計(jì)與 database/sql
差別不大,編碼風(fēng)格較為統(tǒng)一,參考 database/sql
標(biāo)準(zhǔn)庫(kù),sqlx
提供了如下幾種與之對(duì)應(yīng)的數(shù)據(jù)類型:
sqlx.DB
:類似于sql.DB
,表示數(shù)據(jù)庫(kù)對(duì)象,可以用來操作數(shù)據(jù)庫(kù)。sqlx.Tx
:類似于sql.Tx
,事務(wù)對(duì)象。sqlx.Stmt
:類似于sql.Stmt
,預(yù)處理 SQL 語(yǔ)句。sqlx.NamedStmt
:對(duì)sqlx.Stmt
的封裝,支持具名參數(shù)。sqlx.Rows
:類似于sql.Rows
,sqlx.Queryx
的返回結(jié)果。sqlx.Row
:類似于sql.Row
,sqlx.QueryRowx
的返回結(jié)果。
以上類型與 database/sql
提供的對(duì)應(yīng)類型在功能上區(qū)別不大,但 sqlx
為這些類型提供了更友好的方法。
準(zhǔn)備
為了演示 sqlx
用法,我準(zhǔn)備了如下 MySQL 數(shù)據(jù)庫(kù)表:
DROP TABLE IF EXISTS `user`; CREATE TABLE `user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(50) DEFAULT NULL COMMENT '用戶名', `email` varchar(255) NOT NULL DEFAULT '' COMMENT '郵箱', `age` tinyint(4) NOT NULL DEFAULT '0' COMMENT '年齡', `birthday` datetime DEFAULT NULL COMMENT '生日', `salary` varchar(128) DEFAULT NULL COMMENT '薪水', `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `u_email` (`email`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用戶表';
你可以使用 MySQL 命令行或圖形化工具創(chuàng)建這張表。
連接數(shù)據(jù)庫(kù)
使用 sqlx
連接數(shù)據(jù)庫(kù):
package main import ( "database/sql" "log" _ "github.com/go-sql-driver/mysql" "github.com/jmoiron/sqlx" ) func main() { var ( db *sqlx.DB err error dsn = "user:password@tcp(127.0.0.1:3306)/demo?charset=utf8mb4&parseTime=true&loc=Local" ) // 1. 使用 sqlx.Open 連接數(shù)據(jù)庫(kù) db, err = sqlx.Open("mysql", dsn) if err != nil { log.Fatal(err) } // 2. 使用 sqlx.Open 變體方法 sqlx.MustOpen 連接數(shù)據(jù)庫(kù),如果出現(xiàn)錯(cuò)誤直接 panic db = sqlx.MustOpen("mysql", dsn) // 3. 如果已經(jīng)有了 *sql.DB 對(duì)象,則可以使用 sqlx.NewDb 連接數(shù)據(jù)庫(kù),得到 *sqlx.DB 對(duì)象 sqlDB, err := sql.Open("mysql", dsn) if err != nil { log.Fatal(err) } db = sqlx.NewDb(sqlDB, "mysql") // 4. 使用 sqlx.Connect 連接數(shù)據(jù)庫(kù),等價(jià)于 sqlx.Open + db.Ping db, err = sqlx.Connect("mysql", dsn) if err != nil { log.Fatal(err) } // 5. 使用 sqlx.Connect 變體方法 sqlx.MustConnect 連接數(shù)據(jù)庫(kù),如果出現(xiàn)錯(cuò)誤直接 panic db = sqlx.MustConnect("mysql", dsn) }
在 sqlx
中我們可以通過以上 5 種方式連接數(shù)據(jù)庫(kù)。
sqlx.Open
對(duì)標(biāo) sql.Open
方法,返回 *sqlx.DB
類型。
sqlx.MustOpen
與 sqlx.Open
一樣會(huì)返回 *sqlx.DB
實(shí)例,但如果遇到錯(cuò)誤則會(huì) panic
。
sqlx.NewDb
支持從一個(gè) database/sql
包的 *sql.DB
對(duì)象創(chuàng)建一個(gè)新的 *sqlx.DB
類型,并且需要指定驅(qū)動(dòng)名稱。
使用前 3 種方式連接數(shù)據(jù)庫(kù)并不會(huì)立即與數(shù)據(jù)庫(kù)建立連接,連接將會(huì)在合適的時(shí)候延遲建立。為了確保能夠正常連接數(shù)據(jù)庫(kù),往往需要調(diào)用 db.Ping()
方法進(jìn)行驗(yàn)證:
ctx := context.Background() if err := db.PingContext(ctx); err != nil { log.Fatal(err) }
sqlx
提供的 sqlx.Connect
方法就是用來簡(jiǎn)化這一操作的,它等價(jià)于 sqlx.Open
+ db.Ping
兩個(gè)方法,其定義如下:
func Connect(driverName, dataSourceName string) (*DB, error) { db, err := Open(driverName, dataSourceName) if err != nil { return nil, err } err = db.Ping() if err != nil { db.Close() return nil, err } return db, nil }
sqlx.MustConnect
方法在 sqlx.Connect
方法的基礎(chǔ)上,提供了遇到錯(cuò)誤立即 panic
的功能。看到 sqlx.MustConnect
方法的定義你就明白了:
func MustConnect(driverName, dataSourceName string) *DB { db, err := Connect(driverName, dataSourceName) if err != nil { panic(err) } return db }
以后當(dāng)你遇見 MustXxx
類似方法名時(shí)就應(yīng)該想到,其功能往往等價(jià)于 Xxx
方法,不過在其內(nèi)部實(shí)現(xiàn)中,遇到 error
不再返回,而是直接進(jìn)行 panic
,這也是 Go 語(yǔ)言很多庫(kù)中的慣用方法。
聲明模型
我們定義一個(gè) User
結(jié)構(gòu)體來映射數(shù)據(jù)庫(kù)中的 user
表:
type User struct { ID int Name sql.NullString `json:"username"` Email string Age int Birthday time.Time Salary Salary CreatedAt time.Time `db:"created_at"` UpdatedAt time.Time `db:"updated_at"` } type Salary struct { Month int `json:"month"` Year int `json:"year"` } // Scan implements sql.Scanner, use custom types in *sql.Rows.Scan func (s *Salary) Scan(src any) error { if src == nil { return nil } var buf []byte switch v := src.(type) { case []byte: buf = v case string: buf = []byte(v) default: return fmt.Errorf("invalid type: %T", src) } err := json.Unmarshal(buf, s) return err } // Value implements driver.Valuer, use custom types in Query/QueryRow/Exec func (s Salary) Value() (driver.Value, error) { v, err := json.Marshal(s) return string(v), err }
User
結(jié)構(gòu)體在這里可以被稱為「模型」。
執(zhí)行 SQL 命令
database/sql
包提供了 *sql.DB.Exec
方法來執(zhí)行一條 SQL 命令,sqlx
對(duì)其進(jìn)行了擴(kuò)展,提供了 *sqlx.DB.MustExec
方法來執(zhí)行一條 SQL 命令:
func MustCreateUser(db *sqlx.DB) (int64, error) { birthday := time.Date(2000, 1, 1, 0, 0, 0, 0, time.Local) user := User{ Name: sql.NullString{String: "jianghushinian", Valid: true}, Email: "jianghushinian007@outlook.com", Age: 10, Birthday: birthday, Salary: Salary{ Month: 100000, Year: 10000000, }, } res := db.MustExec( `INSERT INTO user(name, email, age, birthday, salary) VALUES(?, ?, ?, ?, ?)`, user.Name, user.Email, user.Age, user.Birthday, user.Salary, ) return res.LastInsertId() }
這里使用 *sqlx.DB.MustExec
方法插入了一條 user
記錄。
*sqlx.DB.MustExec
方法定義如下:
func (db *DB) MustExec(query string, args ...interface{}) sql.Result { return MustExec(db, query, args...) } func MustExec(e Execer, query string, args ...interface{}) sql.Result { res, err := e.Exec(query, args...) if err != nil { panic(err) } return res }
與前文介紹的 sqlx.MustOpen
方法一樣,*sqlx.DB.MustExec
方法也會(huì)在遇到錯(cuò)誤時(shí)直接 panic
,其內(nèi)部調(diào)用的是 *sqlx.DB.Exec
方法。
執(zhí)行 SQL 查詢
database/sql
包提供了 *sql.DB.Query
和 *sql.DB.QueryRow
兩個(gè)查詢方法,其簽名如下:
func (db *DB) Query(query string, args ...any) (*Rows, error) func (db *DB) QueryRow(query string, args ...any) *Row
sqlx
在這兩個(gè)方法的基礎(chǔ)上,擴(kuò)展出如下兩個(gè)方法:
func (db *DB) Queryx(query string, args ...interface{}) (*Rows, error) func (db *DB) QueryRowx(query string, args ...interface{}) *Row
這兩個(gè)方法返回的類型正是前文 sqlx 類型設(shè)計(jì) 中提到的 sqlx.Rows
、sqlx.Row
類型。
下面來講解下這兩個(gè)方法如何使用。
Queryx
使用 *sqlx.DB.Queryx
方法查詢記錄如下:
func QueryxUsers(db *sqlx.DB) ([]User, error) { var us []User rows, err := db.Queryx("SELECT * FROM user") if err != nil { return nil, err } defer rows.Close() for rows.Next() { var u User // sqlx 提供了便捷方法可以將查詢結(jié)果直接掃描到結(jié)構(gòu)體 err = rows.StructScan(&u) if err != nil { return nil, err } us = append(us, u) } return us, nil }
*sqlx.DB.Queryx
方法簽名雖然與 *sql.DB.Query
方法基本相同,但它返回類型 *sqlx.Rows
得到了擴(kuò)展,其提供的 StructScan
方法能夠方便的將查詢結(jié)果直接掃描到 User
結(jié)構(gòu)體,這極大的增加了便攜性,我們?cè)僖膊挥孟袷褂?*sql.Rows
提供的 Scan
方法那樣挨個(gè)寫出 User
的屬性了。
QueryRowx
使用 *sqlx.DB.QueryRowx
方法查詢記錄如下:
func QueryRowxUser(db *sqlx.DB, id int) (User, error) { var u User err := db.QueryRowx("SELECT * FROM user WHERE id = ?", id).StructScan(&u) return u, err }
*sqlx.Row
同樣提供了 StructScan
方法將查詢結(jié)果掃描到結(jié)構(gòu)體。
另外,這里使用了鏈?zhǔn)秸{(diào)用的方式,在調(diào)用 db.QueryRowx()
之后直接調(diào)用了 .StructScan(&u)
,接收的 err
是 StructScan
的返回結(jié)果。這是因?yàn)?db.QueryRowx()
的返回結(jié)果 *sqlx.Row
中記錄了錯(cuò)誤信息 err
,如果查詢階段遇到錯(cuò)誤會(huì)被記錄到 *sqlx.Row.err
中。在調(diào)用 StructScan
方法階段,其內(nèi)部首先判斷 r.err != nil
,如果存在 err
直接返回錯(cuò)誤,沒有錯(cuò)誤則將查詢結(jié)果掃描到 dest
參數(shù)接收到的結(jié)構(gòu)體指針,代碼實(shí)現(xiàn)如下:
type Row struct { err error unsafe bool rows *sql.Rows Mapper *reflectx.Mapper } func (r *Row) StructScan(dest interface{}) error { return r.scanAny(dest, true) } func (r *Row) scanAny(dest interface{}, structOnly bool) error { if r.err != nil { return r.err } ... }
sqlx
不僅擴(kuò)展了 *sql.DB.Query
和 *sql.DB.QueryRow
兩個(gè)查詢方法,它還新增了兩個(gè)查詢方法:
func (db *DB) Get(dest interface{}, query string, args ...interface{}) error func (db *DB) Select(dest interface{}, query string, args ...interface{}) error
*sqlx.DB.Get
方法包裝了 *sqlx.DB.QueryRowx
方法,用以簡(jiǎn)化查詢單條記錄。
*sqlx.DB.Select
方法包裝了 *sqlx.DB.Queryx
方法,用以簡(jiǎn)化查詢多條記錄。
接下來講解這兩個(gè)方法如何使用。
Get
使用 *sqlx.DB.Get
方法查詢記錄如下:
func GetUser(db *sqlx.DB, id int) (User, error) { var u User // 查詢記錄掃描數(shù)據(jù)到 struct err := db.Get(&u, "SELECT * FROM user WHERE id = ?", id) return u, err }
可以發(fā)現(xiàn) *sqlx.DB.Get
方法用起來非常簡(jiǎn)單,我們不再需要調(diào)用 StructScan
方法將查詢結(jié)果掃描到結(jié)構(gòu)體中,只需要將結(jié)構(gòu)體指針當(dāng)作 Get
方法的第一個(gè)參數(shù)傳遞進(jìn)去即可。
其代碼實(shí)現(xiàn)如下:
func (db *DB) Get(dest interface{}, query string, args ...interface{}) error { return Get(db, dest, query, args...) } func Get(q Queryer, dest interface{}, query string, args ...interface{}) error { r := q.QueryRowx(query, args...) return r.scanAny(dest, false) }
根據(jù)源碼可以看出,*sqlx.DB.Get
內(nèi)部調(diào)用了 *sqlx.DB.QueryRowx
方法。
Select
使用 *sqlx.DB.Select
方法查詢記錄如下:
func SelectUsers(db *sqlx.DB) ([]User, error) { var us []User // 查詢記錄掃描數(shù)據(jù)到 slice err := db.Select(&us, "SELECT * FROM user") return us, err }
可以發(fā)現(xiàn) *sqlx.DB.Select
方法用起來同樣非常簡(jiǎn)單,它可以直接將查詢結(jié)果掃描到 []User
切片中。
其代碼實(shí)現(xiàn)如下:
func (db *DB) Select(dest interface{}, query string, args ...interface{}) error { return Select(db, dest, query, args...) } func Select(q Queryer, dest interface{}, query string, args ...interface{}) error { rows, err := q.Queryx(query, args...) if err != nil { return err } // if something happens here, we want to make sure the rows are Closed defer rows.Close() return scanAll(rows, dest, false) }
根據(jù)源碼可以看出,*sqlx.DB.Select
內(nèi)部調(diào)用了 *sqlx.DB.Queryx
方法。
sqlx.In
在 database/sql
中如果想要執(zhí)行 SQL IN 查詢,由于 IN 查詢參數(shù)長(zhǎng)度不固定,我們不得不使用 fmt.Sprintf
來動(dòng)態(tài)拼接 SQL 語(yǔ)句,以保證 SQL 中參數(shù)占位符的個(gè)數(shù)是正確的。
sqlx
提供了 In
方法來支持 SQL IN 查詢,這極大的簡(jiǎn)化了代碼,也使得代碼更易維護(hù)和安全。
使用示例如下:
func SqlxIn(db *sqlx.DB, ids []int64) ([]User, error) { query, args, err := sqlx.In("SELECT * FROM user WHERE id IN (?)", ids) if err != nil { return nil, err } query = db.Rebind(query) rows, err := db.Query(query, args...) if err != nil { return nil, err } defer rows.Close() var us []User for rows.Next() { var user User err = rows.Scan(&user.ID, &user.Name, &user.Email, &user.Age, &user.Birthday, &user.Salary, &user.CreatedAt, &user.UpdatedAt) if err != nil { return nil, err } us = append(us, user) } return us, nil }
調(diào)用 sqlx.In
并傳遞 SQL 語(yǔ)句以及切片類型的參數(shù),它將返回新的查詢 SQL query
以及參數(shù) args
,這個(gè) query
將會(huì)根據(jù) ids
來動(dòng)態(tài)調(diào)整。
比如我們傳遞 ids
為 []int64{1, 2, 3}
,則得到 query
為 SELECT * FROM user WHERE id IN (?, ?, ?)
。
注意,我們接下來又調(diào)用 db.Rebind(query)
重新綁定了 query
變量的參數(shù)占位符。如果你使用 MySQL 數(shù)據(jù)庫(kù),這不是必須的,因?yàn)槲覀兪褂玫?MySQL 驅(qū)動(dòng)程序參數(shù)占位符就是 ?
。而如果你使用 PostgreSQL 數(shù)據(jù)庫(kù),由于 PostgreSQL 驅(qū)動(dòng)程序參數(shù)占位符是 $n
,這時(shí)就必須要調(diào)用 db.Rebind(query)
方法來轉(zhuǎn)換參數(shù)占位符了。
它會(huì)將 SELECT * FROM user WHERE id IN (?, ?, ?)
中的參數(shù)占位符轉(zhuǎn)換為 PostgreSQL 驅(qū)動(dòng)程序能夠識(shí)別的參數(shù)占位符 SELECT * FROM user WHERE id IN ($1, $2, $3)
。
之后的代碼就跟使用 database/sql
查詢記錄沒什么兩樣了。
使用具名參數(shù)
sqlx
提供了兩個(gè)方法 NamedExec
、NamedQuery
,它們能夠支持具名參數(shù) :name
,這樣就不必再使用 ?
這種占位符的形式了。
這兩個(gè)方法簽名如下:
func (db *DB) NamedExec(query string, arg interface{}) (sql.Result, error) func (db *DB) NamedQuery(query string, arg interface{}) (*Rows, error)
其使用示例如下:
func NamedExec(db *sqlx.DB) error { m := map[string]interface{}{ "email": "jianghushinian007@outlook.com", "age": 18, } result, err := db.NamedExec(`UPDATE user SET age = :age WHERE email = :email`, m) if err != nil { return err } fmt.Println(result.RowsAffected()) return nil } func NamedQuery(db *sqlx.DB) ([]User, error) { u := User{ Email: "jianghushinian007@outlook.com", Age: 18, } rows, err := db.NamedQuery("SELECT * FROM user WHERE email = :email OR age = :age", u) if err != nil { return nil, err } defer rows.Close() var users []User for rows.Next() { var user User err := rows.StructScan(&user) if err != nil { return nil, err } users = append(users, user) } return users, nil }
我們可以使用 :name
的方式來命名參數(shù),它能夠匹配 map
或 struct
對(duì)應(yīng)字段的參數(shù)值,這樣的 SQL 語(yǔ)句可讀性更強(qiáng)。
事務(wù)
在事務(wù)的支持上,sqlx
擴(kuò)展出了 Must
版本的事務(wù),使用示例如下:
func MustTransaction(db *sqlx.DB) error { tx := db.MustBegin() tx.MustExec("UPDATE user SET age = 25 WHERE id = ?", 1) return tx.Commit() }
不過這種用法不多,你知道就行。以下是事務(wù)的推薦用法:
func Transaction(db *sqlx.DB, id int64, name string) error { tx, err := db.Begin() if err != nil { return err } defer tx.Rollback() res, err := tx.Exec("UPDATE user SET name = ? WHERE id = ?", name, id) if err != nil { return err } rowsAffected, err := res.RowsAffected() if err != nil { return err } fmt.Printf("rowsAffected: %d\n", rowsAffected) return tx.Commit() }
我們使用 defer
語(yǔ)句來處理事務(wù)的回滾操作,這樣就不必在每次處理錯(cuò)誤時(shí)重復(fù)的編寫調(diào)用 tx.Rollback()
的代碼。
如果代碼正常執(zhí)行到最后,通過 tx.Commit()
來提交事務(wù),此時(shí)即使再調(diào)用 tx.Rollback()
也不會(huì)對(duì)結(jié)果產(chǎn)生影響。
預(yù)處理語(yǔ)句
sqlx
針對(duì) *sql.DB.Prepare
擴(kuò)展出了 *sqlx.DB.Preparex
方法,返回 *sqlx.Stmt
類型。
*sqlx.Stmt
類型支持 Queryx
、QueryRowx
、Get
、Select
這些 sqlx
特有的方法。
其使用示例如下:
func PreparexGetUser(db *sqlx.DB) (User, error) { stmt, err := db.Preparex(`SELECT * FROM user WHERE id = ?`) if err != nil { return User{}, err } var u User err = stmt.Get(&u, 1) return u, err }
*sqlx.DB.Preparex
方法定義如下:
func Preparex(p Preparer, query string) (*Stmt, error) { s, err := p.Prepare(query) if err != nil { return nil, err } return &Stmt{Stmt: s, unsafe: isUnsafe(p), Mapper: mapperFor(p)}, err }
實(shí)際上 *sqlx.DB.Preparex
內(nèi)部還是調(diào)用的 *sql.DB.Preapre
方法,只不過將其返回結(jié)果構(gòu)造成 *sqlx.Stmt
類型并返回。
不安全的掃描
在使用 *sqlx.DB.Get
等方法查詢記錄時(shí),如果 SQL 語(yǔ)句查詢出來的字段與要綁定的模型屬性不匹配,則會(huì)報(bào)錯(cuò)。
示例如下:
func GetUser(db *sqlx.DB) (User, error) { var user struct { ID int Name string Email string // 沒有 Age 屬性 } err := db.Get(&user, "SELECT id, name, email, age FROM user WHERE id = ?", 1) if err != nil { return User{}, err } return User{ ID: user.ID, Name: sql.NullString{String: user.Name}, Email: user.Email, }, nil }
以上示例代碼中,SQL 語(yǔ)句中查詢了 id
、name
、email
、age
4 個(gè)字段,而 user
結(jié)構(gòu)體則只有 ID
、Name
、Email
3 個(gè)屬性,由于無法一一對(duì)應(yīng),執(zhí)行以上代碼,我們將得到如下報(bào)錯(cuò)信息:
missing destination name age in *struct { ID int; Name string; Email string }
這種表現(xiàn)是合理的,符合 Go 語(yǔ)言的編程風(fēng)格,盡早暴露錯(cuò)誤有助于減少代碼存在 BUG 的隱患。
不過,有些時(shí)候,我們就是為了方便想要讓上面的示例代碼能夠運(yùn)行,可以這樣做:
func UnsafeGetUser(db *sqlx.DB) (User, error) { var user struct { ID int Name string Email string // 沒有 Age 屬性 } udb := db.Unsafe() err := udb.Get(&user, "SELECT id, name, email, age FROM user WHERE id = ?", 1) if err != nil { return User{}, err } return User{ ID: user.ID, Name: sql.NullString{String: user.Name}, Email: user.Email, }, nil }
這里我們不再直接使用 db.Get
來查詢記錄,而是先通過 udb := db.Unsafe()
獲取 unsafe
屬性為 true
的 *sqlx.DB
對(duì)象,然后再調(diào)用它的 Get
方法。
*sqlx.DB
定義如下:
type DB struct { *sql.DB driverName string unsafe bool Mapper *reflectx.Mapper }
當(dāng) unsafe
屬性為 true
時(shí),*sqlx.DB
對(duì)象會(huì)忽略不匹配的字段,使代碼能夠正常運(yùn)行,并將能夠匹配的字段正確綁定到 user
結(jié)構(gòu)體對(duì)象上。
通過這個(gè)屬性的名稱我們就知道,這是不安全的做法,不被推薦。
與未使用的變量一樣,被忽略的列是對(duì)網(wǎng)絡(luò)和數(shù)據(jù)庫(kù)資源的浪費(fèi),并且這很容易導(dǎo)致出現(xiàn)模型與數(shù)據(jù)庫(kù)表不匹配而不被感知的情況。
Scan 變體
前文示例中,我們見過了 *sqlx.Rows.Scan
的變體 *sqlx.Rows.StructScan
的用法,它能夠方便的將查詢結(jié)果掃描到 struct
中。
sqlx
還提供了 *sqlx.Rows.MapScan
、*sqlx.Rows.SliceScan
兩個(gè)方法,能夠?qū)⒉樵兘Y(jié)果分別掃描到 map
和 slice
中。
使用示例如下:
func MapScan(db *sqlx.DB) ([]map[string]interface{}, error) { rows, err := db.Queryx("SELECT * FROM user") if err != nil { return nil, err } defer rows.Close() var res []map[string]interface{} for rows.Next() { r := make(map[string]interface{}) err := rows.MapScan(r) if err != nil { return nil, err } res = append(res, r) } return res, err } func SliceScan(db *sqlx.DB) ([][]interface{}, error) { rows, err := db.Queryx("SELECT * FROM user") if err != nil { return nil, err } defer rows.Close() var res [][]interface{} for rows.Next() { // cols is an []interface{} of all the column results cols, err := rows.SliceScan() if err != nil { return nil, err } res = append(res, cols) } return res, err }
其中,rows.MapScan(r)
用法與 rows.StructScan(&u)
用法類似,都是將接收查詢結(jié)果集的目標(biāo)模型指針變量當(dāng)作參數(shù)傳遞進(jìn)來。
rows.SliceScan()
用法略有不同,它不接收參數(shù),而是將結(jié)果保存在 []interface{}
中并返回。
可以按需使用以上兩個(gè)方法。
控制字段名稱映射
講到這里,想必不少同學(xué)心里可能存在一個(gè)疑惑,rows.StructScan(&u)
在將查詢記錄的字段映射到對(duì)應(yīng)結(jié)構(gòu)體屬性時(shí),是如何找到對(duì)應(yīng)關(guān)系的呢?
答案就是 db
結(jié)構(gòu)體標(biāo)簽。
回顧前文講 聲明模型 時(shí),User
結(jié)構(gòu)體中定義的 CreatedAt
、UpdatedAt
兩個(gè)字段,定義如下:
CreatedAt time.Time `db:"created_at"` UpdatedAt time.Time `db:"updated_at"`
這里顯式的標(biāo)明了結(jié)構(gòu)體標(biāo)簽 db
,sqlx
正是使用 db
標(biāo)簽來映射查詢字段和模型屬性。
默認(rèn)情況下,結(jié)構(gòu)體字段會(huì)被映射成全小寫形式,如 ID
字段會(huì)被映射為 id
,而 CreatedAt
字段會(huì)被映射為 createdat
。
因?yàn)樵?user
數(shù)據(jù)庫(kù)表中,創(chuàng)建時(shí)間和更新時(shí)間兩個(gè)字段分別為 created_at
、updated_at
,與 sqlx
默認(rèn)字段映射規(guī)則不匹配,所以我才顯式的為 CreatedAt
和 UpdatedAt
兩個(gè)字段指明了 db
標(biāo)簽,這樣 sqlx
的 rows.StructScan
就能正常工作了。
當(dāng)然,數(shù)據(jù)庫(kù)字段不一定都是小寫,如果你的數(shù)據(jù)庫(kù)字段為全大寫,sqlx
提供了 *sqlx.DB.MapperFunc
方法來控制查詢字段和模型屬性的映射關(guān)系。
其使用示例如下:
func MapperFuncUseToUpper(db *sqlx.DB) (User, error) { copyDB := sqlx.NewDb(db.DB, db.DriverName()) copyDB.MapperFunc(strings.ToUpper) var user User err := copyDB.Get(&user, "SELECT id as ID, name as NAME, email as EMAIL FROM user WHERE id = ?", 1) if err != nil { return User{}, err } return user, nil }
這里為了不改變?cè)械?db
對(duì)象,我們復(fù)制了一個(gè) copyDB
,調(diào)用 copyDB.MapperFunc
并將 strings.ToUpper
傳遞進(jìn)來。
注意這里的查詢語(yǔ)句中,查詢字段全部通過 as
重新命名成了大寫形式,而 User
模型字段 db
默認(rèn)都為小寫形式。
copyDB.MapperFunc(strings.ToUpper)
的作用,就是在調(diào)用 Get
方法將查詢結(jié)果掃描到結(jié)構(gòu)體時(shí),把 User
模型的小寫字段,通過 strings.ToUpper
方法轉(zhuǎn)成大寫,這樣查詢字段和模型屬性就全為大寫了,也就能夠一一匹配上了。
還有一種情況,如果你的模型已存在 json
標(biāo)簽,并且不想重復(fù)的再抄一遍到 db
標(biāo)簽,我們可以直接使用 json
標(biāo)簽來映射查詢字段和模型屬性。
func MapperFuncUseJsonTag(db *sqlx.DB) (User, error) { copyDB := sqlx.NewDb(db.DB, db.DriverName()) // Create a new mapper which will use the struct field tag "json" instead of "db" copyDB.Mapper = reflectx.NewMapperFunc("json", strings.ToLower) var user User // json tag err := copyDB.Get(&user, "SELECT id, name as username, email FROM user WHERE id = ?", 1) if err != nil { return User{}, err } return user, nil }
這里需要直接修改 copyDB.Mapper
屬性,賦值為 reflectx.NewMapperFunc("json", strings.ToLower)
將模型映射的標(biāo)簽由 db
改為 json
,并通過 strings.ToLower
方法轉(zhuǎn)換為小寫。
reflectx
按照如下方式導(dǎo)入:
import "github.com/jmoiron/sqlx/reflectx"
現(xiàn)在,查詢語(yǔ)句中 name
屬性通過使用 as
被重命名為 username
,而 username
剛好與 User
模型中 Name
字段的 json
標(biāo)簽相對(duì)應(yīng):
Name sql.NullString `json:"username"`
所以,以上示例代碼能夠正確映射查詢字段和模型屬性。
總結(jié)
sqlx
建立在 database/sql
包之上,用于簡(jiǎn)化和增強(qiáng)與關(guān)系型數(shù)據(jù)庫(kù)的交互操作。
對(duì)常見數(shù)據(jù)庫(kù)操作方法,sqlx
提供了 Must
版本,如 sqlx.MustOpen
用來連接數(shù)據(jù)庫(kù),*sqlx.DB.MustExec
用來執(zhí)行 SQL 語(yǔ)句,當(dāng)遇到 error
時(shí)將會(huì)直接 panic
。
sqlx
還擴(kuò)展了查詢方法 *sqlx.DB.Queryx
、*sqlx.DB.QueryRowx
、*sqlx.DB.Get
、*sqlx.DB.Select
,并且這些查詢方法支持直接將查詢結(jié)果掃描到結(jié)構(gòu)體。
sqlx
為 SQL IN 操作提供了便捷方法 sqlx.In
。
為了使 SQL 更易閱讀,sqlx
提供了 *sqlx.DB.NamedExec
、*sqlx.DB.NamedQuery
兩個(gè)方法支持具名參數(shù)。
調(diào)用 *sqlx.DB.Unsafe()
方法能夠獲取 unsafe
屬性為 true
的 *sqlx.DB
對(duì)象,在將查詢結(jié)果掃描到結(jié)構(gòu)體使可以用來忽略不匹配的記錄字段。
除了能夠?qū)⒉樵兘Y(jié)果掃描到 struct
,sqlx
還支持將查詢結(jié)果掃描到 map
和 slice
。
sqlx
使用 db
結(jié)構(gòu)體標(biāo)簽來映射查詢字段和模型屬性,如果不顯式指定 db
標(biāo)簽,默認(rèn)映射的模型屬性為小寫形式,可以通過 *sqlx.DB.MapperFunc
函數(shù)來修改默認(rèn)行為。
本文完整代碼示例我放在了 GitHub 上,歡迎點(diǎn)擊查看。
以上就是Go語(yǔ)言使用sqlx操作數(shù)據(jù)庫(kù)的示例詳解的詳細(xì)內(nèi)容,更多關(guān)于Go sqlx操作數(shù)據(jù)庫(kù)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
golang將切片或數(shù)組根據(jù)某個(gè)字段進(jìn)行分組操作
這篇文章主要介紹了golang將切片或數(shù)組根據(jù)某個(gè)字段進(jìn)行分組操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-12-12Go實(shí)現(xiàn)map并發(fā)安全的3種方式總結(jié)
Go的原生map不是并發(fā)安全的,在多協(xié)程讀寫同一個(gè)map的時(shí)候,安全性無法得到保障,這篇文章主要給大家總結(jié)介紹了關(guān)于Go實(shí)現(xiàn)map并發(fā)安全的3種方式,需要的朋友可以參考下2023-10-1015個(gè)Golang中時(shí)間處理的實(shí)用函數(shù)
在Go編程中,處理日期和時(shí)間是一項(xiàng)常見任務(wù),涉及到精確性和靈活性,本文將介紹一系列實(shí)用函數(shù),它們充當(dāng)time包的包裝器,需要的可以參考下2024-01-01Go語(yǔ)言開源庫(kù)實(shí)現(xiàn)Onvif協(xié)議客戶端設(shè)備搜索
這篇文章主要為大家介紹了Go語(yǔ)言O(shè)nvif協(xié)議客戶端設(shè)備搜索示例實(shí)現(xiàn),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-04-04