-
Notifications
You must be signed in to change notification settings - Fork 161
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
Comments
对于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
} |
不需要范型scantype中会给你类型信息,你只需要通过这个类型信息构建一个新值去接受数据 |
用不了泛型,因为我作为一个调用者也不知道具体的类型可能是什么 |
scantype的用法我明白,其实我是想确认下这两函数的区别 func ScanRows(rows *sql.Rows) []any, error {} *sql.Rows有scantype方法,貌似sql.Row没有这个方法吧? |
我草,还真没有,那么就不需要 ScanRow 了,只需要 ScanRows 和 ScanAll,ScanRow 等将来支持了再说 |
请教几个问题 重复及效率问题ScanAll的实现依赖于ScanRows,而ScanRows每次都要重新用反射获取一遍各个列的类型来准备用于接收数据的values. 是否可以构建一个抽象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
all, err := ScanAll(rows)
if err != nil {
// Scan出错
}
if rows.Err() != nil {
// 迭代期间出错 ....可能导致当前结果集读取不完整
}
// 走到这可以安全地使用all 当前ScanAll实现中没有调用rows.Err()所以返回的数据可能不完整,用户需要在调用ScanAll后手动检查rows.Err(). 是否该将rows.Err()纳入ScanAll的职责范围? 如果将rows.Err纳入ScanAll的职责范围,慌报情况不容易区分.
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中读取一行并返回行数据.
使用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)的处理方式. |
好主意!之前我在创建这个 issue 的时候,并没有想到这个问题。你可以按照这个提一个新的合并请求。
这个东西看起来有点像是之前我们设计 Copier 和 Copy 方法。有一个 Scanner 的话会更好。
不过你觉得这里有没有必要考虑将 Scanner 做成接口呢?
|
#182 中实现将Scanner做成了接口,但是写完之后发现好像当前Scanner只能用sql.Rows不能用于sql.Row |
sql.Row 是没有那个 ColumnType 的,所以就算了 |
仅限中文
使用场景
在数据库相关的中间件研发场景里面,很经常遇到的需求是我并不知道我可以用什么类型来接收数据,所以我希望用 sql.Rows 对应的 ScanType 来接收。
也就是逻辑是类似于:
目前来说,这种用法在分库分表的结果集处理里面被大量使用。
另外一个场景是,在设计通用的数据迁移和数据校验中间件里面,也出现了类似的需求。数据迁移是需要把源表的数据读取出来,然后再插入到对应的数据表里面。因为数据迁移要处理的表结构非常多样化,所以实际上开发的时候也难以预料到应该用什么 Go 类型来接收,那么这个 Scan 方法就很合适了。
那么这个方法的定义是:
对应的还有一个方法是:
这个方法只会纯粹扫描 sql.Row。
进一步,如果我们试图将所有的结果取出来,那么定义成为:
注意,你需要提供单元测试,这一个单元测试你使用sqlite 的内存模式来测试。
ScanRows 你可以考虑参考合并请求 https://github.com/ecodeclub/eorm/pull/193/files
The text was updated successfully, but these errors were encountered: