解決Go語言數(shù)據(jù)庫中null值的問題
本文主要介紹如何使用go語言database/sql庫從數(shù)據(jù)庫中讀取null值的問題,以及如何向數(shù)據(jù)庫中插入null值。本文在這里使用的是sql.NullString, sql.NullInt64, sql.NullFloat64等結(jié)構(gòu)體,為了方便書寫,它們的泛指我會(huì)使用sql.Null***來表示
要點(diǎn)
從數(shù)據(jù)庫讀取可能為null值得值時(shí),可以選擇使用sql.NULL***來讀?。换蛘呤褂肐FNULL、COALESCE等命令讓數(shù)據(jù)庫查詢值返回不為”“或者NULL
若需要往數(shù)據(jù)庫中插入null值,則依然可以使用sql.NULL***存儲(chǔ)所需的值,然后進(jìn)行插入NULL值
直接使用sql.NULL***類型容易出現(xiàn)valid遺漏設(shè)置等問題,普通int、string與其轉(zhuǎn)換時(shí),請寫幾個(gè)簡單的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,會(huì)發(fā)生如下錯(cuò)誤錯(cuò)誤
Scan NULL值到string的報(bào)錯(cuò)
sql: Scan error on column index 1: unsupported Scan, storing driver.Value type <nil> into type *string
Scan NULL值到int的報(bào)錯(cuò)
sql: Scan error on column index 1: converting driver.Value type <nil> ("<nil>") to a int: invalid syntax
使用如下的struct來讀取數(shù)據(jù)庫內(nèi)容
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)
運(yùn)行代碼,可以通過日志看出來,錯(cuò)誤來自Scan將NULL值賦值給int或者string時(shí),報(bào)錯(cuò);解決這個(gè)問題可以使用sql原生結(jié)構(gòu)體sql.Null***來解決
使用sqlNull***
sql.Null***在sql庫中聲明如下,在讀取時(shí),(比如讀取的值存儲(chǔ)到NullInt64),假如發(fā)現(xiàn)存儲(chǔ)的值是NULL,則會(huì)將NullInt64的valid設(shè)置為false,然后不會(huì)將值存儲(chǔ)到Int64中,Int64值默認(rèn)為0,如果是NullString則String值時(shí)nil;如果是正常值,則會(huì)將Valid賦值為true,將值存儲(chǔ)到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)
輸出結(jié)果
{yousa 0 { false} {0 false}}
{yousa 0 { false} {0 false}}
使用IFNULL或者COALESCE
coalesce()解釋:返回參數(shù)中的第一個(gè)非空表達(dá)式(從左向右依次類推)
IFNULL(expr1,expr2):如果expr1不是NULL,IFNULL()返回expr1,否則它返回expr2。IFNULL()返回一個(gè)數(shù)字或字符串值,取決于它被使用的上下文環(huán)境。
查詢語句使用一個(gè)默認(rè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ù)庫插入結(jié)果 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的過程:
首先它會(huì)調(diào)用hello.lastNullName的Value方法,獲取到driver.Value,然后檢驗(yàn)Valid值是true還是false,如果是false則會(huì)返回一個(gè)nil值(nil值傳給sql driver會(huì)被認(rèn)為是NULL值),如果是true則會(huì)將hello.lastNullName.String的值傳過去。
PS: 為了保證你所插入的值能如你所期望是NULL值,一定記得要將sql.Null***中Valid值置為false
使用NULL還是有很多危害的,再回顧下數(shù)據(jù)庫中使用NULL值的危害
為什么不建議使用NULL
所有使用NULL值的情況,都可以通過一個(gè)有意義的值的表示,這樣有利于代碼的可讀性和可維護(hù)性,并能從約束上增強(qiáng)業(yè)務(wù)數(shù)據(jù)的規(guī)范性。
NULL值在timestamp類型下容易出問題,特別是沒有啟用參數(shù)explicit_defaults_for_timestamp
NOT IN、!= 等負(fù)向條件查詢在有 NULL 值的情況下返回永遠(yuǎn)為空結(jié)果,查詢?nèi)菀壮鲥e(cuò)
Null 列需要更多的存儲(chǔ)空間:需要一個(gè)額外字節(jié)作為判斷是否為 NULL 的標(biāo)志位
NULL值到非NULL的更新無法做到原地更新,更容易發(fā)生索引分裂,從而影響性能。
PS:但把NULL列改為NOT NULL帶來的性能提示很小,除非確定它帶來了問題,否則不要把它當(dāng)成優(yōu)先的優(yōu)化措施,最重要的是使用的列的類型的適當(dāng)性。
當(dāng)然有些情況是不得不使用NULL值進(jìn)行存儲(chǔ),或者在查詢時(shí)由于left/right join等導(dǎo)致NULL值,但總體來說,能少用就少用。
helper func(提升效率/減少錯(cuò)誤)
如果使用sql.NULL***的話,由于其有兩個(gè)字段,如果直接手動(dòng)賦值的話還是很容易遺漏,所以還是需要簡單的轉(zhuǎn)換函數(shù),這里給了兩個(gè)簡單的helper fuc,分別是將int64轉(zhuǎn)換成NullInt64和將string轉(zhuǎn)換成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} }
補(bǔ)充:golang 處理mysql數(shù)據(jù)庫中的NULL, nil,time類型的值
在用golang獲取數(shù)據(jù)庫的數(shù)據(jù)的時(shí)候,難免會(huì)遇到可控field。這個(gè)時(shí)候拿到的數(shù)據(jù)如果直接用string, time.Time這樣的類型來解析的話會(huì)遇到panic。
下面的方法會(huì)解決這種問題:
表結(jié)構(gòu):
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 | +------------+-----------------+-------+------------+-------------+----------------+----------------+----------+----------+------------+------------+---------------------+
定義一個(gè)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ù)將其轉(zhuǎn)換格式后用另一個(gè)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ù)轉(zhuǎn)換后的數(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) }
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教。
相關(guān)文章
基于Go Int轉(zhuǎn)string幾種方式性能測試
這篇文章主要介紹了Go Int轉(zhuǎn)string幾種方式測試,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2021-04-04go語言goto語句跳轉(zhuǎn)到指定的標(biāo)簽實(shí)現(xiàn)方法
這篇文章主要介紹了go語言goto語句跳轉(zhuǎn)到指定的標(biāo)簽實(shí)現(xiàn)方法,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-05-05Air實(shí)現(xiàn)Go程序?qū)崟r(shí)熱重載使用過程解析示例
這篇文章主要為大家介紹了Air實(shí)現(xiàn)Go程序?qū)崟r(shí)熱重載使用過程解析示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步早日升職加薪2022-04-04Golang使用crypto/ed25519實(shí)現(xiàn)數(shù)字簽名和驗(yàn)證
本文將深入探討如何在?Golang?中使用?crypto/ed25519?進(jìn)行數(shù)字簽名和驗(yàn)證,我們將從基本原理開始,逐步引導(dǎo)讀者了解生成密鑰對、進(jìn)行數(shù)字簽名,以及驗(yàn)證簽名的具體過程,希望對大家有所幫助2024-02-02Go語言實(shí)現(xiàn)登錄驗(yàn)證代碼案例
這篇文章主要介紹了Go語言實(shí)現(xiàn)登錄驗(yàn)證代碼案例,代碼和圖文講解的很清晰,有感興趣的可以學(xué)習(xí)下2021-03-03