• 企业400电话
  • 网络优化推广
  • AI电话机器人
  • 呼叫中心
  • 全 部 栏 目

    网站建设 商标✡知产 微网小程序 电商运营 彩铃•短信 增值拓展业务
    解决Go语言数据库中null值的问题
    POST TIME:2021-10-18 17:14

    本文主要介绍如何使用go语言database/sql库从数据库中读取null值的问题,以及如何向数据库中插入null值。本文在这里使用的是sql.NullString, sql.NullInt64, sql.NullFloat64等结构体,为了方便书写,它们的泛指我会使用sql.Null***来表示

    要点

    从数据库读取可能为null值得值时,可以选择使用sql.NULL***来读取;或者使用IFNULL、COALESCE等命令让数据库查询值返回不为”“或者NULL

    若需要往数据库中插入null值,则依然可以使用sql.NULL***存储所需的值,然后进行插入NULL值

    直接使用sql.NULL***类型容易出现valid遗漏设置等问题,普通int、string与其转换时,请写几个简单的get、set函数

    本demo使用的数据库表以及数据如下

    mysql> desc person;
    +------------+--------------+------+-----+---------+----------------+
    | Field   | Type     | Null | Key | Default | Extra     |
    +------------+--------------+------+-----+---------+----------------+
    | id     | int(11)   | NO  | PRI | NULL  | auto_increment |
    | first_name | varchar(100) | NO  |   | NULL  |        |
    | last_name | varchar(40) | YES |   | NULL  |        |
    | age    | int(11)   | YES |   | NULL  |        |
    +------------+--------------+------+-----+---------+----------------+
    mysql> select * from person;
    +----+------------+-----------+------+
    | id | first_name | last_name | age |
    +----+------------+-----------+------+
    | 1 | yousa   | NULL   | NULL |
    +----+------------+-----------+------+
    mysql> show create table person;
    +--------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
    | Table | Create Table                                                                                                                      |
    +--------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
    | person | CREATE TABLE `person` (
     `id` int(11) NOT NULL AUTO_INCREMENT,
     `first_name` varchar(100) NOT NULL,
     `last_name` varchar(40) DEFAULT NULL,
     `age` int(11) DEFAULT NULL,
     PRIMARY KEY (`id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 |
    +--------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
    1 row in set (0.00 sec)
    

    从数据库中读取NULL值

    如果不作处理直接从数据库中读取NULL值到string/int,会发生如下错误错误

    Scan NULL值到string的报错

    sql: Scan error on column index 1: unsupported Scan, storing driver.Value type nil> into type *string

    Scan NULL值到int的报错

    sql: Scan error on column index 1: converting driver.Value type nil> ("nil>") to a int: invalid syntax

    使用如下的struct来读取数据库内容

    type Person struct {
      firstName        string
      lastName        string 
      age           int
    }
      //由于只有一行,直接使用QueryRow
      row := db.QueryRow("SELECT first_name, last_name FROM person WHERE first_name='yousa'")
      err = row.Scan(hello.firstName, hello.lastName)
      if err != nil {
        fmt.Println(err)
      }
      fmt.Println(hello)
      row1 := db.QueryRow("SELECT first_name, age FROM person WHERE first_name='yousa'")
      err = row1.Scan(hello.firstName, hello.age)
      if err != nil {
        fmt.Println(err)
      }
      fmt.Println(hello)

    运行代码,可以通过日志看出来,错误来自Scan将NULL值赋值给int或者string时,报错;解决这个问题可以使用sql原生结构体sql.Null***来解决

    使用sqlNull***

    sql.Null***在sql库中声明如下,在读取时,(比如读取的值存储到NullInt64),假如发现存储的值是NULL,则会将NullInt64的valid设置为false,然后不会将值存储到Int64中,Int64值默认为0,如果是NullString则String值时nil;如果是正常值,则会将Valid赋值为true,将值存储到Int64中。

    type NullInt64 struct {
      Int64 int64
      Valid bool // Valid is true if Int64 is not NULL
    }
    func (n *NullInt64) Scan(value interface{}) error
    func (n NullInt64) Value() (driver.Value, error)
    type NullString struct {
      String string
      Valid bool // Valid is true if String is not NULL
    }
    func (ns *NullString) Scan(value interface{}) error
    func (ns NullString) Value() (driver.Value, error)

    代码修改为如下:

    type Person struct {
      firstName        string
      lastNullName      sql.NullString
      nullAge         sql.NullInt64
    }
      rowNull := db.QueryRow("SELECT first_name, last_name FROM person WHERE first_name='yousa'")
      err = rowNull.Scan(hello.firstName, hello.lastNullName)
      if err != nil {
        fmt.Println(err)
      }
      fmt.Println(hello)
      rowNull1 := db.QueryRow("SELECT first_name, age FROM person WHERE first_name='yousa'")
      err = rowNull1.Scan(hello.firstName, hello.nullAge)
      if err != nil {
        fmt.Println(err)
      }
      fmt.Println(hello)
    

    输出结果

    {yousa 0 { false} {0 false}}

    {yousa 0 { false} {0 false}}

    使用IFNULL或者COALESCE

    coalesce()解释:返回参数中的第一个非空表达式(从左向右依次类推)

    IFNULL(expr1,expr2):如果expr1不是NULL,IFNULL()返回expr1,否则它返回expr2。IFNULL()返回一个数字或字符串值,取决于它被使用的上下文环境。

    查询语句使用一个默认值来替换NULL即可

    SELECT first_name, COALESCE(age, 0) FROM person;//

    SELECT first_name, IFNULL(age, 0) FROM person;//

    往数据库中插入NULL值

    前面我们对SELECT语句使用了sql.Null***类型,同理,INSERT、UPDATE语句也可以通过使用这种类型来插入nil值

    代码如下:

      hello := Person {
        firstName: "",
        lastName: "",
        age: 0,
        lastNullName: sql.NullString{String:"", Valid:false},
        nullAge: sql.NullInt64{Int64:0, Valid:false}}
      _, err = db.Exec(
        "INSERT INTO person (first_name, last_name) VALUES (?, ?)", "yousa1", hello.lastName)
      if err != nil {
        fmt.Println(err)
      }
      _, err = db.Exec(
        "INSERT INTO person (first_name, last_name) VALUES (?, ?)", "yousa2", hello.lastNullName)
      if err != nil {
        fmt.Println(err)
      }
    //数据库插入结果
    mysql> select * from person;
    +----+------------+-----------+------+
    | id | first_name | last_name | age |
    +----+------------+-----------+------+
    | 1 | yousa   | NULL   | NULL |
    | 2 | yousa1   |      | NULL |
    | 3 | yousa2   | NULL   | NULL |
    +----+------------+-----------+------+
    

    解释下db.Exec操作hello.lastNullName的过程:

    首先它会调用hello.lastNullName的Value方法,获取到driver.Value,然后检验Valid值是true还是false,如果是false则会返回一个nil值(nil值传给sql driver会被认为是NULL值),如果是true则会将hello.lastNullName.String的值传过去。

    PS: 为了保证你所插入的值能如你所期望是NULL值,一定记得要将sql.Null***中Valid值置为false

    使用NULL还是有很多危害的,再回顾下数据库中使用NULL值的危害

    为什么不建议使用NULL

    所有使用NULL值的情况,都可以通过一个有意义的值的表示,这样有利于代码的可读性和可维护性,并能从约束上增强业务数据的规范性。

    NULL值在timestamp类型下容易出问题,特别是没有启用参数explicit_defaults_for_timestamp

    NOT IN、!= 等负向条件查询在有 NULL 值的情况下返回永远为空结果,查询容易出错

    Null 列需要更多的存储空间:需要一个额外字节作为判断是否为 NULL 的标志位

    NULL值到非NULL的更新无法做到原地更新,更容易发生索引分裂,从而影响性能。

    PS:但把NULL列改为NOT NULL带来的性能提示很小,除非确定它带来了问题,否则不要把它当成优先的优化措施,最重要的是使用的列的类型的适当性。

    当然有些情况是不得不使用NULL值进行存储,或者在查询时由于left/right join等导致NULL值,但总体来说,能少用就少用。

    helper func(提升效率/减少错误)

    如果使用sql.NULL***的话,由于其有两个字段,如果直接手动赋值的话还是很容易遗漏,所以还是需要简单的转换函数,这里给了两个简单的helper fuc,分别是将int64转换成NullInt64和将string转换成NullString

    //ToNullString invalidates a sql.NullString if empty, validates if not empty
    func ToNullString(s string) sql.NullString {
      return sql.NullString{String : s, Valid : s != ""}
    }
    //ToNullInt64 validates a sql.NullInt64 if incoming string evaluates to an integer, invalidates if it does not
    func ToNullInt64(s string) sql.NullInt64 {
      i, err := strconv.Atoi(s)
      return sql.NullInt64{Int64 : int64(i), Valid : err == nil}
    }
    

    补充:golang 处理mysql数据库中的NULL, nil,time类型的值

    在用golang获取数据库的数据的时候,难免会遇到可控field。这个时候拿到的数据如果直接用string, time.Time这样的类型来解析的话会遇到panic。

    下面的方法会解决这种问题:

    表结构:

    show create table checksum_mengyao;

    CREATE TABLE `checksum_mengyao` (
     `db` char(64) NOT NULL,
     `tbl` char(64) NOT NULL,
     `chunk` int(11) NOT NULL,
     `chunk_time` float DEFAULT NULL,
     `chunk_index` varchar(200) DEFAULT NULL,
     `lower_boundary` text,
     `upper_boundary` text,
     `this_crc` char(40) NOT NULL,
     `this_cnt` int(11) NOT NULL,
     `master_crc` char(40) DEFAULT NULL,
     `master_cnt` int(11) DEFAULT NULL,
     `ts` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
     PRIMARY KEY (`db`,`tbl`,`chunk`),
     KEY `ts_db_tbl` (`ts`,`db`,`tbl`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8 

    表中的一条记录:

    +------------+-----------------+-------+------------+-------------+----------------+----------------+----------+----------+------------+------------+---------------------+
    | db     | tbl       | chunk | chunk_time | chunk_index | lower_boundary | upper_boundary | this_crc | this_cnt | master_crc | master_cnt | ts         |
    +------------+-----------------+-------+------------+-------------+----------------+----------------+----------+----------+------------+------------+---------------------+
    | db_kb | admin_info |   1 |  0.007406 | NULL    | NULL      | NULL      | 33d5c5be |    1 | 33d5c5be  |     1 | 2019-12-11 10:39:03 |
    +------------+-----------------+-------+------------+-------------+----------------+----------------+----------+----------+------------+------------+---------------------+

    定义一个struct OriginalData 用于接收表中的数据

    type OriginalData struct {
     db11      string
     tbl11      string
     chunk1     int
     chunk_time1   float64
     chunk_index1  sql.NullString
     lower_boundary1 sql.NullString
     upper_boundary1 sql.NullString
     this_crc1    sql.NullString
     this_cnt1    int
     master_crc1   sql.NullString
     master_cnt1   int
     ts1       mysql.NullTime   //"github.com/go-sql-driver/mysql"
    }

    拿到表中数据将其转换格式后用另一个struct DatacheckInfo 去接收,这便于操作这些数据

    type DatacheckInfo struct {
     Db1      string
     Tbl1      string
     Chunk     int
     Chunk_time   float64
     Chunk_index  string
     Lower_boundary string
     Upper_boundary string
     This_crc    string
     This_cnt    int
     Master_crc   string
     Master_cnt   int
     Ts       string
    }

    golang获取表中原始数据

    func SaveAlldata(rows *sql.Rows) []DatacheckInfo {
     var test OriginalData   //保存表中元数据
     var datalist []DatacheckInfo  //保存元数据转换后的数据
     for rows.Next() {
     var dataInfo DatacheckInfo
     rows.Scan(test.db11, test.tbl11, test.chunk1, test.chunk_time1, test.chunk_index1, test.lower_boundary1,
      test.upper_boundary1, test.this_crc1, test.this_cnt1, test.master_crc1, test.master_cnt1, test.ts1)
     dataInfo.Db1 = test.db11
     dataInfo.Tbl1 = test.tbl11
     dataInfo.Chunk = test.chunk1
     dataInfo.Chunk_time = test.chunk_time1
     //fmt.Println(test.chunk_time1)
     
     if test.chunk_index1.Valid {     //true 非null值
      dataInfo.Chunk_index = test.chunk_index1.String
     }else{                //false null值
      dataInfo.Chunk_index = "NULL"
     }
     if test.lower_boundary1.Valid{
      dataInfo.Lower_boundary = test.lower_boundary1.String
     }else {
      dataInfo.Lower_boundary = "NULL"
     }
     if test.upper_boundary1.Valid{
      dataInfo.Upper_boundary = test.upper_boundary1.String
     }else {
      dataInfo.Upper_boundary = "NULL"
     }
     if test.this_crc1.Valid{
      dataInfo.This_crc = test.this_crc1.String
     }else {
      dataInfo.This_crc = "NULL"
     }
     dataInfo.This_cnt = test.this_cnt1
     if test.master_crc1.Valid{
      dataInfo.Master_crc = test.master_crc1.String
     }else {
      dataInfo.Master_crc = "NULL"
     }
     dataInfo.Master_cnt = test.master_cnt1
     
     //fmt.Println(test.ts1, reflect.TypeOf(test.ts1.Valid), reflect.TypeOf(test.ts1.Time))
     if test.ts1.Valid {
      dataInfo.Ts = test.ts1.Time.Format("2006-01-02 15:04:05")
     }else{
      dataInfo.Ts = "NULL"
     }
     datalist = append(datalist,dataInfo)
     fmt.Println(dataInfo)
     } 
     return datalist
    }
     
    func Selectalldata(sdb *sql.DB, ipval string){  //CheckdataDiffsendding()
      //*******省略连接数据库的操作
     rows, err := sdb.Query("SELECT * FROM checksum_mengyao")
     defer rows.Close()
     dataInfo := SaveAlldata(rows) 
    }

    以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。如有错误或未考虑完全的地方,望不吝赐教。

    您可能感兴趣的文章:
    • Go语言模型:string的底层数据结构与高效操作详解
    • Golang 使用gorm添加数据库排他锁,for update
    • golang Gorm与数据库完整性约束详解
    • xorm根据数据库生成go model文件的操作
    • Golang连接Redis数据库的方法
    • golang中连接mysql数据库
    • Go语言获取系统性能数据gopsutil库的操作
    上一篇:golang使用aes库实现加解密操作
    下一篇:go 读取BMP文件头二进制读取方式
  • 相关文章
  • 

    关于我们 | 付款方式 | 荣誉资质 | 业务提交 | 代理合作


    © 2016-2020 巨人网络通讯

    时间:9:00-21:00 (节假日不休)

    地址:江苏信息产业基地11号楼四层

    《增值电信业务经营许可证》 苏B2-20120278

    X

    截屏,微信识别二维码

    微信号:veteran88

    (点击微信号复制,添加好友)

     打开微信