sqlx 是 Go 言语中一个流行的第三方包,它供给了对 Go 规范库 database/sql 的扩展,旨在简化和改进 Go 言语中运用 SQL 的体验,并供给了愈加强大的数据库交互功用。sqlx 保留了 database/sql 接口不变,是 database/sql 的超集,这使得将现有项目中运用的 database/sql 替换为 sqlx 变得相当轻松。

本文重点解说 sqlxdatabase/sql 基础上扩展的功用,关于 database/sql 已经支撑的功用则不会详细解说。假如你对 database/sql 不熟悉,可以检查我的另一篇文章《在 Go 中怎么运用 database/sql 来操作数据库》。

装置

sqlx 装置办法同 Go 言语中其他第三方包相同:

$ go get github.com/jmoiron/sqlx

sqlx 类型规划

sqlx 的规划与 database/sql 不同不大,编码风格较为一致,参阅 database/sql 规范库,sqlx 供给了如下几种与之对应的数据类型:

  • sqlx.DB:相似于 sql.DB,表明数据库目标,可以用来操作数据库。
  • sqlx.Tx:相似于 sql.Tx,业务目标。
  • sqlx.Stmt:相似于 sql.Stmt,预处理 SQL 句子。
  • sqlx.NamedStmt:对 sqlx.Stmt 的封装,支撑签字参数。
  • sqlx.Rows:相似于 sql.Rowssqlx.Queryx 的回来成果。
  • sqlx.Row:相似于 sql.Rowsqlx.QueryRowx 的回来成果。

以上类型与 database/sql 供给的对应类型在功用上差异不大,但 sqlx 为这些类型供给了更友好的办法。

预备

为了演示 sqlx 用法,我预备了如下 MySQL 数据库表:

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 指令行或图形化工具创立这张表。

衔接数据库

运用 sqlx 衔接数据库:

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 衔接数据库
	db, err = sqlx.Open("mysql", dsn)
	if err != nil {
		log.Fatal(err)
	}
	// 2. 运用 sqlx.Open 变体办法 sqlx.MustOpen 衔接数据库,假如出现过错直接 panic
	db = sqlx.MustOpen("mysql", dsn)
	// 3. 假如已经有了 *sql.DB 目标,则可以运用 sqlx.NewDb 衔接数据库,得到 *sqlx.DB 目标
	sqlDB, err := sql.Open("mysql", dsn)
	if err != nil {
		log.Fatal(err)
	}
	db = sqlx.NewDb(sqlDB, "mysql")
	// 4. 运用 sqlx.Connect 衔接数据库,等价于 sqlx.Open + db.Ping
	db, err = sqlx.Connect("mysql", dsn)
	if err != nil {
		log.Fatal(err)
	}
	// 5. 运用 sqlx.Connect 变体办法 sqlx.MustConnect 衔接数据库,假如出现过错直接 panic
	db = sqlx.MustConnect("mysql", dsn)
}

sqlx 中咱们可以经过以上 5 种办法衔接数据库。

sqlx.Open 对标 sql.Open 办法,回来 *sqlx.DB 类型。

sqlx.MustOpensqlx.Open 相同会回来 *sqlx.DB 实例,但假如遇到过错则会 panic

sqlx.NewDb 支撑从一个 database/sql 包的 *sql.DB 目标创立一个新的 *sqlx.DB 类型,而且需求指定驱动称号。

运用前 3 种办法衔接数据库并不会当即与数据库树立衔接,衔接将会在合适的时候延迟树立。为了保证可以正常衔接数据库,往往需求调用 db.Ping() 办法进行验证:

ctx := context.Background()
if err := db.PingContext(ctx); err != nil {
	log.Fatal(err)
}

sqlx 供给的 sqlx.Connect 办法便是用来简化这一操作的,它等价于 sqlx.Open + db.Ping 两个办法,其界说如下:

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 办法的基础上,供给了遇到过错当即 panic 的功用。看到 sqlx.MustConnect 办法的界说你就理解了:

func MustConnect(driverName, dataSourceName string) *DB {
	db, err := Connect(driverName, dataSourceName)
	if err != nil {
		panic(err)
	}
	return db
}

