go语言提供标准接口以及有第三方的驱动实现了对mysql等数据库的操作,对于数据查询结果的处理,比较蛋疼,先看示例代码,假设有这样的表student:
建表的sql如下:
CREATE TABLE `student` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(64) NOT NULL,
`nick` varchar(64) DEFAULT NULL,
`country` varchar(128) DEFAULT NULL,
`province` varchar(64) DEFAULT NULL,
`city` varchar(64) DEFAULT NULL,
`img_url` varchar(256) DEFAULT NULL,
`status` int(11) DEFAULT NULL,
`create_time` varchar(32) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
手动插入一些数据做查询代码测试用,也可写代码插入数据,这里只讨论查询结果的处理,因此采用sql插入的方式:
INSERT INTO `student` VALUES ('1', 'Jack', 'Jack', 'England', '', '', 'http://img2.imgtn.bdimg.com/it/u=3588772980,2454248748&fm=27&gp=0.jpg', '1', '2018-06-26 17:08:35');
INSERT INTO `student` VALUES ('2', 'Emily', 'Emily', 'England', '', '', 'http://img2.imgtn.bdimg.com/it/u=3588772980,2454248748&fm=27&gp=0.jpg', '2', null);
INSERT INTO `student` VALUES ('3', 'Jobs', 'Jobs', 'America', '', '', 'http://img2.imgtn.bdimg.com/it/u=3588772980,2454248748&fm=27&gp=0.jpg', '3', null);
INSERT INTO `student` VALUES ('4', 'Cook', 'Cook', 'America', '', '', 'http://img2.imgtn.bdimg.com/it/u=3588772980,2454248748&fm=27&gp=0.jpg', '1', null);
好,准备工作做完,现在开始编写查询的代码。
首先定义一个struct
type Student struct {
Id int
Name string
Nick string
Country string
Province string
City string
ImgUrl string
Status int
CreateTime string
}
定义一个函数listStudent()
func listStudent() {
dns := fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8", "dbuser", "dbpwd", "dbip", "dbname")
db, err := sql.Open("mysql", dns)
if err != nil {
fmt.Println(err)
panic(err)
}
defer db.Close()
err = db.Ping()
if err != nil {
fmt.Println(err)
panic(err)
}
rows, err := db.Query("SELECT * FROM student")
if err != nil {
fmt.Println(err)
panic(err)
}
for rows.Next() {
var student Student
err = rows.Scan(&student.Id, &student.Name, &student.Nick, &student.Country, &student.Province, &student.City, &student.ImgUrl, &student.Status, &student.CreateTime)
if err != nil {
fmt.Println(err)
panic(err)
}
fmt.Println(student)
}
}
这是一般的处理方式,在rows.Scan时,传入的参数个数必须与SELECT返回的字段个数一致,否则会报错,如上面代码修改为:
err = rows.Scan(&student.Id, &student.Name, &student.Nick, &student.Country, &student.Province, &student.City, &student.ImgUrl, &student.Status)
去掉了最后一个字段student.CreateTime,运行时报错:
sql: expected 9 destination arguments in Scan, not 8
意思就是期望有9个参数,Scan只接收到了8个参数,这是由于SELECT * FROM student,返回该表的所有字段(9个),所以Scan也必须传入9个字段,且读取的顺序必须与SELECT结果一致,这是比较蛋疼的地方,为什么这么说?假如这个表有十几个字段或者需要联合几个表一起查询,字段的数量比较多时,这个函数用起来就非常不爽了;或者假如我只需用到其中的几个字段时,你的SELECT字段就必须明确,不能用SELECT *。
为此,做了如下改进,借鉴java jdbc中的Mapper思路,增加如下函数:
func DoRowsMapper(rows *sql.Rows) ([]*Student) {
// 获取列名
columns, err := rows.Columns()
if err != nil {
panic(err.Error()) // proper error handling instead of panic in your app
}
// Make a slice for the values
values := make([]sql.RawBytes, len(columns))
// rows.Scan wants '[]interface{}' as an argument, so we must copy the
// references into such a slice
// See http://code.google.com/p/go-wiki/wiki/InterfaceSlice for details
scanArgs := make([]interface{}, len(values))
for i := range values {
scanArgs[i] = &values[i]
}
// Fetch rows
var res []*Student
for rows.Next() {
// get RawBytes from data
err = rows.Scan(scanArgs...)
if err != nil {
panic(err.Error()) // proper error handling instead of panic in your app
}
// 这个map用来存储一行数据,列名为map的key,map的value为列的值
rowMap := make(map[string]string)
var value string
for i, col := range values {
// Here we can check if the value is nil (NULL value)
if col != nil {
value = string(col)
rowMap[columns[i]] = value
}
}
// 赋值
var stu Student
stu.Id, _ = strconv.Atoi(rowMap["id"])
stu.Name = rowMap["name"]
stu.Nick = rowMap["nick"]
stu.Country = rowMap["country"]
stu.Province = rowMap["province"]
stu.City = rowMap["city"]
stu.ImgUrl = rowMap["img_url"]
stu.Status, _ = strconv.Atoi(rowMap["status"])
stu.CreateTime = rowMap["create_time"]
res = append(res, &stu)
}
return res
}
这里使用sql.RawBytes来读取一行记录中每列数据的值,转为string,然后存入一个map中,map的key为列名,这样,只需通过列名找到需要的字段及值,进行转换即可。
listStudent函数修改为:
func listStudent() {
...
/*
for rows.Next() {
var student Student
err = rows.Scan(&student.Id, &student.Name, &student.Nick, &student.Country, &student.Province, &student.City, &student.ImgUrl, &student.Status, &student.CreateTime)
if err != nil {
fmt.Println(err)
panic(err)
}
fmt.Println(student)
}
*/
stus := DoRowsMapper(rows)
for _, v := range stus {
fmt.Println(*v)
}
}
以上方法看似解决了,是否有更好的方式呢?这时我想到了反射。
那么,Student结构修改为:
type Student struct {
Id int `db:"id"`
Name string `db:"name"`
Nick string `db:"nick"`
Country string `db:"country"`
Province string `db:"province"`
City string `db:"city"`
ImgUrl string `db:"img_url"`
Status int `db:"status"`
CreateTime string `db:"create_time"`
}
即每个字段加上了tag,这个tag跟表student中的字段名是对应的,这样,我们可以在rowMap中根据tag名找到对应的列值,通过反射设置到对象的字段中,详细代码如下:
func DoRowsMapper(rows *sql.Rows) ([]*Student) {
...
for rows.Next() {
...
// 赋值
var stu Student
t := reflect.TypeOf(stu)
v := reflect.ValueOf(&stu).Elem() // 为了改变对象的内部值,需使用引用
for i := 0; i < t.NumField(); i++ {
f := v.Field(i)
fieldName := t.Field(i).Tag.Get("db")
if f.Kind() == reflect.Int {
val, _ := strconv.Atoi(rowMap[fieldName]) // 通过tag获取列数据
f.SetInt(int64(val))
} else if f.Kind() == reflect.String {
f.SetString(rowMap[fieldName])
}
}
/*
stu.Id, _ = strconv.Atoi(rowMap["id"])
stu.Name = rowMap["name"]
stu.Nick = rowMap["nick"]
stu.Country = rowMap["country"]
stu.Province = rowMap["province"]
stu.City = rowMap["city"]
stu.ImgUrl = rowMap["img_url"]
stu.Status, _ = strconv.Atoi(rowMap["status"])
stu.CreateTime = rowMap["create_time"]
*/
res = append(res, &stu)
}
return res
}
上面的代码我就省略掉重复的了,至此,对数据库查询结果的处理方法的改进到此完成。当然,可以使用第三方封装好的orm库方便的处理。不知朋友们还有没有更好的方法?