Giter VIP home page Giter VIP logo

Comments (9)

longyue0521 avatar longyue0521 commented on September 22, 2024 1

@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)的处理方式.

from ekit.

hookokoko avatar hookokoko commented on September 22, 2024

对于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
}

from ekit.

juniaoshaonian avatar juniaoshaonian commented on September 22, 2024

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

from ekit.

flycash avatar flycash commented on September 22, 2024

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

from ekit.

hookokoko avatar hookokoko commented on September 22, 2024

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

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

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

from ekit.

flycash avatar flycash commented on September 22, 2024

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

from ekit.

flycash avatar flycash commented on September 22, 2024

from ekit.

longyue0521 avatar longyue0521 commented on September 22, 2024

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

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

from ekit.

flycash avatar flycash commented on September 22, 2024

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

from ekit.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.