gorm 如何实现支持任意原生SQL查询

目标:实现一个通用查询。传入任意的原生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 大神帮助指导,哈哈哈

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

MClink

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值