如果扫描仪对象带有github.com/go-sql-driver/mysql驱动程序,则扫描失败

daolsyd0  于 2023-11-14  发布在  Go
关注(0)|答案(1)|浏览(98)

我尝试在Go应用程序中使用Scanner对象来查询数据库服务器。我使用Microsoft SQL Server没有问题,但发现如果我使用带有Rows.Scan的scanner对象来查询MySQL,则src参数对于所有类型都是[]uint8
我的table是

CREATE TABLE IF NOT EXISTS fruits (
        id BIGINT(20) NOT NULL AUTO_INCREMENT,
        name VARCHAR(45),
        year SMALLINT,
        amount INT,
        price FLOAT(23),
        PRIMARY KEY (id)
) ENGINE=InnoDB CHARSET=utf8mb4;

字符串
这是我的代码

type Scanner struct {
        FieldName string
}

func (s *Scanner) Scan(src any) error {
        fmt.Printf("FieldName: %s, src: type=%[2]T value=%[2]v", s.FieldName, src)
        return nil
}

// prepare to query
id_scanner := &Scanner{"Id"}
name_scanner := &Scanner{"Name"}
year_scanner := &Scanner{"Year"}
amount_scanner := &Scanner{"Amount"}
price_scanner := &Scanner{"Price"}

// query single row
row := db.QueryRow("select * from fruits where id=?", 1)
err := row.Scan(id_scanner, name_scanner, year_scanner, amount_scanner, price_scanner)


errnil,输出看起来非常好,例如

FieldName: Id, src: type=int64 value=1
FieldName: Name, src: type=[]uint8 value=[97 112 112 108 101]
FieldName: Year, src: type=int64 value=1965
FieldName: Amount, src: type=int64 value=123
FieldName: Price, src: type=float32 value=7.55


这表明Row.Scan可以与Scanner对象一起工作。但是,Rows.Scan会失败:

rows, err := db.Query("select * from fruits;")
// err is nil
defer rows.Close()
for rows.Next() {
        err := rows.Scan(id_scanner, name_scanner, year_scanner, amount_scanner, price_scanner)
        // err is nil, but output is wrong
}


在mysql:8..0.31 docker镜像上运行,输出是不同的:

FieldName: Id, src: type=[]uint8 value=[49]
FieldName: Name, src: type=[]uint8 value=[97 112 112 108 101]
FieldName: Year, src: type=[]uint8 value=[49 57 54 53]
FieldName: Amount, src: type=[]uint8 value=[49 50 51]
FieldName: Price, src: type=[]uint8 value=[55 46 53 53]

  • 对于Id字段,值[49]是字符串“1”。
  • 对于Name字段,值[97 112 112 108 101]是字符串“apple”。
  • 对于Year字段,值[49 57 54 53]是字符串“1965”。
  • 对于Amount字段,值[49 50 51]是字符串“123”。
  • 对于Price字段,值[55 46 53 53]是字符串“7.55”。

看起来src参数是fmt.Print的结果,如果涉及到Scanner对象,则类似。
我希望看到Rows.Scan产生与Row.Scan相同的输出。
有谁知道我哪里出错了吗?为什么代码可以在Microsoft SQL Server中工作,但不能在MySQL中工作?

ljsrvy3e

ljsrvy3e1#

我在mysql 8.0.34和github.com/go-sql-driver/mysql驱动程序上重现了您的问题。我还检查了其他数据库引擎不会在如此简单的情况下发生-在我的情况下postgres 14.x。似乎不应该发生,因为Row.Scan的文档说扫描与Rows. Scan的工作原理相同。确实令人困惑。
但是,我认为您的实验可能与database/sql API的典型用法无关(下面将详细介绍)。
您似乎正在修补sql.Scanner接口,以了解它是如何工作的。您已经观察到,当接收器获取src参数时,它已经示例化为正确类型的值,或者它只是字节流(但正确的字节流)当它被不同地调用时。在实践中,这似乎不是一个问题。首先,为列实现自己的sql.Scanner并不常见。即使这样做,我注意到有一些神秘的情况,当我收到字节时,我需要相应地解析它们。(我在postgres上的自定义类型发生了这种情况,这并不是QueryQueryRows之间的区别-事实上,我没有深入到隔离差异的程度)最后我使用了一个类型开关,处理[]byte和预期的具体类型,例如:

func (d *MyDate) Scan(value any) error {
    switch x := value.(type) {
    case time.Time:
        // convert from timestamp...
        return nil
    case []byte:
        // parse ...
    default:
        return fmt.Errorf("scan date: unknown type %T", x)
    }
}

字符串
在为自己的类型实现列扫描器时,您可能会使用相同的策略。
请记住,你通常只是将Scan放入你的结构体字段,而不考虑自定义扫描。下面的代码适用于Row.ScanRows.Scan

type Fruit struct {
    Id     int
    Name   string
    Year   string
    Amount int
    Price  float64
}

func run() (err error) {

    var x Fruit

    fmt.Println("query 1 row:")
    row := db.QueryRow("select * from fruits where id=?", 1)
    err = row.Scan(&x.Id, &x.Name, &x.Year, &x.Amount, &x.Price)
    if err != nil {
        return err
    }
    fmt.Printf("%+v\n", x)

    fmt.Println("query rows:")
    rows, err := db.Query("select * from fruits;")
    if err != nil {
        return err
    }
    defer rows.Close()
    for rows.Next() {
        err = rows.Scan(&x.Id, &x.Name, &x.Year, &x.Amount, &x.Price)
        if err != nil {
            return err
        }
        fmt.Printf("%+v\n", x)
    }
    return rows.Err()
}


结果是:

query 1 row:
{Id:1 Name:bobofrut Year:2023 Amount:100 Price:3.14}
query rows:
{Id:1 Name:bobofrut Year:2023 Amount:100 Price:3.14}


希望这对你有帮助!
请注意,我使用了一些简化,将Year作为字符串。实际上,它可以是各种类型,自定义日期,time.Time截断为年份,等等。它似乎与您的情况无关。
你可能听说过将货币存储为float是一个糟糕的想法-请使用mysql DECIMAL(M,2)和Go端的shopspring/decimal
PS3.你的Scan实现中的fmt.Printf在格式参数的编号方面有一个小错误,对于给定的两个参数,它应该是... type=%[2]T value=%[2]v。我猜你只是从你的编码的其他时刻错误地粘贴代码。

相关问题