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 來(lái)操作數(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ì)象,可以用來(lái)操作數(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 中我們可以通過(guò)以上 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 方法就是用來(lái)簡(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 方法,不過(guò)在其內(nèi)部實(shí)現(xiàn)中,遇到 error 不再返回,而是直接進(jìn)行 panic,這也是 Go 語(yǔ)言很多庫(kù)中的慣用方法。
聲明模型
我們定義一個(gè) User 結(jié)構(gòu)體來(lái)映射數(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 方法來(lái)執(zhí)行一條 SQL 命令,sqlx 對(duì)其進(jìn)行了擴(kuò)展,提供了 *sqlx.DB.MustExec 方法來(lái)執(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 類型。
下面來(lái)講解下這兩個(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)化查詢多條記錄。
接下來(lái)講解這兩個(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 方法用起來(lái)非常簡(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 方法用起來(lái)同樣非常簡(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 來(lái)動(dòng)態(tài)拼接 SQL 語(yǔ)句,以保證 SQL 中參數(shù)占位符的個(gè)數(shù)是正確的。
sqlx 提供了 In 方法來(lái)支持 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 來(lái)動(dòng)態(tài)調(diào)整。
比如我們傳遞 ids 為 []int64{1, 2, 3},則得到 query 為 SELECT * FROM user WHERE id IN (?, ?, ?)。
注意,我們接下來(lái)又調(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) 方法來(lái)轉(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 的方式來(lái)命名參數(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()
}不過(guò)這種用法不多,你知道就行。以下是事務(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ǔ)句來(lái)處理事務(wù)的回滾操作,這樣就不必在每次處理錯(cuò)誤時(shí)重復(fù)的編寫調(diào)用 tx.Rollback() 的代碼。
如果代碼正常執(zhí)行到最后,通過(guò) tx.Commit() 來(lái)提交事務(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 方法,只不過(guò)將其返回結(jié)果構(gòu)造成 *sqlx.Stmt 類型并返回。
不安全的掃描
在使用 *sqlx.DB.Get 等方法查詢記錄時(shí),如果 SQL 語(yǔ)句查詢出來(lái)的字段與要綁定的模型屬性不匹配,則會(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è)屬性,由于無(wú)法一一對(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 的隱患。
不過(guò),有些時(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 來(lái)查詢記錄,而是先通過(guò) 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ì)象上。
通過(guò)這個(gè)屬性的名稱我們就知道,這是不安全的做法,不被推薦。
與未使用的變量一樣,被忽略的列是對(duì)網(wǎng)絡(luò)和數(shù)據(jù)庫(kù)資源的浪費(fèi),并且這很容易導(dǎo)致出現(xiàn)模型與數(shù)據(jù)庫(kù)表不匹配而不被感知的情況。
Scan 變體
前文示例中,我們見過(guò)了 *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)來(lái)。
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)簽來(lái)映射查詢字段和模型屬性。
默認(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 方法來(lái)控制查詢字段和模型屬性的映射關(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)來(lái)。
注意這里的查詢語(yǔ)句中,查詢字段全部通過(guò) as 重新命名成了大寫形式,而 User 模型字段 db 默認(rèn)都為小寫形式。
copyDB.MapperFunc(strings.ToUpper) 的作用,就是在調(diào)用 Get 方法將查詢結(jié)果掃描到結(jié)構(gòu)體時(shí),把 User 模型的小寫字段,通過(guò) strings.ToUpper 方法轉(zhuǎn)成大寫,這樣查詢字段和模型屬性就全為大寫了,也就能夠一一匹配上了。
還有一種情況,如果你的模型已存在 json 標(biāo)簽,并且不想重復(fù)的再抄一遍到 db 標(biāo)簽,我們可以直接使用 json 標(biāo)簽來(lái)映射查詢字段和模型屬性。
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,并通過(guò) strings.ToLower 方法轉(zhuǎn)換為小寫。
reflectx 按照如下方式導(dǎo)入:
import "github.com/jmoiron/sqlx/reflectx"
現(xiàn)在,查詢語(yǔ)句中 name 屬性通過(guò)使用 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 用來(lái)連接數(shù)據(jù)庫(kù),*sqlx.DB.MustExec 用來(lái)執(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)體使可以用來(lái)忽略不匹配的記錄字段。
除了能夠?qū)⒉樵兘Y(jié)果掃描到 struct,sqlx 還支持將查詢結(jié)果掃描到 map 和 slice。
sqlx 使用 db 結(jié)構(gòu)體標(biāo)簽來(lái)映射查詢字段和模型屬性,如果不顯式指定 db 標(biāo)簽,默認(rèn)映射的模型屬性為小寫形式,可以通過(guò) *sqlx.DB.MapperFunc 函數(shù)來(lái)修改默認(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ì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-12-12
go語(yǔ)言中布隆過(guò)濾器低空間成本判斷元素是否存在方式
這篇文章主要為大家介紹了go語(yǔ)言中布隆過(guò)濾器低空間成本判斷元素是否存在方式詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-09-09
Go實(shí)現(xiàn)map并發(fā)安全的3種方式總結(jié)
Go的原生map不是并發(fā)安全的,在多協(xié)程讀寫同一個(gè)map的時(shí)候,安全性無(wú)法得到保障,這篇文章主要給大家總結(jié)介紹了關(guān)于Go實(shí)現(xiàn)map并發(fā)安全的3種方式,需要的朋友可以參考下2023-10-10
15個(gè)Golang中時(shí)間處理的實(shí)用函數(shù)
在Go編程中,處理日期和時(shí)間是一項(xiàng)常見任務(wù),涉及到精確性和靈活性,本文將介紹一系列實(shí)用函數(shù),它們充當(dāng)time包的包裝器,需要的可以參考下2024-01-01
Go語(yǔ)言開源庫(kù)實(shí)現(xiàn)Onvif協(xié)議客戶端設(shè)備搜索
這篇文章主要為大家介紹了Go語(yǔ)言O(shè)nvif協(xié)議客戶端設(shè)備搜索示例實(shí)現(xiàn),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-04-04

