sqly - database/sql 的扩展

摘要

本文主要介绍 sqly 是一款基于 golang 官方的 database/sql 包的扩展,其主要目标是简化官方包的查询操作和事务操作等,其设计思想参考了python 轻量级 MySQLdb 封装模块 torndb

背景

最初有编写 sqly 这个想法,是笔者在自学 go 语言过程中发现 database/sql 这个包对于用惯了 torndb 和 Django ORM 的笔者来说实在是太不友好了。
本着对 torndb 这个包的欣赏,笔者决定用 golang 来模仿一个类似的包, 同时也能像 golang 中的 json 包一样像 json.Unmarshal 方法一样方便 将sql 查询结果反射成 struct 对象。

原理

开始介绍 sqly 之前我们先来看一下采用 Golang 官方的 database/sql 包如何实现数据的查询和响应 go 对象(struct) 的反序列化。

例1

1
2
3
4
5
6
7
8
9
10
type user struct {
ID int64 `json:"id"`
Name string `json:"name"`
Gender int8 `json:"gender"`
}

db, err := sql.Open("mysql", "addr")
u := user{}
rows := db.QueryRow("SELECT `id`, `name`, `gender` FROM `user` WHERE `id`=?", 1)
rows.Scan(&u.ID, &u.Name, &u.Sex)

从例1中,我们可以看出,采用官方包的查询需要将 user 对象中的各个属性字段和查询语句中对应的字段一一对应起来,例如: u.ID 对应 id字段,u.Name 对应 name 字段,假如 user 表字段比较多的情况下,字段的一一对应在开发过程中将会是很大的一分部工作量,而且很容易出现错误。 更有甚者,在采用 SELECT * 语句的情况下,这将更是一种灾难了。

有没有一种方案能够自动完成 struct 中属性字段和数据库中字段的一一映射,实现从数据库数据到 go 对象的反向序列化。此时我们可以联想到 Golang 的反射 reflect 就可以解决这个问题,当然由于反射效率较低,执行速度肯定会比原生的方式慢。 如 例2。

例2,一个简单的根据reflect 方法来实现 struct 对象反序列化例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
func ToStruct(rows *sql.Rows, to interface{}) error {
v := reflect.ValueOf(to)
if v.Elem().Type().Kind() != reflect.Sturct {
return errors.New("Expect a struct")
}
dist := []interface{}{}
cols, err := rows.Columns()
addrMap := map[string]interface{} // 用于承载赋值指针
for i := 0; i < v.Elem().NumField(); i++ {
// 反射出各个字段的赋值地址
val := v.Elem().Field(i)
col := v.Elem().Type().Field(i).Tag.Get("sql")
addrMap[col] = val.Addr().Interface()
}
for _, col := range cols {
// 将struct 中各个字段的地址与 sql 结果中的各个字段一一对应
dist = append(dist, addrMap[col])
}
return rows.Scan(scan_dest...)
}
func main() {
type user struct {
ID int64 `json:"id" sql:"id"` //sql tag 就是反射时候去的值,与数据库列名一致
Name string `json:"name" sql:"name"`
Gender int8 `json:"gender" sql:"gender`
}
db, err := sql.Open("mysql", "addr")
rows, err := db.Query("SELECT `id`, `name`, `gender` FROM `user` WHERE `id`=?", 1)
if err != nil {
panic(err)
}
for rows.Next() {
u := &user{}
ToStruct(rows, u)
fmt.Println(u.ID, u.Name, u.Gender)
}
}

本文的主角 sqly 核心原理就基于此

通过反射实现 sql 查询结果到 struct 对象反序列化的库还有 sqlx, go-sqlbuilder 等。
大家是否发现 sqly 是否和 sqlx 有一定渊源,在笔者开发 sqly 过程中偶然发现了 sqlx 这包,经研究发现其实现的功能和我要开发的功能也有一定类似,而且恰好笔者姓名拼音中有较多的 “y”, 就愉快地命名为 sqly。

