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
}

隔离级别深入理解

图 1

  • 序列化
  • 可重复度
  • 读已提交
  • 读未提交

串行化读

图 2

挨个执行事务

可重复度

图 3

t1 提交事务,t2看不到修改

A 事务 无法看到 B事务的修改, A事务在一个select语句执行结果总是相同 ,满足事务的隔离性,事务之间互不影响

读已提交

图 4

t1 没提交前,t2看不到修改

读未提交

图 5

脏读

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 modeselect * 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
}