Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

sqlx:ScanRows 和 ScanAll方法 #179

Closed
flycash opened this issue Apr 22, 2023 · 9 comments · Fixed by #182
Closed

sqlx:ScanRows 和 ScanAll方法 #179

flycash opened this issue Apr 22, 2023 · 9 comments · Fixed by #182
Milestone

Comments

@flycash
Copy link
Contributor

flycash commented Apr 22, 2023

仅限中文

使用场景

在数据库相关的中间件研发场景里面,很经常遇到的需求是我并不知道我可以用什么类型来接收数据,所以我希望用 sql.Rows 对应的 ScanType 来接收。

也就是逻辑是类似于:

typ := rows.ScanType()
val := reflect.New(typ)
rows.Scan(val)

目前来说,这种用法在分库分表的结果集处理里面被大量使用。

另外一个场景是,在设计通用的数据迁移和数据校验中间件里面,也出现了类似的需求。数据迁移是需要把源表的数据读取出来,然后再插入到对应的数据表里面。因为数据迁移要处理的表结构非常多样化,所以实际上开发的时候也难以预料到应该用什么 Go 类型来接收,那么这个 Scan 方法就很合适了。

那么这个方法的定义是:

func ScanRows(rows *sql.Rows) []any, error {

}

对应的还有一个方法是:

func ScanRow(row sql.Row) []any, error {

}

这个方法只会纯粹扫描 sql.Row。

进一步,如果我们试图将所有的结果取出来,那么定义成为:

func ScanAll(rows sql.Rows)  [][]any, error {
    
}

注意,你需要提供单元测试,这一个单元测试你使用sqlite 的内存模式来测试。

ScanRows 你可以考虑参考合并请求 https://github.com/ecodeclub/eorm/pull/193/files
image

@hookokoko
Copy link
Contributor

hookokoko commented Apr 23, 2023

对于func ScanRow(row sql.Row) ([]any, error) {} 有个疑问,直接scan的话,要写入的目标要通过泛型指定吗?类似下面这样

func ScanRow[T any](row sql.Row) ([]T, error) {
	// 列的个数如何定?
	res := make([]T, 0, 8)
	err := row.Scan(res)
	if err != nil {
		return nil, err
	}
	return res, nil
}

@juniaoshaonian
Copy link
Contributor

不需要范型scantype中会给你类型信息,你只需要通过这个类型信息构建一个新值去接受数据

@flycash
Copy link
Contributor Author

flycash commented Apr 24, 2023

用不了泛型,因为我作为一个调用者也不知道具体的类型可能是什么

@hookokoko
Copy link
Contributor

scantype的用法我明白,其实我是想确认下这两函数的区别

func ScanRows(rows *sql.Rows) []any, error {}
func ScanRow(row sql.Row) []any, error {}

*sql.Rows有scantype方法,貌似sql.Row没有这个方法吧?

@flycash
Copy link
Contributor Author

flycash commented Apr 24, 2023

我草,还真没有,那么就不需要 ScanRow 了,只需要 ScanRows 和 ScanAll,ScanRow 等将来支持了再说

@longyue0521 longyue0521 changed the title sqlx:ScanRows, ScanRow 和 ScanAll方法 sqlx:ScanRows 和 ScanAll方法 May 7, 2023
@longyue0521
Copy link
Collaborator

@flycash

请教几个问题

重复及效率问题

ScanAll的实现依赖于ScanRows,而ScanRows每次都要重新用反射获取一遍各个列的类型来准备用于接收数据的values.
即便就ScanRows自身来讲,最常见的使用场景——同一个sql.Rows,不断循环或间隔一定时间调用ScanRows(*sql.Rows),重复及效率问题也是存在.

是否可以构建一个抽象Scanner来解决重复及效率问题.下面是v1版Scanner与当前的ScanRows和ScanAll语义一致.

type Scanner struct {
	sqlRows      *sql.Rows
	cachedValues []any
}

func NewScanner(r *sql.Rows) (*Scanner, error) {
        if r == nil {
            // .....
        }
	colsInfo, err := r.ColumnTypes()
	if err != nil {
		return nil, err
	}
	values := make([]any, len(colsInfo))
	for i, colInfo := range colsInfo {
		typ := colInfo.ScanType()
		for typ.Kind() == reflect.Pointer {
			typ = typ.Elem()
		}
		values[i] = reflect.New(typ).Interface()
	}
	return &Scanner{sqlRows: r, cachedValues: values}, nil
}

func (s *Scanner) ScanOne() ([]any, error) {
        // 每次都用同一个列集合来获取数据
	err := s.sqlRows.Scan(s.cachedValues...)
	if err != nil {
		return nil, err
	}
        // 获取当前行的各个列值
	values := make([]any, len(s.cachedValues))
	for i := 0; i < len(s.cachedValues); i++ {
		values[i] = reflect.ValueOf(s.cachedValues[i]).Elem().Interface()
	}
	return values, nil
}

func (s *Scanner) ScanAll() ([][]any, error) {
	res := make([][]any, 0, 32)
	for s.sqlRows.Next() {
		cols, err := s.ScanOne()
		if err != nil {
			return nil, err
		}
		res = append(res, cols)
	}
	return res, nil
}

// 用v1版Scnner重构下面的代码通过了测试集合
func ScanRows(rows *sql.Rows) ([]any, error) {
	scanner, err := NewScanner(rows)
	if err != nil {
		return nil, err
	}
	return scanner.ScanOne()
}

