sql driver.Valuer 和 sql.Sanner
实现 scan 方法 实现获取自定义类型
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
|
// JsonColumn 代表存储字段的 json 类型
// 主要用于没有提供默认 json 类型的数据库
// T 可以是结构体,也可以是切片或者 map
// 一切可以被 json 库所处理的类型都能被用作 T
type JsonColumn[T any] struct {
Val T
Valid bool
}
// Value 返回一个 json 串。类型是 []byte
func (j JsonColumn[T]) Value() (driver.Value, error) {
if !j.Valid {
return nil, nil
}
return json.Marshal(j.Val)
}
// Scan 将 src 转化为对象
// src 的类型必须是 []byte, *[]byte, string, sql.RawBytes, *sql.RawBytes 之一
func (j *JsonColumn[T]) Scan(src any) error {
var bs []byte
switch val := src.(type) {
case []byte:
bs = val
case *[]byte:
bs = *val
case string:
bs = []byte(val)
case sql.RawBytes:
bs = val
case *sql.RawBytes:
bs = *val
default:
return fmt.Errorf("ekit:JsonColumn.Scan 不支持 src 类型 %v", src)
}
if err := json.Unmarshal(bs, &j.Val); err != nil {
return err
}
j.Valid = true
return nil
}
|
隔离级别深入理解
串行化读
挨个执行事务
可重复度
t1 提交事务,t2看不到修改
A 事务 无法看到 B事务的修改, A事务在一个select语句执行结果总是相同 ,满足事务的隔离性,事务之间互不影响
读已提交
t1 没提交前,t2看不到修改
读未提交
脏读
A事务看到B事务没提交的修改
不可重复读
一个事务内两次查询同一行结果不一致, 未提交度和已提交读都是不可重复读
幻读
读到 其他事务插入的新数据, mysql innodb 引擎的可重复度是不会引发幻读的!
原理是 MVCC机制
InnoDB 是如何解决幻读的?
在读已提交的情况下,及时采用了 MVCC 方式也会出现幻读,如果我们同时开启事务A 和 事务B, 现在事务A 中进行某个条件的查询,读取的时候采用排他锁,在事务B 中增加一条复核该条件范围的数据,并提及,然后事务A中再查询该条件范围的数据,就会发现结果集中多了一条数据,这样便出现了幻读
select * from t where d=0 就是快照读,对于同一个事务来说,每次读到的结果是一样的。
select *from t where d=0 in share mode
或 select * from t where d=0 for update
就是当前读,总是读取当前数据行的最新版本,关于数据行版本问题可参考事务究竟有没有被隔离
注意,需要用 in share mode 或者 for update 上间隙锁,才能阻塞其他事务插入数据
参考文档
prepared statement 使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
stmt, err := s.db.Prepare("SELECT * FROM `test_model` WHERE `id` = ?")
if err != nil {
t.Fatal(err)
}
// SELECT * FROM `user` WHERE `id` = 1
_, err = stmt.QueryContext(context.Background(), 1)
assert.Nil(t, err)
// SELECT * FROM `user` WHERE `id` = 1
_, err = stmt.QueryContext(context.Background(), 2)
assert.Nil(t, err)
// 用完就关闭
err = stmt.Close()
assert.Nil(t, err)
|
go sqlmock
go-sqlmock 本质是一个实现了 sql/driver 接口的 mock 库,它的设计目标是支持在测试中,模拟任何 sql driver 的行为,而不需要一个真正的数据库连接,这对 TDD 很有帮助。
sqlmock 其实已经存在很多年了,从目前业界的态度,普遍看还是不太建议用了,因为目前基于内存的 Fake 实现已经比较成熟,这样用一个 fake working implementation 的心智负担,以及对真实 sql 语句的处理都会更直观,便捷。大体上看,用 sqlmock 的劣势有两点:
sqlmock 用起来还是有点繁琐的,不如直接用 sqlite 或其他内存MySQL 实现去端到端检验效果(纯指对dal方法对单测,不是指外层测试穿透).
DAL 层代码唯一会担心出问题的地方就是sql写错、返回内容不对或者未命中索引,或是并发问题。而 sqlmock 直接 mock了 DAL层,也就什么都测不出来了;如果我不担心sql出错, 那直接 mock DB 接口就完事了。 而且sql太多,mock起来太累了。 sqlmock对增加dao层代码信心没有帮助。
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 TestSqlMock(t *testing.T) {
mockDB, mock, err := sqlmock.New()
if err != nil {
t.Fatal(err)
}
defer func() { _ = mockDB.Close() }()
mock.ExpectBegin()
// mock 返回的行
mockRows := sqlmock.NewRows([]string{"id", "name"}).AddRow(1, "Tom")
// 或者 WillReturnError
mock.ExpectQuery("SELECT .*").WillReturnRows(mockRows)
mockResult := sqlmock.NewResult(12, 1)
// 或者 WillReturnError
mock.ExpectExec("UPDATE .*").WillReturnResult(mockResult)
mock.ExpectCommit()
tx, err := mockDB.Begin()
assert.Nil(t, err)
rows, err := tx.QueryContext(context.Background(), "SELECT * FROM `user`")
cs, err := rows.Columns()
assert.Nil(t, err)
assert.Equal(t, []string{"id", "name"}, cs)
rows.Next()
var id int
var name string
err = rows.Scan(&id, &name)
assert.Nil(t, err)
res, err := tx.ExecContext(context.Background(), "UPDATE `user` SET `age` = 12")
assert.Nil(t, err)
affected, err := res.RowsAffected()
assert.Nil(t, err)
assert.Equal(t, int64(1), affected)
err = tx.Commit()
assert.Nil(t, err)
}
|
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
|
package main
import (
"github.com/DATA-DOG/go-sqlmock"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
func main() {
sqlDB, _, err := sqlmock.New()
if err != nil {
panic(err)
}
gormDB, err := gorm.Open(mysql.New(mysql.Config{
Conn: sqlDB,
}), &gorm.Config{
SkipInitializeWithVersion: true,
})
if err != nil {
panic(err) // Error here
}
_ = gormDB
}
|