以后当你遇见 MustXxx 相似办法名时就应该想到,其功用往往等价于 Xxx 办法,不过在其内部完成中,遇到 error 不再回来,而是直接进行 panic,这也是 Go 言语很多库中的惯用办法。

声明模型

咱们界说一个 User 结构体来映射数据库中的 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 结构体在这儿可以被称为「模型」。

履行 SQL 指令

database/sql 包供给了 *sql.DB.Exec 办法来履行一条 SQL 指令,sqlx 对其进行了扩展,供给了 *sqlx.DB.MustExec 办法来履行一条 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 办法也会在遇到过错时直接 panic,其内部调用的是 *sqlx.DB.Exec 办法。

履行 SQL 查询

database/sql 包供给了 *sql.DB.Query*sql.DB.QueryRow 两个查询办法,其签名如下:

func (db *DB) Query(query string, args ...any) (*Rows, error)
func (db *DB) QueryRow(query string, args ...any) *Row

sqlx 在这两个办法的基础上,扩展出如下两个办法:

func (db *DB) Queryx(query string, args ...interface{}) (*Rows, error)
func (db *DB) QueryRowx(query string, args ...interface{}) *Row

这两个办法回来的类型正是前文 sqlx 类型规划 中提到的 sqlx.Rowssqlx.Row 类型。

下面来解说下这两个办法怎么运用。

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 供给了快捷办法可以将查询成果直接扫描到结构体
		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 得到了扩展,其供给的 StructScan 办法可以便利的将查询成果直接扫描到 User 结构体,这极大的增加了便携性,咱们再也不必像运用 *sql.Rows 供给的 Scan 办法那样挨个写出 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 办法将查询成果扫描到结构体。

另外,这儿运用了链式调用的办法,在调用 db.QueryRowx() 之后直接调用了 .StructScan(&u),接纳的 errStructScan 的回来成果。这是因为 db.QueryRowx() 的回来成果 *sqlx.Row 中记载了过错信息 err,假如查询阶段遇到过错会被记载到 *sqlx.Row.err 中。在调用 StructScan 办法阶段,其内部首先判断 r.err != nil,假如存在 err 直接回来过错,没有过错则将查询成果扫描到 dest 参数接纳到的结构体指针,代码完成如下:

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 不仅扩展了 *sql.DB.Query*sql.DB.QueryRow 两个查询办法,它还新增了两个查询办法:

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 办法,用以简化查询单条记载。

*sqlx.DB.Select 办法包装了 *sqlx.DB.Queryx 办法,用以简化查询多条记载。

接下来解说这两个办法怎么运用。

Get

运用 *sqlx.DB.Get 办法查询记载如下:

func GetUser(db *sqlx.DB, id int) (User, error) {
	var u User
	// 查询记载扫描数据到 struct
	err := db.Get(&u, "SELECT * FROM user WHERE id = ?", id)
	return u, err
}

可以发现 *sqlx.DB.Get 办法用起来非常简略,咱们不再需求调用 StructScan 办法将查询成果扫描到结构体中,只需求将结构体指针当作 Get 办法的第一个参数传递进去即可。

其代码完成如下:

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)
}

依据源码可以看出,*sqlx.DB.Get 内部调用了 *sqlx.DB.QueryRowx 办法。

Select

运用 *sqlx.DB.Select 办法查询记载如下:

func SelectUsers(db *sqlx.DB) ([]User, error) {
	var us []User
	// 查询记载扫描数据到 slice
	err := db.Select(&us, "SELECT * FROM user")
	return us, err
}

可以发现 *sqlx.DB.Select 办法用起来相同非常简略,它可以直接将查询成果扫描到 []User 切片中。

其代码完成如下:

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)
}

依据源码可以看出,*sqlx.DB.Select 内部调用了 *sqlx.DB.Queryx 办法。

sqlx.In

database/sql 中假如想要履行 SQL IN 查询,由于 IN 查询参数长度不固定,咱们不得不运用 fmt.Sprintf 来动态拼接 SQL 句子,以保证 SQL 中参数占位符的个数是正确的。

sqlx 供给了 In 办法来支撑 SQL IN 查询,这极大的简化了代码,也使得代码更易维护和安全。

运用示例如下:

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
}

调用 sqlx.In 并传递 SQL 句子以及切片类型的参数,它将回来新的查询 SQL query 以及参数 args,这个 query 将会依据 ids 来动态调整。