func ScanAll(rows *sql.Rows) ([][]any, error) {
	scanner, err := NewScanner(rows)
	if err != nil {
		return nil, err
	}
	return scanner.ScanAll()
}

抽象的职责划分问题——半“自动/委托”,全“自动/委托”

使用原生的sql.Rows读取当前结果集的所有行,代码大致如下:

defer rows.Close()
var all
for rows.Next() {
     var values
     if err := rows.Scan(values...); err != nil {
         // 扫描当前values出错
     }
     // 代码走到这,可以安全地使用values
     all := append(all, values)
}
if rows.Err() != nil {
    // 迭代期间出错 ....可能导致当前结果集读取不完整
}

// 走到这可以安全地使用all
  1. 用户使用ScanAll实现上述等效代码:
all, err := ScanAll(rows)
if err != nil {
    // Scan出错
}
if rows.Err() != nil {
   // 迭代期间出错 ....可能导致当前结果集读取不完整
}
// 走到这可以安全地使用all

当前ScanAll实现中没有调用rows.Err()所以返回的数据可能不完整,用户需要在调用ScanAll后手动检查rows.Err().
另外,rows.Err()可能“谎报”,比如连接超时,但当前结果集已经读完了对后续数据使用无影响.

是否该将rows.Err()纳入ScanAll的职责范围? 如果将rows.Err纳入ScanAll的职责范围,慌报情况不容易区分.

  1. 用户使用ScanRows实现上述等效代码:
defer rows.Close()
var all
for rows.Next() {
     values, err := ScanRows(rows)
     if err != nil {
         // Scan报错
     }
     // 代码走到这,可以安全地使用values
     all := append(all, values)
}
if rows.Err() != nil {
    // 迭代期间出错 ....可能导致当前结果集读取不完整
}

类似的,ScanRows中没有调用rows.Next()及rows.Err()所以当前ScanRows的职责就是从rows中读取一行并返回行数据.
是否可以考虑将rows.Next和rows.Err纳入ScanRows的职责范围内?

  1. v1版Scanner的接口不在同一个抽象层次上

使用v1版Scanner,用户需要记住并区分使用ScanOne和ScanAll哪个要提前调用rows.Next()(或者之后要调用rows.Err)哪个不用,统一抽象层次这样符合用户的直觉. 另外,sql.Rows.Scan的注释中明确提示调用Scan前要先调用Next准备数据,所以将rows.Next和rows.Err纳入职责范围

下面是v2的Scanner

var ErrNoMoreRows = errors.New("ekit: no more rows in result set")

type Scanner struct {
	sqlRows      *sql.Rows
	cachedValues []any
}

func NewScanner(r *sql.Rows) (*Scanner, error) {
	// 读取列的元数据缓存
	colsInfo, err := r.ColumnTypes()
	if err != nil {
		return nil, err
	}
	values := make([]any, len(colsInfo))
	for i, colInfo := range colsInfo {
		typ := colInfo.ScanType()
		for typ.Kind() == reflect.Pointer {
			typ = typ.Elem()
		}
		values[i] = reflect.New(typ).Interface()
	}
	return &Scanner{sqlRows: r, cachedValues: values}, nil
}

func (s *Scanner) ScanOne() ([]any, error) {
	// 调用Next
	if !s.sqlRows.Next() {
                 // 检测迭代错误
		if err := s.sqlRows.Err(); err != nil {
			return nil, err
		}
		// 数据读取完
		return nil, fmt.Errorf("%w", ErrNoMoreRows)
	}
	err := s.sqlRows.Scan(s.cachedValues...)
	if err != nil {
		return nil, err
	}
	values := make([]any, len(s.cachedValues))
	for i := 0; i < len(s.cachedValues); i++ {
		values[i] = reflect.ValueOf(s.cachedValues[i]).Elem().Interface()
	}
	return values, nil
}

func (s *Scanner) ScanAll() ([][]any, error) {
	res := make([][]any, 0, 32)
	for {
		cols, err := s.ScanOne()
		if err != nil {
                         // 需要区分正常结束,只是没有数据的情况
			if errors.Is(err, ErrNoMoreRows) {
				break
			}
			return nil, err
		}
		res = append(res, cols)
	}
	return res, nil
}

统一抽象层次后, Scanner.ScanAll中的代码就是用户使用ScanOne的代码,缺点是有点丑——采用了sql.Row(不是sql.Rows)的处理方式.

@flycash
Copy link
Contributor Author

flycash commented May 10, 2023 via email

@longyue0521
Copy link
Collaborator

好主意!之前我在创建这个 issue 的时候,并没有想到这个问题。你可以按照这个提一个新的合并请求。 这个东西看起来有点像是之前我们设计 Copier 和 Copy 方法。有一个 Scanner 的话会更好。 不过你觉得这里有没有必要考虑将 Scanner 做成接口呢?

#182 中实现将Scanner做成了接口,但是写完之后发现好像当前Scanner只能用sql.Rows不能用于sql.Row

@flycash
Copy link
Contributor Author

flycash commented May 21, 2023

sql.Row 是没有那个 ColumnType 的,所以就算了

@flycash flycash linked a pull request May 21, 2023 that will close this issue
@flycash flycash closed this as completed May 21, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: Done
Development

Successfully merging a pull request may close this issue.

4 participants