欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Go語(yǔ)言使用sqlx操作數(shù)據(jù)庫(kù)的示例詳解

 更新時(shí)間:2023年06月19日 09:57:52   作者:江湖十年  
sqlx?是?Go?語(yǔ)言中一個(gè)流行的第三方包,它提供了對(duì)?Go?標(biāo)準(zhǔn)庫(kù)?database/sql?的擴(kuò)展,本文重點(diǎn)講解?sqlx?在?database/sql?基礎(chǔ)上擴(kuò)展的功能,希望對(duì)大家有所幫助

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)講解 sqlxdatabase/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.MustOpensqlx.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),接收的 errStructScan 的返回結(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},則得到 querySELECT * 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ù),它能夠匹配 mapstruct 對(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、nameemail、age 4 個(gè)字段,而 user 結(jié)構(gòu)體則只有 IDName、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é)果分別掃描到 mapslice 中。

使用示例如下:

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ī)則不匹配,所以我才顯式的為 CreatedAtUpdatedAt 兩個(gè)字段指明了 db 標(biāo)簽,這樣 sqlxrows.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é)果掃描到 mapslice。

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 Copier入門到入坑探究

    Golang Copier入門到入坑探究

    這篇文章主要為大家介紹了Golang Copier入門到入坑探究,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-11-11
  • golang將切片或數(shù)組根據(jù)某個(gè)字段進(jìn)行分組操作

    golang將切片或數(shù)組根據(jù)某個(gè)字段進(jìn)行分組操作

    這篇文章主要介紹了golang將切片或數(shù)組根據(jù)某個(gè)字段進(jìn)行分組操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧
    2020-12-12
  • Golang開發(fā)之接口的具體使用詳解

    Golang開發(fā)之接口的具體使用詳解

    在 Golang 中,接口是一種類型,它是由一組方法簽名組成的抽象集合。這篇文章主要為大家介紹了Golang接口的具體使用,希望對(duì)大家有所幫助
    2023-04-04
  • go語(yǔ)言中布隆過濾器低空間成本判斷元素是否存在方式

    go語(yǔ)言中布隆過濾器低空間成本判斷元素是否存在方式

    這篇文章主要為大家介紹了go語(yǔ)言中布隆過濾器低空間成本判斷元素是否存在方式詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-09-09
  • 一文帶你輕松理解Go中的內(nèi)存逃逸問題

    一文帶你輕松理解Go中的內(nèi)存逃逸問題

    這篇文章主要給大家介紹Go中的內(nèi)存逃逸問題,文中通過代碼示例講解的非常詳細(xì),對(duì)我們的學(xué)習(xí)或工作有一定的參考價(jià)值,感興趣的同學(xué)可以跟著小編一起來學(xué)習(xí)
    2023-06-06
  • Go實(shí)現(xiàn)map并發(fā)安全的3種方式總結(jié)

    Go實(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-10
  • 詳解go語(yǔ)言json的使用技巧

    詳解go語(yǔ)言json的使用技巧

    這篇文章主要介紹了詳解go語(yǔ)言json的使用技巧,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2021-03-03
  • 15個(gè)Golang中時(shí)間處理的實(shí)用函數(shù)

    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ǔ)言開源庫(kù)實(shí)現(xiàn)Onvif協(xié)議客戶端設(shè)備搜索

    這篇文章主要為大家介紹了Go語(yǔ)言O(shè)nvif協(xié)議客戶端設(shè)備搜索示例實(shí)現(xiàn),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-04-04
  • Go語(yǔ)言struct類型詳解

    Go語(yǔ)言struct類型詳解

    這篇文章主要介紹了Go語(yǔ)言struct類型詳解,struct是一種數(shù)據(jù)類型,可以用來定義自己想的數(shù)據(jù)類型,需要的朋友可以參考下
    2014-10-10

最新評(píng)論