比方咱们传递 ids[]int64{1, 2, 3},则得到 querySELECT * FROM user WHERE id IN (?, ?, ?)

注意,咱们接下来又调用 db.Rebind(query) 从头绑定了 query 变量的参数占位符。假如你运用 MySQL 数据库,这不是必须的,因为咱们运用的 MySQL 驱动程序参数占位符便是 ?。而假如你运用 PostgreSQL 数据库,由于 PostgreSQL 驱动程序参数占位符是 $n,这时就必须要调用 db.Rebind(query) 办法来转换参数占位符了。

它会将 SELECT * FROM user WHERE id IN (?, ?, ?) 中的参数占位符转换为 PostgreSQL 驱动程序可以辨认的参数占位符 SELECT * FROM user WHERE id IN ($1, $2, $3)

之后的代码就跟运用 database/sql 查询记载没什么两样了。

运用签字参数

sqlx 供给了两个办法 NamedExecNamedQuery,它们可以支撑签字参数 :name,这样就不必再运用 ? 这种占位符的方式了。

这两个办法签名如下:

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 的办法来命名参数,它可以匹配 mapstruct 对应字段的参数值,这样的 SQL 句子可读性更强。

业务

在业务的支撑上,sqlx 扩展出了 Must 版别的业务,运用示例如下:

func MustTransaction(db *sqlx.DB) error {
	tx := db.MustBegin()
	tx.MustExec("UPDATE user SET age = 25 WHERE id = ?", 1)
	return tx.Commit()
}

不过这种用法不多,你知道就行。以下是业务的引荐用法:


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 句子来处理业务的回滚操作,这样就不必在每次处理过错时重复的编写调用 tx.Rollback() 的代码。

假如代码正常履行到最后,经过 tx.Commit() 来提交业务,此时即使再调用 tx.Rollback() 也不会对成果产生影响。

预处理句子

sqlx 针对 *sql.DB.Prepare 扩展出了 *sqlx.DB.Preparex 办法,回来 *sqlx.Stmt 类型。

*sqlx.Stmt 类型支撑 QueryxQueryRowxGetSelect 这些 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
}

实际上 *sqlx.DB.Preparex 内部还是调用的 *sql.DB.Preapre 办法,只不过将其回来成果构造成 *sqlx.Stmt 类型并回来。

不安全的扫描

在运用 *sqlx.DB.Get 等办法查询记载时,假如 SQL 句子查询出来的字段与要绑定的模型特点不匹配,则会报错。

示例如下:

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 句子中查询了 idnameemailage 4 个字段,而 user 结构体则只要 IDNameEmail 3 个特点,由于无法一一对应,履行以上代码,咱们将得到如下报错信息:

missing destination name age in *struct { ID int; Name string; Email string }

这种表现是合理的,契合 Go 言语的编程风格,尽早暴露过错有助于减少代码存在 BUG 的隐患。

不过,有些时候,咱们便是为了便利想要让上面的示例代码可以运行,可以这样做:

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 目标,然后再调用它的 Get 办法。

*sqlx.DB 界说如下:

type DB struct {
	*sql.DB
	driverName string
	unsafe     bool
	Mapper     *reflectx.Mapper
}

unsafe 特点为 true 时,*sqlx.DB 目标会疏忽不匹配的字段,使代码可以正常运行,并将可以匹配的字段正确绑定到 user 结构体目标上。

经过这个特点的称号咱们就知道,这是不安全的做法,不被引荐。

与未运用的变量相同,被疏忽的列是对网络和数据库资源的糟蹋,而且这很简单导致出现模型与数据库表不匹配而不被感知的状况。

Scan 变体

前文示例中,咱们见过了 *sqlx.Rows.Scan 的变体 *sqlx.Rows.StructScan 的用法,它可以便利的将查询成果扫描到 struct 中。

sqlx 还供给了 *sqlx.Rows.MapScan*sqlx.Rows.SliceScan 两个办法,可以将查询成果别离扫描到 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) 用法相似,都是将接纳查询成果集的目标模型指针变量当作参数传递进来。

rows.SliceScan() 用法略有不同,它不接纳参数,而是将成果保存在 []interface{} 中并回来。

