解决Go语言数据库中null值的问题

本文主要介绍如何使用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)
}

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

(0)

相关推荐

  • xorm根据数据库生成go model文件的操作

    你懂的,手工翻译表定义到go结构体是很枯燥的. so,用xorm搞定. go get github.com/go-xorm/cmd/xorm 安装以下依赖,用到哪个装哪个. github.com/go-xorm/xorm 驱动 Mysql: github.com/go-sql-driver/mysql Postgres: github.com/lib/pq SQLite: github.com/mattn/go-sqlite3 MSSQL: github.com/denisenkom/go-ms

  • golang中连接mysql数据库

    golang中连接mysql数据库,需要使用一个第三方类库github.com/go-sql-driver/mysql,在这个类库中就实现了mysql的连接池,并且只需要设置两个参数就可以实现 一般连接mysql首先需要调用sql.Open函数,但是此时并没有真正的去连接mysql,而是只创建了一个Db的对象而已.当执行Query或者是Exec方法时,才会去真正的连接数据库. 默认情况下.每次执行sql语句,都会创建一条tcp连接,执行结束就会断掉连接,但是会保留两条连接闲置.当下次再执行 sq

  • golang Gorm与数据库完整性约束详解

    数据库约束要点: 主键约束(非空且唯一)外键约束 子表外键字段的值必须在主表被参照字段值得范围内,或者为NULL:外键参照的必须是主表的主键或唯一键:主表主键/唯一键被子表参照时,主表相应记录不允许被删除 在golang中,采用orm对数据库进行建模是比较方便的.grom是其中一个比较流行的orm工具. 本篇基于golang.grom1.91.和PostgreSQL来进行说明. 注:本文的例子是极端情况,一般情况只是单字段主键. 1.实体完整性: 每个关系(表)有且仅有一个主键,每一个主键值必须

  • Golang 使用gorm添加数据库排他锁,for update

    适用于先读后更新的数据竞争场景,且应该将加锁操作放到事务中,防止锁被自动释放,原因参考mysql doc func UpdateUser(db *gorm.DB, id int64) error { tx := db.Begin() defer func() { if r := recover(); r != nil { tx.Rollback() } }() if err := tx.Error; err != nil { return err } user := User{} // 锁住指定

  • Golang连接Redis数据库的方法

    Golang连接Redis数据库 golang连接数据库,这里博主推荐使用go-redis这个库,理由很简单(连接数据库的操作类似在数据库里面输入命令) go-redis的安装方式 go get -v https://github.com/go-redis/redis -v小v,是输出过程.一般情况下不带-v什么反馈都看不到. 连接redis的方式 package ... import ( "github.com/go-redis/redis" ) func main() { clie

  • 解决Go语言数据库中null值的问题

    本文主要介绍如何使用go语言database/sql库从数据库中读取null值的问题,以及如何向数据库中插入null值.本文在这里使用的是sql.NullString, sql.NullInt64, sql.NullFloat64等结构体,为了方便书写,它们的泛指我会使用sql.Null***来表示 要点 从数据库读取可能为null值得值时,可以选择使用sql.NULL***来读取:或者使用IFNULL.COALESCE等命令让数据库查询值返回不为""或者NULL 若需要往数据库中插入

  • SQL 中 NULL值测试代码

    刚刚想从数据库中的表EXPERT_DETAILS中检索出修改人Modifier(类型 VARCHAR2(20),可为空)为空的那些记录,因为该字段的类型为VARCHAR2(20),我使用的SQL语句为 复制代码 代码如下: select * from expert_details twhere t.modifier = '' 没有检索出一条记录,而这与存储在该表中的记录是不相符的.后来想到即便是空字符型存储在数据库中也应该是NULL而不是''. 然后我使用下列SQL 语句,仍然没有检索出一条记录

  • MySQL数据库中null的知识点总结

    在mysql数据库中,null是一个经常出现的情况,关于mysql中的null,有哪些注意事项呢?下面简单总结归纳下,后续会不断补充. 1. is null 首先判断数据库中某一列的值是否为null,不能用等于来判断,必须使用 is.比如,select * from users where user_name is null 或者 select * from users where user_name is not null,而不能是 select * from users where user

  • 教你解决往mysql数据库中存入汉字报错的方法

    一.遇到的问题 在向数据库中存入汉字时遇到这样的问题: Cause: java.sql.SQLException: Incorrect string value: '\xE6\x9F\xAF\xE5\x8D\x97' for column 'user_name' at row 1 二.分析问题 这是由于数据库设计有问题导致的,当初创建数据库的时候直接选择的默认,没有修改为utf-8,后来尝试手动修改还是不行. 尝试把数据库和表的默认字段改为utf8,但是还是不能存汉字. 三.真正的问题 真正的问

  • 解决Properties属性文件中的值有等号和换行的小问题

    目录 Properties属性文件中的值有等号和换行 Properties属性文件可以这样写 处理properties文件中key包含空格和等号的情况 处理方案 Properties属性文件中的值有等号和换行 Spring配置Shiro的过滤器时,有个filterChainDefinitions属性,值中有等号有换行,尝试写到Properties属性文件中遇到问题 <!-- 配置shiro过滤器 --> <bean id="shiroFilter" class=&qu

  • JS中获取数据库中的值的方法

    实施过程如下:    1.引用Ajax.dll    2.在App_Code写具体的方法,最好单独建立一个类文件,然后写具体方法.       复制代码 代码如下: public class AjaxMethod www.jb51.net{    public AjaxMethod()    {        //        //TODO: 在此处添加构造函数逻辑        //    }    [Ajax.AjaxMethod(Ajax.HttpSessionStateRequirem

  • Oracle数据库中对null值的排序及mull与空字符串的区别

    order by排序之null值处理方法 在对业务数据排序时候,发现有些字段的记录是null值,这时排序便出现了有违我们使用习惯的数据大小顺序问题.在Oracle中规定,在Order by排序时缺省认为null是最大值,所以如果是ASC升序则被排在最后,而DESC降序则排在最前.所以,为何分析数据的直观性方便性,我们需要对null的记录值进行相应处理. 这是四种oracle排序中NULL值处理的方法: 1.使用nvl函数 语法:Nvl(expr1, expr2)     若EXPR1是NULL,

  • thinkphp实现把数据库中的列的值存到下拉框中的方法

    1. 先去数据库中查值,查询整个数据表,结果为二维数组. $project = M("project"); $cell = $project->where(array('status'=>1))->order("id desc")->select(); //var_dump($cell); $this->assign('cell',$cell); 2.前台获取遍历 <select class="test" st

  • 数据库中聚簇索引与非聚簇索引的区别[图文]

    在<数据库原理>里面,对聚簇索引的解释是:聚簇索引的顺序就是数据的物理存储顺序,而对非聚簇索引的解释是:索引顺序与数据物理排列顺序无关.正式因为如此,所以一个表最多只能有一个聚簇索引. 不过这个定义太抽象了.在SQL Server中,索引是通过二叉树的数据结构来描述的,我们可以这么理解聚簇索引:索引的叶节点就是数据节点.而非聚簇索引的叶节点仍然是索引节点,只不过有一个指针指向对应的数据块.如下图: 非聚簇索引 聚簇索引 聚簇索引与非聚簇索引的本质区别到底是什么?什么时候用聚簇索引,什么时候用非

  • C#操作数据库中存取图片文件的方法

    本文实例讲述了C#操作数据库中存取图片文件的方法.分享给大家供大家参考.具体如下: private string sqlconnstr = "Data Source=.;Database=db_test;User id=sa;PWD=123456"; /*功能:把一种图片插入到数据库中 *返回值:无 */ void InsertImageToDB() { //将需要存储的图片读取为数据流 FileStream fs = new FileStream(@"D:/Bear.jpg

随机推荐