功能特点

  • 简化 database/sql 原生的查询结果到 struct 的赋值方式,可以像 json.Unmarshal 一样方便得将数据库查询结果反射成 golang 友好的数据类型。

  • 抽象出了 Query, Get, Update, Insert, Delete 方法和保留了原有的 Exec 方法

  • 支持事务操作的封装,通过闭包函数封装了开启事务,提交事务的所有过程

  • 支持胶囊操作,屏蔽事务操作和非事务操作在语句编写上的差异,避免事务和非事务数据库连接混用的问题

sqly 使用

安装依赖

go get github.com/FeifeiyuM/sqly

在 go.mod 中添加 github.com/FeifeiyuM/sqly latest

连接数据库

func New(opt sqly.Option) (SqlY, error)
例3,数据库连接

1
2
3
4
5
6
7
8
9
10
11
12
   opt := &sqly.Option{
Dsn: "test:mysql123@tcp(127.0.0.1:3306)/test_db?charset=utf8mb4&collation=utf8mb4_unicode_ci&parseTime=true&loc=Local",
DriverName: "mysql",
MaxIdleConns: 0,
MaxOpenConns: 0,
ConnMaxLifeTime: 0,
}
db, err := sqly.New(opt)
if err != nil {
fmt.Println("test error")
}
...

配置属性字段说明:

  • Dsn: 格式化的数据库服务访问参数 例如:mysql 格式化方式如下 [username[:password]@][protocol[(address)]]/dbname[?param1=value1&…&paramN=valueN]
  • DriverName: 使用的数据库驱动类型 例如: mysql, postgres, sqlite3 等,(在使用 sqly 时需要引入具体驱动包)
  • MaxIdleConns: 最大空闲连接数
  • MaxOpenConns: 最大连接池大小
  • ConnMaxLifeTime: 连接的生命周期

tips: 如果要使用 time.Time 的字段类型, 连接数据库的 dsn 配置中加上 parseTime=true

