Golang——gorm库vs标准sql库

本文章不做全面比较,只是比较当数据表存在null字段时,基于gorm库和sql标准库的一个重要的差异。
假设mysql存在如下一张表:

CREATE TABLE `student` (
  `id` int NOT NULL,
  `name` varchar(20) DEFAULT '',
  `score` int DEFAULT NULL,
  `classes` int DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3
复制代码
mysql> select * from student;
+----+-------+-------+---------+
| id | name  | score | classes |
+----+-------+-------+---------+
|  1 | test1 |   100 |       3 |
|  2 | test2 |   100 |    NULL |
+----+-------+-------+---------+
2 rows in set (0.00 sec)
复制代码

ps: 当然一般创建数据表时,规范的做法是,字段需要not null default 0指定默认值。这里只是为了演示两种库的差异。

  1. gorm
package main

import (
	"fmt"

	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

type Student struct {
	Name     string
	Id       int
	Score    float64
	Classes  int
	AvgScore float64
}

func main() {
	dsn := "root:12345678@tcp(127.0.0.1:3306)/test?charset=utf8mb4&parseTime=True&loc=Local"
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	if err != nil {
		panic(err)
	}

	var stu Student
	db.Select("id,name,score,classes,score/classes as avg_score").Table("student").Where("id=?", 2).Find(&stu)
	fmt.Println(stu)

}
复制代码

输出:

{test2 2 100 0 0}
复制代码

可以看出null值被转换成了0,而且没有异常抛出。

  1. sql
package main

import (
	"fmt"

	"database/sql"

	_ "github.com/go-sql-driver/mysql"
)

type Student struct {
	Name     string
	Id       int
	Score    float64
	Classes  int
	AvgScore float64
}

func main() {
	db, err := sql.Open("mysql", "root:12345678@tcp(127.0.0.1:3306)/test")
	if err != nil {
		panic(err)
	}

	rows, err := db.Query("select id,name,score,classes,score/classes as avg_score from student")
	if err != nil {
		panic(err)
	}

	var stu Student
	for rows.Next() {
		err := rows.Scan(&stu.Id, &stu.Name, &stu.Score, &stu.Classes, &stu.AvgScore)
		if err != nil {
			panic(err)
		}
		fmt.Println(stu)
	}
}
复制代码

输出:

{test1 1 100 3 33.3333}
panic: sql: Scan error on column index 3, name "classes": converting NULL to int is unsupported

goroutine 1 [running]:
main.main()
        /xxx/core/sqls/yyy/test2.go:34 +0x2d9
exit status 2
复制代码

类型转换的时候抛了异常。查看Scan代码,包含一个convertAssignRows调用:

switch dv.Kind() {
	case reflect.Ptr:
		if src == nil {
			dv.Set(reflect.Zero(dv.Type()))
			return nil
		}
		dv.Set(reflect.New(dv.Type().Elem()))
		return convertAssignRows(dv.Interface(), src, rows)
         //目标类型为如下
	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
                //结果类型为nil
		if src == nil {
			return fmt.Errorf("converting NULL to %s is unsupported", dv.Kind())
		}
		s := asString(src)
		i64, err := strconv.ParseInt(s, 10, dv.Type().Bits())
		if err != nil {
			err = strconvErr(err)
			return fmt.Errorf("converting driver.Value type %T (%q) to a %s: %v", src, s, dv.Kind(), err)
		}
		dv.SetInt(i64)
		return nil
	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
		if src == nil {
			return fmt.Errorf("converting NULL to %s is unsupported", dv.Kind())
		}
		s := asString(src)
		u64, err := strconv.ParseUint(s, 10, dv.Type().Bits())
		if err != nil {
			err = strconvErr(err)
			return fmt.Errorf("converting driver.Value type %T (%q) to a %s: %v", src, s, dv.Kind(), err)
		}
		dv.SetUint(u64)
		return nil
	case reflect.Float32, reflect.Float64:
		if src == nil {
			return fmt.Errorf("converting NULL to %s is unsupported", dv.Kind())
		}
		s := asString(src)
		f64, err := strconv.ParseFloat(s, dv.Type().Bits())
		if err != nil {
			err = strconvErr(err)
			return fmt.Errorf("converting driver.Value type %T (%q) to a %s: %v", src, s, dv.Kind(), err)
		}
		dv.SetFloat(f64)
		return nil
	case reflect.String:
		if src == nil {
			return fmt.Errorf("converting NULL to %s is unsupported", dv.Kind())
		}
		switch v := src.(type) {
		case string:
			dv.SetString(v)
			return nil
		case []byte:
			dv.SetString(string(v))
			return nil
		}
	}
复制代码

当查询结果类型为nil, 目标类型为int时,会抛类型转换的异常。但是如果目标类型为指针类型,不会抛异常,所以将代码改为:

package main

import (
	"fmt"

	"database/sql"

	_ "github.com/go-sql-driver/mysql"
)

type Student struct {
	Name     string
	Id       int
	Score    float64
	Classes  *int
	AvgScore *float64
}

func main() {
	db, err := sql.Open("mysql", "root:12345678@tcp(127.0.0.1:3306)/test")
	if err != nil {
		panic(err)
	}

	rows, err := db.Query("select id,name,score,classes,score/classes as avg_score from student")
	if err != nil {
		panic(err)
	}

	var stu Student
	for rows.Next() {
		err := rows.Scan(&stu.Id, &stu.Name, &stu.Score, &stu.Classes, &stu.AvgScore)
		if err != nil {
			panic(err)
		}
		fmt.Println(stu)
	}
}

复制代码

输出:

{test1 1 100 0xc000016358 0xc000016360}
{test2 2 100 <nil> <nil>}
复制代码

结论:基于SQL库的查询需要注意结果字段为NULL的情况,对应结构体字段需要声明为指针类型。
那么,为什么基于gorm库的不需要考虑这种情况呢?

// assign stmt.ReflectValue
	if stmt.Dest != nil {
		stmt.ReflectValue = reflect.ValueOf(stmt.Dest)
		for stmt.ReflectValue.Kind() == reflect.Ptr {
			if stmt.ReflectValue.IsNil() && stmt.ReflectValue.CanAddr() {
				stmt.ReflectValue.Set(reflect.New(stmt.ReflectValue.Type().Elem()))
			}

			stmt.ReflectValue = stmt.ReflectValue.Elem()
		}
		if !stmt.ReflectValue.IsValid() {
			db.AddError(ErrInvalidValue)
		}
	}
复制代码

因为gorm库本身已经做了这种兼容处理。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享