go语言数据库查询后对结果的处理方法的探讨

  • 问题

go语言提供标准接口以及有第三方的驱动实现了对mysql等数据库的操作,对于数据查询结果的处理,比较蛋疼,先看示例代码,假设有这样的表student:

go语言数据库查询后对结果的处理方法的探讨_第1张图片

建表的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库方便的处理。不知朋友们还有没有更好的方法?

你可能感兴趣的:(go语言数据库查询后对结果的处理方法的探讨)