本文主要介紹如何使用go語言database/sql庫從數(shù)據(jù)庫中讀取null值的問題,以及如何向數(shù)據(jù)庫中插入null值。本文在這里使用的是sql.NullString, sql.NullInt64, sql.NullFloat64等結構體,為了方便書寫,它們的泛指我會使用sql.Null***來表示
要點
從數(shù)據(jù)庫讀取可能為null值得值時,可以選擇使用sql.NULL***來讀??;或者使用IFNULL、COALESCE等命令讓數(shù)據(jù)庫查詢值返回不為”“或者NULL
若需要往數(shù)據(jù)庫中插入null值,則依然可以使用sql.NULL***存儲所需的值,然后進行插入NULL值
直接使用sql.NULL***類型容易出現(xiàn)valid遺漏設置等問題,普通int、string與其轉換時,請寫幾個簡單的get、set函數(shù)
本demo使用的數(shù)據(jù)庫表以及數(shù)據(jù)如下
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)
從數(shù)據(jù)庫中讀取NULL值
如果不作處理直接從數(shù)據(jù)庫中讀取NULL值到string/int,會發(fā)生如下錯誤錯誤
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來讀取數(shù)據(jù)庫內容
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),假如發(fā)現(xiàn)存儲的值是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()解釋:返回參數(shù)中的第一個非空表達式(從左向右依次類推)
IFNULL(expr1,expr2):如果expr1不是NULL,IFNULL()返回expr1,否則它返回expr2。IFNULL()返回一個數(shù)字或字符串值,取決于它被使用的上下文環(huán)境。
查詢語句使用一個默認值來替換NULL即可
SELECT first_name, COALESCE(age, 0) FROM person;//
SELECT first_name, IFNULL(age, 0) FROM person;//
往數(shù)據(jù)庫中插入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)
}
//數(shù)據(jù)庫插入結果
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還是有很多危害的,再回顧下數(shù)據(jù)庫中使用NULL值的危害
為什么不建議使用NULL
所有使用NULL值的情況,都可以通過一個有意義的值的表示,這樣有利于代碼的可讀性和可維護性,并能從約束上增強業(yè)務數(shù)據(jù)的規(guī)范性。
NULL值在timestamp類型下容易出問題,特別是沒有啟用參數(shù)explicit_defaults_for_timestamp
NOT IN、!= 等負向條件查詢在有 NULL 值的情況下返回永遠為空結果,查詢容易出錯
Null 列需要更多的存儲空間:需要一個額外字節(jié)作為判斷是否為 NULL 的標志位
NULL值到非NULL的更新無法做到原地更新,更容易發(fā)生索引分裂,從而影響性能。
PS:但把NULL列改為NOT NULL帶來的性能提示很小,除非確定它帶來了問題,否則不要把它當成優(yōu)先的優(yōu)化措施,最重要的是使用的列的類型的適當性。
當然有些情況是不得不使用NULL值進行存儲,或者在查詢時由于left/right join等導致NULL值,但總體來說,能少用就少用。
helper func(提升效率/減少錯誤)
如果使用sql.NULL***的話,由于其有兩個字段,如果直接手動賦值的話還是很容易遺漏,所以還是需要簡單的轉換函數(shù),這里給了兩個簡單的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數(shù)據(jù)庫中的NULL, nil,time類型的值
在用golang獲取數(shù)據(jù)庫的數(shù)據(jù)的時候,難免會遇到可控field。這個時候拿到的數(shù)據(jù)如果直接用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 用于接收表中的數(shù)據(jù)
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"
}
拿到表中數(shù)據(jù)將其轉換格式后用另一個struct DatacheckInfo 去接收,這便于操作這些數(shù)據(jù)
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獲取表中原始數(shù)據(jù)
func SaveAlldata(rows *sql.Rows) []DatacheckInfo {
var test OriginalData //保存表中元數(shù)據(jù)
var datalist []DatacheckInfo //保存元數(shù)據(jù)轉換后的數(shù)據(jù)
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()
//*******省略連接數(shù)據(jù)庫的操作
rows, err := sdb.Query("SELECT * FROM checksum_mengyao")
defer rows.Close()
dataInfo := SaveAlldata(rows)
}
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。如有錯誤或未考慮完全的地方,望不吝賜教。
您可能感興趣的文章:- Go語言模型:string的底層數(shù)據(jù)結構與高效操作詳解
- Golang 使用gorm添加數(shù)據(jù)庫排他鎖,for update
- golang Gorm與數(shù)據(jù)庫完整性約束詳解
- xorm根據(jù)數(shù)據(jù)庫生成go model文件的操作
- Golang連接Redis數(shù)據(jù)庫的方法
- golang中連接mysql數(shù)據(jù)庫
- Go語言獲取系統(tǒng)性能數(shù)據(jù)gopsutil庫的操作