可以按需运用以上两个办法。

操控字段称号映射

讲到这儿,想必不少同学心里可能存在一个疑问,rows.StructScan(&u) 在将查询记载的字段映射到对应结构体特点时,是怎么找到对应联系的呢?

答案便是 db 结构体标签。

回顾前文讲 声明模型 时,User 结构体中界说的 CreatedAtUpdatedAt 两个字段,界说如下:

CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`

这儿显式的标明晰结构体标签 dbsqlx 正是运用 db 标签来映射查询字段和模型特点。

默许状况下,结构体字段会被映射成全小写方式,如 ID 字段会被映射为 id,而 CreatedAt 字段会被映射为 createdat

因为在 user 数据库表中,创立时刻和更新时刻两个字段别离为 created_atupdated_at,与 sqlx 默许字段映射规则不匹配,所以我才显式的为 CreatedAtUpdatedAt 两个字段指明晰 db 标签,这样 sqlxrows.StructScan 就能正常工作了。

当然,数据库字段不一定都是小写,假如你的数据库字段为全大写,sqlx 供给了 *sqlx.DB.MapperFunc 办法来操控查询字段和模型特点的映射联系。

其运用示例如下:

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
}

这儿为了不改变原有的 db 目标,咱们复制了一个 copyDB,调用 copyDB.MapperFunc 并将 strings.ToUpper 传递进来。

注意这儿的查询句子中,查询字段全部经过 as 从头命名成了大写方式,而 User 模型字段 db 默许都为小写方式。

copyDB.MapperFunc(strings.ToUpper) 的作用,便是在调用 Get 办法将查询成果扫描到结构体时,把 User 模型的小写字段,经过 strings.ToUpper 办法转成大写,这样查询字段和模型特点就全为大写了,也就可以一一匹配上了。

还有一种状况,假如你的模型已存在 json 标签,而且不想重复的再抄一遍到 db 标签,咱们可以直接运用 json 标签来映射查询字段和模型特点。

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) 将模型映射的标签由 db 改为 json,并经过 strings.ToLower 办法转换为小写。

reflectx 依照如下办法导入:

import "github.com/jmoiron/sqlx/reflectx"

现在,查询句子中 name 特点经过运用 as 被重命名为 username,而 username 刚好与 User 模型中 Name 字段的 json 标签相对应:

Name     sql.NullString `json:"username"`

所以,以上示例代码可以正确映射查询字段和模型特点。

总结

sqlx 树立在 database/sql 包之上,用于简化和增强与联系型数据库的交互操作。

对常见数据库操作办法,sqlx 供给了 Must 版别,如 sqlx.MustOpen 用来衔接数据库,*sqlx.DB.MustExec 用来履行 SQL 句子,当遇到 error 时将会直接 panic

sqlx 还扩展了查询办法 *sqlx.DB.Queryx*sqlx.DB.QueryRowx*sqlx.DB.Get*sqlx.DB.Select,而且这些查询办法支撑直接将查询成果扫描到结构体。

sqlx 为 SQL IN 操作供给了快捷办法 sqlx.In

为了使 SQL 更易阅读,sqlx 供给了 *sqlx.DB.NamedExec*sqlx.DB.NamedQuery 两个办法支撑签字参数。

调用 *sqlx.DB.Unsafe() 办法可以获取 unsafe 特点为 true*sqlx.DB 目标,在将查询成果扫描到结构体使可以用来疏忽不匹配的记载字段。

除了可以将查询成果扫描到 structsqlx 还支撑将查询成果扫描到 mapslice

sqlx 运用 db 结构体标签来映射查询字段和模型特点,假如不显式指定 db 标签,默许映射的模型特点为小写方式,可以经过 *sqlx.DB.MapperFunc 函数来修改默许行为。

本文完好代码示例我放在了 GitHub 上,欢迎点击检查。

期望此文能对你有所帮助。

联系我

  • 微信:jianghushinian
  • 邮箱:jianghushinian007@outlook.com
  • 博客地址:jianghushinian.cn/

参阅

  • sqlx 源码:github.com/jmoiron/sql…
  • sqlx 文档:pkg.go.dev/github.com/…
  • sqlx 官网:jmoiron.github.io/sqlx/
  • 本文示例代码:github.com/jianghushin…