目标:实现一个通用查询。传入任意的原生SQL,使其能基于 gorm 上获取结果
一、gorm 能否直接实现?
首先,我们分析一下,首先要支持任意原生SQL,然后返回结果无法确定结构体。
很多人说,gorm 本身支持原生SQL查询啊。是的没错,但是实现上却有一定的限制,通过阅读文档,发现以下两种查询方式:
// 第一种
type Result struct {
ID int
Name string
Age int
}
var result Result
db.Raw("SELECT id, name, age FROM users WHERE id = ?", 3).Scan(&result)
// 第二种
result := map[string]interface{}{}
db.Model(&User{}).First(&result)
// SELECT * FROM `users` ORDER BY `users`.`id` LIMIT 1
首先第一种,满足了传入原生SQL查询,但是需要定义结构体。也就意味着,你必须得知道你的SQL查的到底是什么,无法做到通用查询。
然后我们看看第二种,第二种可以返回任意结果。但是却无法指定原生SQL
不可兼得啊,难搞。
二、利用反射间接实现
重点来了。经过向身边大神的请教,发现有这么一种实现的方式:
func scanRows2map(rows *sql.Rows) []map[string]interface{} {
res := make([]map[string]interface{}, 0) // 定义结果 map
colTypes, _ := rows.ColumnTypes() // 列信息
var rowParam = make([]interface{}, len(colTypes)) // 传入到 rows.Scan 的参数 数组
var rowValue = make([]interface{}, len(colTypes)) // 接收一行数据的数组
for i, colType := range colTypes {
rowValue[i] = reflect.New(colType.ScanType()) // 跟据数据库参数类型,创建默认值 和类型
rowParam[i] = reflect.ValueOf(&rowValue[i]).Interface() // 跟据接收的数据的类型反射出值的地址
}
// 遍历
for rows.Next() {
rows.Scan(rowParam...)
record := make(map[string]interface{})
for i, colType := range colTypes {
if rowValue[i] == nil { // 如果是 nil 用空字符串代替
record[colType.Name()] = ""
} else {
record[colType.Name()] = rowValue[i]
//record[colType.Name()] = Byte2Str(rowValue[i].([]byte))
}
}
res = append(res, record)
}
return res
}
我们分析以下以上的代码,首先,传入的参数是一个 sql.rows 类型变量,返回的是一个 []map[string]interface{}。其中
rows 可以通过 db.Raw(sql).Rows() 获得。因此这个函数可以满足我们的需求。
看起来好像挺完美的,但是我们实际上去使用一下。发现了一些奇怪的地方。我将获取到 res(类型为[]map[string]interface{}) 进行了 Marshal
即:
...//此处省略数据库连接等操作
rows, _ := db.Raw("select * from admin").Rows()
fmt.Println(rows)
res := scanRows2map(rows)
jsonString, _ := json.Marshal(res)
fmt.Println(jsonString)
得到的结果是:
[{"id":"MQ==","name":"YWRtaW4x","pass":"MTExMTEx"},{"id":"Mg==","name":"YWRtaW4y","pass":"MjIyMjIy"}]
熟悉的模样,这不是被base64编码了吗?
一开始以为是在某一层做了数据处理,debug 了一下,查出来的数据是正常的,Marshal 之后才被编码。下班回家后忍不住想查查原因,搜到这几句话:
将一个对象编码成JSON数据,接受一个interface{}对象,返回[]byte和error: func Marshal(v interface{}) ([]byte, error) Marshal函数将会递归遍历整个对象,依次按成员类型对这个对象进行编码,类型转换规则如下:
- bool类型 转换为JSON的Boolean
- 整数,浮点数等数值类型 转换为JSON的Number
- string 转换为JSON的字符串(带""引号)
- struct 转换为JSON的Object,再根据各个成员的类型递归打包
- 数组或切片 转换为JSON的Array
- []byte 会先进行base64编码然后转换为JSON字符串
- map 转换为JSON的Object,key必须是string
- interface{} 按照内部的实际类型进行转换 nil 转为JSON的null channel,func等类型 会返回UnsupportedTypeError
[]byte 会被base64编码再转换为json字符串。原因终于找到了。尝试断言了一下,几个值确实是[]byte 类型的。
在这里我再说一下 []byte 是什么类型:
byte 又称 uint8 类型,代表了 ASCII 码的一个字符。
那么如何解决这个问题呢?
既然 []byte 有这种问题,那么我们就转换一个,把 []byte 转为string 类型,就可以避过这种问题了。
改写一下上面的代码:
func scanRows2map(rows *sql.Rows) []map[string]string {
res := make([]map[string]string, 0) // 定义结果 map
colTypes, _ := rows.ColumnTypes() // 列信息
var rowParam = make([]interface{}, len(colTypes)) // 传入到 rows.Scan 的参数 数组
var rowValue = make([]interface{}, len(colTypes)) // 接收数据一行列的数组
for i, colType := range colTypes {
rowValue[i] = reflect.New(colType.ScanType()) // 跟据数据库参数类型,创建默认值 和类型
rowParam[i] = reflect.ValueOf(&rowValue[i]).Interface()// 跟据接收的数据的类型反射出值的地址
}
// 遍历
for rows.Next() {
rows.Scan(rowParam...) // 赋值到 rowValue 中
record := make(map[string]string)
for i, colType := range colTypes {
if rowValue[i] == nil {
record[colType.Name()] = ""
} else {
record[colType.Name()] = Byte2Str(rowValue[i].([]byte))
}
}
res = append(res, record)
}
return res
}
// []byte to string
func Byte2Str(b []byte) string {
return *(*string)(unsafe.Pointer(&b))
}
重新执行,获得结果是:
[{"id":"1","name":"admin1","pass":"111111"},{"id":"2","name":"admin2","pass":"222222"}]
此时,问题搞定,对于写过PHP的人来说,go 确实是事多。但是感觉比较有趣,在此记录一下。
确实查了网上好久,没找到解决方式,多亏公司 go 大神帮助指导,哈哈哈