详细配置可以查看 【Go database/sql tutorial](http://go-database-sql.org/connection-pool.html), go-sql-driver/mysql 等。

非事务操作

通用执行操作,执行任意一条sql语句(插入,更新,删除,建表等),查询语句可以执行,但不返回结果

func (s SqlY) Exec(query string, args …interface{}) (Affected, error)
func (s SqlY) ExecCtx(ctx context.Context, query string, args …interface{}) (Affected, error)

例4,使用 Exec 方法建表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
query := "CREATE TABLE `account` (" +
"`id` int(10) unsigned NOT NULL AUTO_INCREMENT," +
"`nickname` varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL," +
"`avatar` varchar(200) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'avatar url'," +
"`mobile` varchar(16) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'mobile number'," +
"`email` varchar(320) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT 'email'," +
"`password` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT 'password'," +
"`role` tinyint(4) DEFAULT NULL COMMENT 'role'," +
"`expire_time` datetime DEFAULT NULL COMMENT 'expire_time'," +
"`is_valid` tinyint(4) DEFAULT NULL COMMENT 'is_valid'," +
"`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP," +
"PRIMARY KEY (`id`)," +
"UNIQUE KEY `mobile_index` (`mobile`)," +
"KEY `email_index` (`email`)" +
") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;"
aff, err = db.Exec(query)
if err != nil {
fmt.Println("create table error")
}
fmt.Println(aff)

affected 对象介绍

1
2
3
4
type Affected struct {
LastId int64 // 影响的最后一条记录id
RowsAffected // 总共影响的行数
}

插入一条数据

func (s SqlY) Insert(query string, args …interface{}) (Affected, error)
func (s SqlY) InsertCtx(ctx context.Context, query string, args …interface{}) (Affected, error)

例5, 插入一条数据

1
2
3
4
5
6
7
8
9
query := "INSERT INTO `account` (`nickname`, `mobile`, `email`, `role`) VALUES (?, ?, ?, ?);"

aff, err := db.Insert(query, "nick_test3", "18812311235", "test@foxmail.com", 1)
if err != nil {
fmt.Println("failed to insert data")
}
if aff != nil {
fmt.Printf("auto_id: %v, affected_rows: %v\n", aff.LastId, aff.RowsAffected)
}

插入多条数据

func (s SqlY) InsertMany(query string, args [][]interface{}) (Affected, error)
func (s SqlY) InsertManyCtx(ctx context.Context, query string, args [][]interface{}) (Affected, error)

例6,插入多条数据

1
2
3
4
5
6
7
8
9
10
11
12
13
query := "INSERT INTO `account` (`nickname`, `mobile`, `email`, `role`) VALUES (?, ?, ?, ?);"
var vals = [][]interface{}{
{"testq1", "18112342345", "testq1@foxmail.com", 1},
{"testq2", "18112342346", "testq2@foxmail.com", nil},
}
aff, err = db.InsertMany(query, vals)
if err != nil {
fmt.Sprintln("create account error")
}
if err != nil {
fmt.Sprintln("create accounts error")
}
fmt.Println(aff)

更新一条数据

func (s SqlY) Update(query string, args …interface{}) (Affected, error)
func (s SqlY) ExecCtx(ctx context.Context, query string, args …interface{}) (Affected, error)

例7,更新一条数据

1
2
3
4
5
6
query := "UPDATE `account` SET `nickname`=? WHERE `mobile`=?;"
aff, err := db.Update(query, "lucy", "18812311231")
if err != nil {
fmt.Println("update accounts error")
}
fmt.Println(aff)

执行多条更新语句(其本质是将多条update语句以封号隔开后一次性提交)

func (s SqlY) UpdateMany(query string, args [][]interface{}) (Affected, error)
func (s SqlY) UpdateManyCtx(ctx context.Context, query string, args [][]interface{}) (Affected, error)

例8,多条更新语句

1
2
3
4
5
6
7
8
9
10
11
12
13
query := "UPDATE `account` SET `password`=? WHERE `id`=?"  
var params [][]interface{}
for _, id := range ids {
hash := sha1.New()
_, _ = hash.Write([]byte(strconv.FormatInt(id.ID, 10)))
passwd := hex.EncodeToString(hash.Sum(nil))
params = append(params, []interface{}{passwd, id.ID})
}
aff, err := db.UpdateMany(query, params)
if err != nil {
fmt.Println("update accounts error")
}
fmt.Println(aff)

删除操作

func (s SqlY) Delete(query string, args …interface{}) (Affected, error)
func (s SqlY) DeleteCtx(ctx context.Context, query string, args …interface{}) (Affected, error)

例9,删除操作

1
2
3
4
5
6
query := "DELETE FROM `account` WHERE `mobile`=?;"
aff, err := db.Delete(query, "18812311231")
if err != nil {
fmt.Sprintln("delete account error")
}
fmt.Println(aff)

查询一条数据 (dest 传值是指针类型)

func (s SqlY) Get(dest interface{}, query string, args …interface{}) error
func (s
SqlY) GetCtx(ctx context.Context, dest interface{}, query string, args …interface{}) error

例10,查询单条数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// account 对象定义, 
// sql tag 对应的就是 例4中对应的 account 表各个字段名
type Account struct {
ID int64 `sql:"id" json:"id"`
Nickname string `sql:"nickname" json:"nickname"`
Avatar sqly.NullString `sql:"avatar" json:"avatar"`
Email string `sql:"email" json:"email"`
Mobile string `sql:"mobile" json:"mobile"`
Role sqly.NullInt32 `sql:"role" json:"role"`
Password string `sql:"password" json:"password"`
ExpireTime sqly.NullTime `sql:"expire_time" json:"expire_time"`
IsValid sqly.NullBool `sql:"is_valid" json:"is_valid"`
CreateTime time.Time `sql:"create_time" json:"create_time"`
}
// 先实例化 Account
acc := new(Account)
// 查询语句
query := "SELECT * FROM `account` WHERE `mobile`=?"
// Get 函数能将查询结果自动反射至 acc 对象
err := db.Get(acc, query, "18812311235")
if err != nil {
fmt.Println("query account error")
}
accStr, err := json.Marshal(acc1)
if err != nil {
fmt.Println("marshal acc error")
}
fmt.Println(accStr)

查询数据 (dest 必须为数组指针)

func (s SqlY) Query(dest interface{}, query string, args …interface{}) error
func (s
SqlY) QueryCtx(ctx context.Context, dest interface{}, query string, args …interface{}) error

例11,查询多条数据

1
2
3
4
5
6
7
8
9
10
11
var accs []*Account // Account 为例10 定义的 Account 结构体

query := "SELECT * FROM `account` WHERE `mobile` IN ?"
var mobiles = []string{"18812311235", "18112342346"}

err := db.Query(&accs, query, mobiles)
if err != nil {
fmt.Printf("query accounts error")
}
accsStr, _ := json.Marshal(accs)
fmt.Println("marshal acc error")

事务操作

sqly 的事务封装和非事务封装方法基本类似,其最主要的特点是支持将事务操作封装成回调函数,在闭包内执行,屏蔽事务开启,提交,回滚等一系列操作。

开启事务

func (s SqlY) NewTrans() (Trans, error)

事务提交

func (t *Trans) Commit() error

事务回滚

func (t *Trans) Rollback()

事务通用执行

func (t Trans) Exec(query string, args …interface{}) (Affected, error)
func (t Trans) ExecCtx(ctx context.Context, query string, args …interface{}) (Affected, error)

事务插入

func (t Trans) Insert(query string, args …interface{}) (Affected, error)
func (t Trans) InsertCtx(ctx context.Context, query string, args …interface{}) (Affected, error)

事务插入多条

func (t Trans) InsertMany(query string, args [][]interface{}) (Affected, error)
func (t Trans) InsertManyCtx(ctx context.Context, query string, args [][]interface{}) (Affected, error)

事务更新

func (t Trans) Update(query string, args …interface{}) (Affected, error)
func (t Trans) UpdateCtx(ctx context.Context, query string, args …interface{}) (Affected, error)

事务更新多条

func (t Trans) UpdateMany(query string, args [][]interface{}) (Affected, error)
func (t Trans) UpdateManyCtx(ctx context.Context, query string, args [][]interface{}) (Affected, error)

事务删除

func (t Trans) Delete(query string, args …interface{}) (Affected, error)
func (t Trans) DeleteCtx(ctx context.Context, query string, args …interface{}) (Affected, error)

事务查询单条数据

func (t Trans) Get(dest interface{}, query string, args …interface{}) error
func (t
Trans) GetCtx(ctx context.Context, dest interface{}, query string, args …interface{}) error

事务查询

func (t Trans) Query(dest interface{}, query string, args …interface{}) error
func (t
Trans) QueryCtx(ctx context.Context, dest interface{}, query string, args …interface{}) error

例12,事务操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
db, err := sqly.New(opt)
if err != nil {
fmt.Println("test error")
}
// 开始事务
tx, err := sqly.NewTrans()
if err != nil {
fmt.Printf("failed to begin transaction")
return
}
// 回滚
defer func() {
_ = tx.Rollback()
}()
// 执行事务
// 查询
acc := new(Account)
query = "SELECT * FROM `account` WHERE `mobile`=?"
ctx := context.TODO()
err = tx.GetCtx(ctx, acc, query, "18812311235")
if err != nil {
fmt.Printf("get accout error")
return
}
// 更新
query = "UPDATE `account` SET `is_valid`=? WHERE id=?"
aff, err := tx.UpdateCtx(ctx, query, true, acc.ID)
if err != nil {
fmt.Println("update account error")
}
fmt.Println(aff)
// 删除
query = "DELETE FROM `account` WHERE id!=?"
_, err = tx.DeleteCtx(ctx, query, acc.ID)
if err != nil {
fmt.Println("delete accounts error")
}
// 插入
query = "INSERT INTO `account` (`nickname`, `mobile`, `email`, `role`) VALUES (?, ?, ?, ?);"
aff, err = tx.InsertCtx(ctx, query, "nick_ruby", "13565656789", nil)
if err != nil {
fmt.Println("insert account error")
}
fmt.Println(aff)
// 提交
_ = tx.Commit()

从例12 事务操作中可以看出,1、事务操作函数 query, delete, update, insert, exec 用法和非事务操作一致;2、在开发过程中需要显式初始化事务,提交事务,回滚事务。

为了简化事务操作,sqly 通过回调函数的方式封装了事务初始化,提交,回滚的相关操作,因而不需要每次在一个事务中都编写这部分逻辑

事务封装

type TxFunc func(tx Trans) (interface{}, error) // 回调函数,业务逻辑都在回调函数内执行
func (s
SqlY) Transaction(txFunc TxFunc) (interface{}, error)

例13,事务封装操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
db, err := sqly.New(opt)
if err != nil {
fmt.Println("test error")
}
// 通过 Transaction 进入事务操作
// 如果返回的 err 不为空,会执行回滚操作
res, err := db.Transaction(func(tx *sqly.Trans) (interface{}, error) {
ctx := context.TODO()
// 查询
acc := new(Account)
query = "SELECT * FROM `account` WHERE `mobile`=?"
err = tx.GetCtx(ctx, acc, query, "18812311235")
if err != nil {
fmt.Printf("get account error")
return
}
// 更新
query = "UPDATE `account` SET `is_valid`=? WHERE id=?"
aff, err := tx.UpdateCtx(ctx, query, true, acc.ID)
if err != nil {
fmt.Println("update account error")
}
fmt.Println(aff)
// 删除
query = "DELETE FROM `account` WHERE id!=?"
_, err = tx.DeleteCtx(ctx, query, acc.ID)
if err != nil {
fmt.Println("delete accounts error")
}
// 插入
query = "INSERT INTO `account` (`nickname`, `mobile`, `email`, `role`) VALUES (?, ?, ?, ?);"
aff, err = tx.InsertCtx(ctx, query, "nick_ruby", "13565656789", nil)
if err != nil {
fmt.Println("insert account error")
}
fmt.Println(aff)
})
if err := nil {
fmt.Println("do transaction error")
}
fmt.Println(res)

胶囊操作

从例13中可以看出,在执行事务操作的时候,事务句柄 tx 需要显式传递,有时候常常不注意会将事务和非事务操作混用。 严重时会导致数据库死锁的发生,下面聊聊笔者真实线上发生一次事故。

例14,事务和非事务混用的死锁案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 开始数据库连接时,允许最大连接数为 3
opt := &sqly.Option{
Dsn: "test:mysql123@tcp(127.0.0.1:3306)/test_db?charset=utf8mb4&collation=utf8mb4_unicode_ci&parseTime=true&loc=Local",
DriverName: "mysql",
MaxIdleConns: 0,
MaxOpenConns: 2,
ConnMaxLifeTime: 0,
}
db, err := sqly.New(opt)
if err != nil {
fmt.Println("test error")
}

func updateAccount(mobile string) (interface, error) {
return db.Transaction(func(tx *sqly.Trans) (interface{}, error) {
ctx := context.TODO()
// 查询
acc := new(Account)
query = "SELECT * FROM `account` WHERE `mobile`=?"
// 此处采用非事务查询
err = db.GetCtx(ctx, acc, query, "18812311235")
if err != nil {
return nil, err
}
// 更新
query = "UPDATE `account` SET `is_valid`=? WHERE id=?"
aff, err := tx.UpdateCtx(ctx, query, true, acc.ID)
if err != nil {
return nil, err
}
return aff, nil
})
}

查询连接数分析:
1、创建数据库连接的时候,定义了最大连接池大小为 2。
2、在 updateAccount 方法中,在执行 db.Transaction() 的时候会申请一个数据库连接(用于事务的连接),并且该连接会保持至函数执行结束后释放。
3、在回调函数内,db.GetCtx() 由于没有使用事务句柄,会重新申请一个数据库连接,查询执行完后立即释放。
在函数 updateAccount 没有并发的情况下,可以正常执行。如果存在并发调用 updateAccount 函数时,db.Transaction() 执行的时候就会将连接数耗尽,当代码执行至 db.GetCtx()方法时会一直等待获取连接池中释放的连接,GetCtx() 的等待又会造成 db.Transaction() 函数一直无法结束,无法释放其申请的连接,从而造成了一个典型的死锁。

为了解决开发中由于编写代码时的疏忽而造成类似的问题, sqly 封装了胶囊操作,尽可能屏蔽了事务操作和非事务操作的差异。只要开启了事务,所有查询都只能使用事务的 Query()等方法,非事务操作采用非事务的 Query() 等方法,而且不需要显式得再函数直接传递事务句柄 tx。

胶囊操作原理

从例13,中可以观察到,在执行查询,更新,插入,删除等方法的时候,除了变量 tx 句柄,还有个 go 的 上下文 context 参数 ctx。ctx 可以用于携带上下文信息,利用 ctx 这个特点,我们可以将是否开启事务和事务句柄等信息封装入 ctx 中传递。

胶囊操作方法

初始化胶囊句柄

func NewCapsule(sqlY SqlY) Capsule

胶囊封装(用法和事务封装 Transaction() 类似)

type CapFunc func(ctx context.Context) (interface{}, error)
func (c *Capsule) StartCapsule(ctx context.Context, isTrans bool, capFunc CapFunc) (interface{}, error)
StartCapsule 开启胶囊操作,参数 ctx 上下文用于携带胶囊句柄,isTrans 是否开始事务 true 开启。 CapFunc 回调函数,所有逻辑都在该回调内实现

胶囊通用执行

func (c Capsule) Exec(ctx context.Context, query string, args …interface{}) (Affected, error)

胶囊插入

func (c Capsule) Insert(ctx context.Context, query string, args …interface{}) (Affected, error)

胶囊插入多条数据

func (c Capsule) InsertMany(ctx context.Context, query string, args [][]interface{}) (Affected, error)

胶囊更新

func (c Capsule) Update(ctx context.Context, query string, args …interface{}) (Affected, error)

胶囊更新多条

func (c Capsule) UpdateMany(ctx context.Context, query string, args [][]interface{}) (Affected, error)

胶囊删除

func (c Capsule) Delete(ctx context.Context, query string, args …interface{}) (Affected, error)

胶囊查询单条数据

func (c *Capsule) Get(ctx context.Context, dest interface{}, query string, args …interface{}) error

胶囊查询

func (c *Capsule) Query(ctx context.Context, dest interface{}, query string, args …interface{}) error

例15,胶囊事务操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
db, err := New(opt)  // 初始化sqly(数据库连接)
if err != nil {
t.Error(err)
}
capsule := NewCapsule(db) // 创建一个胶囊句柄
ctx := context.TODO() // 胶囊查询必须携带 context 参数
// isTrans=true 开始事务查询
ret, err := capsule.StartCapsule(ctx, true, func(ctx context.Context) (interface{}, error) {
// 在回调函数内执行相关数据库操作
var accs []*Account
query := "SELECT `id`, `nickname`, `avatar`, `email`, `mobile`, `password`, `role` " +
"FROM `account`"
err := capsule.Query(ctx, &accs, query, "18812311232") // 执行查询
if err != nil {
return nil, err
}
query = "UPDATE `account` SET `nickname`=? WHERE `id`=?"
_, err = capsule.Update(ctx, query, "nick_trans2", accs[0].ID) // 更新
if err != nil {
return nil, err
}
query = "UPDATE `account` SET `avatar`=? WHERE `id`=?"
aff, err := capsule.Update(ctx, query, "test2.png", accs[1].ID) // 更新
if err != nil {
return nil, err
}
query = "INSERT INTO `account` (`nickname`, `mobile`, `email`, `role`) " +
"VALUES (?, ?, ?, ?);"
aff, err = capsule.Insert(ctx, query, "nick_test2", "18712311235", "testx1@foxmail.com", 1) // 插入
if err != nil {
return nil, err
}
return aff, nil
})
if err != nil {
fmt.Println("err", err.Error())
}
fmt.Println(ret)

例16,胶囊非事务操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
   db, err := New(opt)  // 初始化sqly(数据库连接)
if err != nil {
t.Error(err)
}
capsule := NewCapsule(db) // 创建一个胶囊句柄
ctx := context.TODO() // 胶囊查询必须携带 context 参数
// isTrans=false 不开启事务
ret, err := capsule.StartCapsule(ctx, false, func(ctx context.Context) (interface{}, error) {
// 在回调函数内执行相关数据库操作
var accs []*Account
query := "SELECT `id`, `nickname`, `avatar`, `email`, `mobile`, `password`, `role` " +
"FROM `account`"
err := capsule.Query(ctx, &accs, query, "18812311232") // 查询
if err != nil {
return nil, err
}
query = "UPDATE `account` SET `nickname`=? WHERE `id`=?"
_, err = capsule.Update(ctx, query, "nick_trans3", accs[0].ID) // 更新
if err != nil {
return nil, err
}
query = "UPDATE `account` SET `avatar`=? WHERE `id`=?"
aff, err := capsule.Update(ctx, query, "test3.png", accs[1].ID) // 更新
if err != nil {
return nil, err
}
query = "INSERT INTO `account` (`nickname`, `mobile`, `email`, `role`) " +
"VALUES (?, ?, ?, ?);"
aff, err = capsule.Insert(ctx, query, "nick_test3", "18712311235", "testx1@foxmail.com", 1) // 插入
if err != nil {
return nil, err
}
return aff, nil
})
if err != nil {
fmt.Println("err", err.Error())
}
fmt.Println(ret)

从例15和例16两个例子中观察可以发现,采用胶囊操作时,事务操作和非事务操作区别仅在于 StartCapsule 中是否开启事务,其他操作都一致。因此开发过程中只需要在开始时考虑事务的问题,其他时候都不需要关注是否开启事务,减少出错的环节。

sqly 支持的数据类型

struct

  • 在 struct 中定义的属性必须是可以被 database/sql Scan 的数据类型,int64, float64, bool, []byte, string, time.Time, nil 等

  • 假如数据库中 Email 字段允许为空,且其值为空时,在 Scan 的时候会出现异常,因此 sql 官方封装了 sql.NullTime, sql.NullBool, sql.NullFloat64, sql.NullInt64, sql.NullInt32, sql.NullString。sqly 对其进行了进一步封装,sqly.NullTime, sqly.NullBool, sqly.NullFloat64, sqly.NullInt64, sqly.NullInt32, sqly.NullString。 其特点是在使用 json.Marshal 时对应字段为空的会自动解析为 null; json 字符串使用 json.UnMarshal 时,会自动解析为对应的 sqly.NullTime 等扩展类型

  • 如果使用 tinyint 或 int 类表示 bool 字段类型,例如:0 为 false, 1或其它为 true, 在定义字段类型时,可以使用 sqly.Boolean 类型来支持,在 scan 的时候会字段将 int 类型转换成 bool, 如果值只有 0 或 1 可以使用原生 bool

例17 定义一个 struct 对象(来自例10)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// sql tag 对应的就是 例4中对应的 account 表各个字段名
type Account struct {
ID int64 `sql:"id" json:"id"`
Nickname string `sql:"nickname" json:"nickname"`
Avatar sqly.NullString `sql:"avatar" json:"avatar"`
Email string `sql:"email" json:"email"`
Mobile string `sql:"mobile" json:"mobile"`
Role sqly.NullInt32 `sql:"role" json:"role"`
Password string `sql:"password" json:"password"`
ExpireTime sqly.NullTime `sql:"expire_time" json:"expire_time"`
IsValid sqly.NullBool `sql:"is_valid" json:"is_valid"`
CreateTime time.Time `sql:"create_time" json:"create_time"`
}

acc := Account{
ID: 1,
Nickname: "nickname",
Avatar: NullString{String: "", Valid: false},
Email: "123@gmail.com",
Mobile: "",
Role: NullInt32{Int32: 1, Valid: true},
Password: "",
IsValid: NullBool{Bool: true, Valid: true},
CreateTime: time.Now(),
AddTime: NullTime{Time: time.Now(), Valid: true},
Birthday: NullTime{},
}
// 传入的一定需要 acc 对象的指针
b, err := json.Marshal(&acc)
if err != nil {
t.Error(err)
}
fmt.Println(string(b))
// output {"id":1,"nickname":"nickname","avatar":null,"email":"123@gmail.com","mobile":"","role":1,"password":"","is_valid":true,"stature":null,"create_time":"2020-09-07T15:58:28.113861+08:00","add_time":"2020-09-07T15:58:28.113861+08:00","birthday":null}

// 如果是值传入,无法达到预期的效果
b, err = json.Marshal(acc)
fmt.Println(string(b))
// output {"id":1,"nickname":"nickname","avatar":{"String":"","Valid":false},"email":"123@gmail.com","mobile":"","role":{"Int32":1,"Valid":true},"password":"","is_valid":{"Bool":true,"Valid":true},"stature":{"Float64":0,"Valid":false},"create_time":"2020-09-07T16:00:52.217445+08:00","add_time":{"Time":"2020-09-07T16:00:52.217445+08:00","Valid":true},"birthday":{"Time":"0001-01-01T00:00:00Z","Valid":false}}

struct 嵌套支持

例18,嵌套 struct 支持

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
db, err := New(opt)
if err != nil {
return
}
type Contact struct {
Email string `sql:"email" json:"email"`
Mobile string `sql:"mobile" json:"mobile"`
}
type Base struct {
Contact Contact `json:"contact"`
Nickname string `sql:"nickname" json:"nickname"`
Avatar NullString `sql:"avatar" json:"avatar"`
}
type Acc struct {
ID int64 `sql:"id" json:"id"`
Role NullInt32 `sql:"role" json:"role"`
Base Base `json:"base"`
Password string `sql:"password" json:"password"`
IsValid NullBool `sql:"is_valid" json:"is_valid"`
CreateTime time.Time `sql:"create_time" json:"create_time"`
}
var accs []*Acc
query := "SELECT `id`, `avatar`, `email`, `mobile`, `nickname`, `password`, `role`, `create_time`, `is_valid` FROM `account`;"
err = db.Query(&accs, query)
if err != nil {
fmt.Println("query account error")
reutrn
}
resStr, _ := json.Marshal(accs)
fmt.Println(string(resStr))

可 scan 类型及其数组支持

支持 int, int32, int64, string, time.Time,
和其数组 []int, []int32, []int64, []string, []time.Time

例19,可Scan 类型支持

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
   db, err := New(opt)
if err != nil {
t.Error(err)
}
// int
query := "SELECT COUNT(*) FROM `account`;"
var num int
err = db.Get(&num, query)
if err != nil {
t.Error(err)
}
fmt.Println("num", num)
// time
query := "SELECT `create_time` FROM `account` limit 1;"
create := &NullTime{}
err = db.Get(create, query)
if err != nil {
t.Error(err)
}
fmt.Println("create", create)

map[string]inteface{}

(目前支持只 MySQL),支持还不完善,不推荐使用

1
2
3
4
5
6
7
8
9
10
11
12
13
db, err := New(opt)
if err != nil {
t.Error(err)
}
var accs []map[string]interface{}
query := "SELECT * FROM `account`;"

err = db.Query(&accs, query, nil)
if err != nil {
t.Error(err)
}
accStr, _ := json.Marshal(accs)
fmt.Printf("rows %s", accStr)

总结

sqly 只是一个简化数据库操作的工具,其设计逻辑同 torndb 类似,需要手写 sql 语句,不提供类似 go-sqlbuilder 或 gorm 这些框架强大的生成 sql 的 api。

sqly 仓库地址: https://github.com/FeifeiyuM/sqly 欢迎来踩,贡献您的宝贵建议